Service accounts: Split user and service account database (#46442)

* ServiceAccounts: remove unused endpoint

* ServiceAccounts: remove usage of getOrgUsers from service accounts

* use dialect for boolean str true in delete

* return service account results directly

* Move Service Account Deletions to sa package

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: gamab <gabi.mabs@gmail.com>

* Move service account methods to service accounts

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: gamab <gabi.mabs@gmail.com>

* Service accounts should not interfere with users

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* filter service accounts in user services

* mispell fix

* fix overextended lines

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* fix variable

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: gamab <gabi.mabs@gmail.com>
This commit is contained in:
Jguer 2022-03-14 17:24:07 +00:00 committed by GitHub
parent 99b4dfc27d
commit ef9fe26886
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 203 additions and 289 deletions

View File

@ -107,22 +107,20 @@ type UpdateOrgUserCommand struct {
// QUERIES
type GetOrgUsersQuery struct {
UserID int64
OrgId int64
Query string
Limit int
IsServiceAccount bool
UserID int64
OrgId int64
Query string
Limit int
User *SignedInUser
Result []*OrgUserDTO
}
type SearchOrgUsersQuery struct {
OrgID int64
Query string
Page int
Limit int
IsServiceAccount bool
OrgID int64
Query string
Page int
Limit int
User *SignedInUser
Result SearchOrgUsersQueryResult

View File

@ -55,7 +55,6 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
auth := acmiddleware.Middleware(api.accesscontrol)
api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) {
serviceAccountsRoute.Get("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeAll)), routing.Wrap(api.ListServiceAccounts))
serviceAccountsRoute.Get("/search", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead)), routing.Wrap(api.SearchOrgServiceAccountsWithPaging))
serviceAccountsRoute.Post("/", auth(middleware.ReqOrgAdmin,
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.CreateServiceAccount))
@ -128,27 +127,6 @@ func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) r
}
}
func (api *ServiceAccountsAPI) ListServiceAccounts(c *models.ReqContext) response.Response {
serviceAccounts, err := api.store.ListServiceAccounts(c.Req.Context(), c.OrgId, -1)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to list service accounts", err)
}
saIDs := map[string]bool{}
for i := range serviceAccounts {
serviceAccounts[i].AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccounts[i].Name)
saIDs[strconv.FormatInt(serviceAccounts[i].Id, 10)] = true
}
metadata := api.getAccessControlMetadata(c, saIDs)
if len(metadata) > 0 {
for i := range serviceAccounts {
serviceAccounts[i].AccessControl = metadata[strconv.FormatInt(serviceAccounts[i].Id, 10)]
}
}
return response.JSON(http.StatusOK, serviceAccounts)
}
func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata {
if api.accesscontrol.IsDisabled() || !c.QueryBool("accesscontrol") {
return map[string]accesscontrol.Metadata{}
@ -229,45 +207,26 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqC
if page < 1 {
page = 1
}
query := &models.SearchOrgUsersQuery{
OrgID: c.OrgId,
Query: c.Query("query"),
Page: page,
Limit: perPage,
User: c.SignedInUser,
IsServiceAccount: true,
}
serviceAccounts, err := api.store.SearchOrgServiceAccounts(ctx, query)
serviceAccountSearch, err := api.store.SearchOrgServiceAccounts(ctx, c.OrgId, c.Query("query"), page, perPage, c.SignedInUser)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to get service accounts for current organization", err)
}
saIDs := map[string]bool{}
for i := range serviceAccounts {
serviceAccounts[i].AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccounts[i].Name)
for i := range serviceAccountSearch.ServiceAccounts {
sa := serviceAccountSearch.ServiceAccounts[i]
sa.AvatarUrl = dtos.GetGravatarUrlWithDefault("", sa.Name)
saIDString := strconv.FormatInt(serviceAccounts[i].Id, 10)
saIDString := strconv.FormatInt(sa.Id, 10)
saIDs[saIDString] = true
metadata := api.getAccessControlMetadata(c, map[string]bool{saIDString: true})
serviceAccounts[i].AccessControl = metadata[strconv.FormatInt(serviceAccounts[i].Id, 10)]
tokens, err := api.store.ListTokens(ctx, serviceAccounts[i].OrgId, serviceAccounts[i].Id)
sa.AccessControl = metadata[strconv.FormatInt(sa.Id, 10)]
tokens, err := api.store.ListTokens(ctx, sa.OrgId, sa.Id)
if err != nil {
api.log.Warn("Failed to list tokens for service account", "serviceAccount", serviceAccounts[i].Id)
api.log.Warn("Failed to list tokens for service account", "serviceAccount", sa.Id)
}
serviceAccounts[i].Tokens = int64(len(tokens))
sa.Tokens = int64(len(tokens))
}
type searchOrgServiceAccountsQueryResult struct {
TotalCount int64 `json:"totalCount"`
ServiceAccounts []*serviceaccounts.ServiceAccountDTO `json:"serviceAccounts"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
result := searchOrgServiceAccountsQueryResult{
TotalCount: query.Result.TotalCount,
ServiceAccounts: serviceAccounts,
Page: query.Result.Page,
PerPage: query.Result.PerPage,
}
return response.JSON(http.StatusOK, result)
return response.JSON(http.StatusOK, serviceAccountSearch)
}

View File

@ -120,7 +120,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
serviceAccountRequestScenario(t, http.MethodPost, serviceAccountPath, testUser, func(httpmethod string, endpoint string, user *tests.TestUser) {
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
marshalled, err := json.Marshal(tc.body)
require.NoError(t, err)
@ -179,7 +179,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
}
serviceAccountRequestScenario(t, http.MethodDelete, serviceAccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
createduser := tests.SetupUserServiceAccount(t, store, testcase.user)
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store, database.NewServiceAccountsStore(store))
server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store, database.NewServiceAccountsStore(store))
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, fmt.Sprint(createduser.Id))).Code
require.Equal(t, testcase.expectedCode, actual)
})
@ -203,7 +203,7 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
}
serviceAccountRequestScenario(t, http.MethodDelete, serviceAccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
createduser := tests.SetupUserServiceAccount(t, store, testcase.user)
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store, database.NewServiceAccountsStore(store))
server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store, database.NewServiceAccountsStore(store))
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, createduser.Id)).Code
require.Equal(t, testcase.expectedCode, actual)
})
@ -218,7 +218,7 @@ func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint str
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
routerRegister routing.RouteRegister,
acmock *accesscontrolmock.Mock,
sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) *web.Mux {
sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) {
a := NewServiceAccountsAPI(setting.NewCfg(), svc, acmock, routerRegister, saStore)
a.RegisterAPIEndpoints(featuremgmt.WithFeatures(featuremgmt.FlagServiceAccounts))
@ -240,7 +240,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
c.Map(ctx)
})
a.RouterRegister.Register(m.Router)
return m
return m, a
}
func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
@ -309,7 +309,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
createdUser := tests.SetupUserServiceAccount(t, store, *tc.user)
scopeID = int(createdUser.Id)
}
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, scopeID))
@ -413,43 +413,40 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
serviceAccountRequestScenario(t, http.MethodPatch, serviceAccountIDPath, tc.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
scopeID := tc.Id
if tc.user != nil {
createdUser := tests.SetupUserServiceAccount(t, store, *tc.user)
scopeID = int(createdUser.Id)
}
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
server, saAPI := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
scopeID := tc.Id
if tc.user != nil {
createdUser := tests.SetupUserServiceAccount(t, store, *tc.user)
scopeID = int(createdUser.Id)
}
var rawBody io.Reader = http.NoBody
if tc.body != nil {
body, err := json.Marshal(tc.body)
require.NoError(t, err)
rawBody = bytes.NewReader(body)
}
var rawBody io.Reader = http.NoBody
if tc.body != nil {
body, err := json.Marshal(tc.body)
require.NoError(t, err)
rawBody = bytes.NewReader(body)
}
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, scopeID), rawBody)
actual := requestResponse(server, http.MethodPatch, fmt.Sprintf(serviceAccountIDPath, scopeID), rawBody)
actualCode := actual.Code
require.Equal(t, tc.expectedCode, actualCode)
actualCode := actual.Code
require.Equal(t, tc.expectedCode, actualCode)
if actualCode == http.StatusOK {
actualBody := map[string]interface{}{}
err := json.Unmarshal(actual.Body.Bytes(), &actualBody)
require.NoError(t, err)
assert.Equal(t, scopeID, int(actualBody["id"].(float64)))
assert.Equal(t, string(*tc.body.Role), actualBody["role"].(string))
assert.Equal(t, *tc.body.Name, actualBody["name"].(string))
assert.Equal(t, tc.user.Login, actualBody["login"].(string))
if actualCode == http.StatusOK {
actualBody := map[string]interface{}{}
err := json.Unmarshal(actual.Body.Bytes(), &actualBody)
require.NoError(t, err)
assert.Equal(t, scopeID, int(actualBody["id"].(float64)))
assert.Equal(t, string(*tc.body.Role), actualBody["role"].(string))
assert.Equal(t, *tc.body.Name, actualBody["name"].(string))
assert.Equal(t, tc.user.Login, actualBody["login"].(string))
// Ensure the user was updated in DB
query := models.GetOrgUsersQuery{UserID: int64(scopeID), OrgId: 1, IsServiceAccount: true}
err = store.GetOrgUsers(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, *tc.body.Name, query.Result[0].Name)
require.Equal(t, string(*tc.body.Role), query.Result[0].Role)
}
})
// Ensure the user was updated in DB
sa, err := saAPI.store.RetrieveServiceAccount(context.Background(), 1, int64(scopeID))
require.NoError(t, err)
require.Equal(t, *tc.body.Name, sa.Name)
require.Equal(t, string(*tc.body.Role), sa.Role)
}
})
}
}

View File

@ -130,7 +130,7 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
bodyString = string(b)
}
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, database.NewServiceAccountsStore(store))
actual := requestResponse(server, http.MethodPost, endpoint, strings.NewReader(bodyString))
actualCode := actual.Code
@ -221,7 +221,7 @@ func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
endpoint := fmt.Sprintf(serviceaccountIDTokensDetailPath, sa.Id, token.Id)
bodyString := ""
server := setupTestServer(t, svcMock, routing.NewRouteRegister(), tc.acmock, store, saStore)
server, _ := setupTestServer(t, svcMock, routing.NewRouteRegister(), tc.acmock, store, saStore)
actual := requestResponse(server, http.MethodDelete, endpoint, strings.NewReader(bodyString))
actualCode := actual.Code
@ -343,7 +343,7 @@ func TestServiceAccountsAPI_ListTokens(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
endpoint := fmt.Sprintf(serviceAccountIDPath+"/tokens", sa.Id)
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, &saStoreMockTokens{saAPIKeys: tc.tokens})
server, _ := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store, &saStoreMockTokens{saAPIKeys: tc.tokens})
actual := requestResponse(server, http.MethodGet, endpoint, http.NoBody)
actualCode := actual.Code

View File

@ -54,33 +54,37 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, org
Tokens: 0,
}, nil
}
func (s *ServiceAccountsStoreImpl) DeleteServiceAccount(ctx context.Context, orgID, serviceaccountID int64) error {
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
return deleteServiceAccountInTransaction(sess, orgID, serviceaccountID)
})
func ServiceAccountDeletions() []string {
deletes := []string{
"DELETE FROM api_key WHERE service_account_id = ?",
}
deletes = append(deletes, sqlstore.UserDeletions()...)
return deletes
}
func deleteServiceAccountInTransaction(sess *sqlstore.DBSession, orgID, serviceAccountID int64) error {
user := models.User{}
has, err := sess.Where(`org_id = ? and id = ? and is_service_account = true`, orgID, serviceAccountID).Get(&user)
if err != nil {
return err
}
if !has {
return serviceaccounts.ErrServiceAccountNotFound
}
for _, sql := range sqlstore.ServiceAccountDeletions() {
_, err := sess.Exec(sql, user.Id)
func (s *ServiceAccountsStoreImpl) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error {
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
user := models.User{}
has, err := sess.Where(`org_id = ? and id = ? and is_service_account = ?`,
orgID, serviceAccountID, s.sqlStore.Dialect.BooleanStr(true)).Get(&user)
if err != nil {
return err
}
}
return nil
if !has {
return serviceaccounts.ErrServiceAccountNotFound
}
for _, sql := range ServiceAccountDeletions() {
_, err := sess.Exec(sql, user.Id)
if err != nil {
return err
}
}
return nil
})
}
func (s *ServiceAccountsStoreImpl) UpgradeServiceAccounts(ctx context.Context) error {
basicKeys := s.sqlStore.GetNonServiceAccountAPIKeys(ctx)
basicKeys := s.sqlStore.GetAllOrgsAPIKeys(ctx)
if len(basicKeys) > 0 {
s.log.Info("Launching background thread to upgrade API keys to service accounts", "numberKeys", len(basicKeys))
go func() {
@ -96,7 +100,7 @@ func (s *ServiceAccountsStoreImpl) UpgradeServiceAccounts(ctx context.Context) e
}
func (s *ServiceAccountsStoreImpl) ConvertToServiceAccounts(ctx context.Context, keys []int64) error {
basicKeys := s.sqlStore.GetNonServiceAccountAPIKeys(ctx)
basicKeys := s.sqlStore.GetAllOrgsAPIKeys(ctx)
if len(basicKeys) == 0 {
return nil
}
@ -117,16 +121,29 @@ func (s *ServiceAccountsStoreImpl) ConvertToServiceAccounts(ctx context.Context,
}
func (s *ServiceAccountsStoreImpl) CreateServiceAccountFromApikey(ctx context.Context, key *models.ApiKey) error {
sa, err := s.sqlStore.CreateServiceAccountForApikey(ctx, key.OrgId, key.Name, key.Role)
if err != nil {
return fmt.Errorf("failed to create service account for API key with error : %w", err)
prefix := "sa-autogen-"
cmd := models.CreateUserCommand{
Login: fmt.Sprintf("%v-%v-%v", prefix, key.OrgId, key.Name),
Name: prefix + key.Name,
OrgId: key.OrgId,
DefaultOrgRole: string(key.Role),
IsServiceAccount: true,
}
err = s.sqlStore.UpdateApikeyServiceAccount(ctx, key.Id, sa.Id)
if err != nil {
return fmt.Errorf("failed to attach new service account to API key for keyId: %d and newServiceAccountId: %d with error: %w", key.Id, sa.Id, err)
newSA, errCreateSA := s.sqlStore.CreateUser(ctx, cmd)
if errCreateSA != nil {
return fmt.Errorf("failed to create service account: %w", errCreateSA)
}
s.log.Debug("Updated basic api key", "keyId", key.Id, "newServiceAccountId", sa.Id)
if errUpdateKey := s.assignApiKeyToServiceAccount(ctx, key.Id, newSA.Id); errUpdateKey != nil {
return fmt.Errorf(
"failed to attach new service account to API key for keyId: %d and newServiceAccountId: %d with error: %w",
key.Id, newSA.Id, errUpdateKey,
)
}
s.log.Debug("Updated basic api key", "keyId", key.Id, "newServiceAccountId", newSA.Id)
return nil
}
@ -136,9 +153,10 @@ func (s *ServiceAccountsStoreImpl) ListTokens(ctx context.Context, orgID int64,
err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
var sess *xorm.Session
quotedUser := s.sqlStore.Dialect.Quote("user")
sess = dbSession.
Join("inner", "user", "user.id = api_key.service_account_id").
Where("user.org_id=? AND user.id=?", orgID, serviceAccountID).
Join("inner", quotedUser, quotedUser+".id = api_key.service_account_id").
Where(quotedUser+".org_id=? AND "+quotedUser+".id=?", orgID, serviceAccountID).
Asc("name")
return sess.Find(&result)
@ -146,35 +164,6 @@ func (s *ServiceAccountsStoreImpl) ListTokens(ctx context.Context, orgID int64,
return result, err
}
func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*serviceaccounts.ServiceAccountDTO, error) {
query := models.GetOrgUsersQuery{OrgId: orgID, IsServiceAccount: true}
if serviceAccountID > 0 {
query.UserID = serviceAccountID
}
if err := s.sqlStore.GetOrgUsers(ctx, &query); err != nil {
return nil, err
}
saDTOs := make([]*serviceaccounts.ServiceAccountDTO, len(query.Result))
for i, user := range query.Result {
saDTOs[i] = &serviceaccounts.ServiceAccountDTO{
Id: user.UserId,
OrgId: user.OrgId,
Name: user.Name,
Login: user.Login,
Role: user.Role,
}
tokens, err := s.ListTokens(ctx, user.OrgId, user.UserId)
if err != nil {
return nil, err
}
saDTOs[i].Tokens = int64(len(tokens))
}
return saDTOs, nil
}
// RetrieveServiceAccountByID returns a service account by its ID
func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) {
serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{}
@ -298,8 +287,16 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context,
return updatedUser, err
}
func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, query *models.SearchOrgUsersQuery) ([]*serviceaccounts.ServiceAccountDTO, error) {
serviceAccounts := make([]*serviceaccounts.ServiceAccountDTO, 0)
func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(
ctx context.Context, orgID int64, query string, page int, limit int,
signedInUser *models.SignedInUser,
) (*serviceaccounts.SearchServiceAccountsResult, error) {
searchResult := &serviceaccounts.SearchServiceAccountsResult{
TotalCount: 0,
ServiceAccounts: make([]*serviceaccounts.ServiceAccountDTO, 0),
Page: page,
PerPage: limit,
}
err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
sess := dbSession.Table("org_user")
@ -309,7 +306,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
whereParams := make([]interface{}, 0)
whereConditions = append(whereConditions, "org_user.org_id = ?")
whereParams = append(whereParams, query.OrgID)
whereParams = append(whereParams, orgID)
whereConditions = append(whereConditions,
fmt.Sprintf("%s.is_service_account = %s",
@ -317,7 +314,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
s.sqlStore.Dialect.BooleanStr(true)))
if s.sqlStore.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "serviceaccounts", serviceaccounts.ActionRead)
acFilter, err := accesscontrol.Filter(signedInUser, "org_user.user_id", "serviceaccounts", serviceaccounts.ActionRead)
if err != nil {
return err
}
@ -325,8 +322,8 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
whereParams = append(whereParams, acFilter.Args...)
}
if query.Query != "" {
queryWithWildcards := "%" + query.Query + "%"
if query != "" {
queryWithWildcards := "%" + query + "%"
whereConditions = append(whereConditions, "(email "+s.sqlStore.Dialect.LikeStr()+" ? OR name "+s.sqlStore.Dialect.LikeStr()+" ? OR login "+s.sqlStore.Dialect.LikeStr()+" ?)")
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
}
@ -334,9 +331,9 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
if len(whereConditions) > 0 {
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
}
if query.Limit > 0 {
offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset)
if limit > 0 {
offset := limit * (page - 1)
sess.Limit(limit, offset)
}
sess.Cols(
@ -350,7 +347,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
"user.is_disabled",
)
sess.Asc("user.email", "user.login")
if err := sess.Find(&serviceAccounts); err != nil {
if err := sess.Find(&searchResult.ServiceAccounts); err != nil {
return err
}
@ -366,7 +363,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
if err != nil {
return err
}
query.Result.TotalCount = count
searchResult.TotalCount = count
return nil
})
@ -374,7 +371,7 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context,
return nil, err
}
return serviceAccounts, nil
return searchResult, nil
}
func contains(s []int64, e int64) bool {

View File

@ -13,7 +13,7 @@ import (
)
func TestStore_CreateServiceAccount(t *testing.T) {
sqlStore, store := setupTestDatabase(t)
_, store := setupTestDatabase(t)
t.Run("create service account", func(t *testing.T) {
saDTO, err := store.CreateServiceAccount(context.Background(), 1, "new Service Account")
require.NoError(t, err)
@ -21,17 +21,11 @@ func TestStore_CreateServiceAccount(t *testing.T) {
assert.Equal(t, "new Service Account", saDTO.Name)
assert.Equal(t, 0, int(saDTO.Tokens))
query := models.GetUserByIdQuery{Id: saDTO.Id}
err = sqlStore.GetUserById(context.Background(), &query)
retrieved := query.Result
retrieved, err := store.RetrieveServiceAccount(context.Background(), 1, saDTO.Id)
require.NoError(t, err)
assert.Equal(t, "sa-new-service-account", retrieved.Login)
assert.Equal(t, "new Service Account", retrieved.Name)
assert.Equal(t, "sa-new-service-account", retrieved.Email)
assert.Equal(t, "", retrieved.Password)
assert.Equal(t, 1, int(retrieved.OrgId))
assert.Len(t, retrieved.Salt, 10)
assert.Equal(t, true, retrieved.IsServiceAccount)
})
}

View File

@ -61,3 +61,27 @@ func (s *ServiceAccountsStoreImpl) DeleteServiceAccountToken(ctx context.Context
return nil
})
}
// assignApiKeyToServiceAccount sets the API key service account ID
func (s *ServiceAccountsStoreImpl) assignApiKeyToServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error {
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
key := models.ApiKey{Id: apikeyId}
exists, err := sess.Get(&key)
if err != nil {
s.log.Warn("API key not loaded", "err", err)
return err
}
if !exists {
s.log.Warn("API key not found", "err", err)
return models.ErrApiKeyNotFound
}
key.ServiceAccountId = &saccountId
if _, err := sess.ID(key.Id).Update(&key); err != nil {
s.log.Warn("Could not update api key", "err", err)
return err
}
return nil
})
}

View File

@ -40,6 +40,12 @@ type ServiceAccountDTO struct {
AvatarUrl string `json:"avatarUrl"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
}
type SearchServiceAccountsResult struct {
TotalCount int64 `json:"totalCount"`
ServiceAccounts []*ServiceAccountDTO `json:"serviceAccounts"`
Page int `json:"page"`
PerPage int `json:"perPage"`
}
type ServiceAccountProfileDTO struct {
Id int64 `json:"id" xorm:"user_id"`

View File

@ -14,9 +14,10 @@ type Service interface {
type Store interface {
CreateServiceAccount(ctx context.Context, orgID int64, name string) (*ServiceAccountDTO, error)
ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*ServiceAccountDTO, error)
SearchOrgServiceAccounts(ctx context.Context, query *models.SearchOrgUsersQuery) ([]*ServiceAccountDTO, error)
UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error)
SearchOrgServiceAccounts(ctx context.Context, orgID int64, query string, page int, limit int,
signedInUser *models.SignedInUser) (*SearchServiceAccountsResult, error)
UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64,
saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error)
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error)
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
UpgradeServiceAccounts(ctx context.Context) error

View File

@ -68,7 +68,6 @@ var _ serviceaccounts.Store = new(ServiceAccountsStoreMock)
type Calls struct {
CreateServiceAccount []interface{}
ListServiceAccounts []interface{}
RetrieveServiceAccount []interface{}
DeleteServiceAccount []interface{}
UpgradeServiceAccounts []interface{}
@ -110,10 +109,6 @@ func (s *ServiceAccountsStoreMock) ListTokens(ctx context.Context, orgID int64,
s.Calls.ListTokens = append(s.Calls.ListTokens, []interface{}{ctx, orgID, serviceAccount})
return nil, nil
}
func (s *ServiceAccountsStoreMock) ListServiceAccounts(ctx context.Context, orgID int64, serviceAccountID int64) ([]*serviceaccounts.ServiceAccountDTO, error) {
s.Calls.ListServiceAccounts = append(s.Calls.ListServiceAccounts, []interface{}{ctx, orgID})
return nil, nil
}
func (s *ServiceAccountsStoreMock) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) {
s.Calls.RetrieveServiceAccount = append(s.Calls.RetrieveServiceAccount, []interface{}{ctx, orgID, serviceAccountID})
@ -128,8 +123,9 @@ func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context,
return nil, nil
}
func (s *ServiceAccountsStoreMock) SearchOrgServiceAccounts(ctx context.Context, query *models.SearchOrgUsersQuery) ([]*serviceaccounts.ServiceAccountDTO, error) {
s.Calls.SearchOrgServiceAccounts = append(s.Calls.SearchOrgServiceAccounts, []interface{}{ctx, query})
func (s *ServiceAccountsStoreMock) SearchOrgServiceAccounts(ctx context.Context, orgID int64, query string, page int, limit int,
user *models.SignedInUser) (*serviceaccounts.SearchServiceAccountsResult, error) {
s.Calls.SearchOrgServiceAccounts = append(s.Calls.SearchOrgServiceAccounts, []interface{}{ctx, orgID, query, page, limit, user})
return nil, nil
}

View File

@ -41,13 +41,12 @@ func (ss *SQLStore) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuer
})
}
// GetAPIKeys queries the database based
// on input on GetApiKeysQuery
func (ss *SQLStore) GetNonServiceAccountAPIKeys(ctx context.Context) []*models.ApiKey {
// GetAllOrgsAPIKeys queries the database for valid non SA APIKeys across all orgs
func (ss *SQLStore) GetAllOrgsAPIKeys(ctx context.Context) []*models.ApiKey {
result := make([]*models.ApiKey, 0)
err := ss.WithDbSession(ctx, func(dbSession *DBSession) error {
sess := dbSession. //CHECK how many API keys do our clients have? Can we load them all?
Where("(expires IS NULL OR expires >= ?) AND service_account_id < 1 ", timeNow().Unix()).Asc("name")
Where("(expires IS NULL OR expires >= ?) AND service_account_id IS NULL", timeNow().Unix()).Asc("name")
return sess.Find(&result)
})
if err != nil {
@ -114,30 +113,6 @@ func (ss *SQLStore) AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand)
})
}
// UpdateApikeyServiceAccount sets a service account for an existing API key
func (ss *SQLStore) UpdateApikeyServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
key := models.ApiKey{Id: apikeyId}
exists, err := sess.Get(&key)
if err != nil {
ss.log.Warn("API key not loaded", "err", err)
return err
}
if !exists {
ss.log.Warn("API key not found", "err", err)
return models.ErrApiKeyNotFound
}
key.ServiceAccountId = &saccountId
if _, err := sess.ID(key.Id).Update(&key); err != nil {
ss.log.Warn("Could not update api key", "err", err)
return err
}
return nil
})
}
func (ss *SQLStore) GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
var apikey models.ApiKey

View File

@ -136,10 +136,6 @@ func (m *SQLStoreMock) DeleteOldLoginAttempts(ctx context.Context, cmd *models.D
return m.ExpectedError
}
func (m *SQLStoreMock) CreateServiceAccountForApikey(ctx context.Context, orgId int64, keyname string, role models.RoleType) (*models.User, error) {
return nil, m.ExpectedError
}
func (m *SQLStoreMock) CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error) {
return nil, m.ExpectedError
}
@ -595,7 +591,7 @@ func (m *SQLStoreMock) GetAPIKeys(ctx context.Context, query *models.GetApiKeysQ
return m.ExpectedError
}
func (m *SQLStoreMock) GetNonServiceAccountAPIKeys(ctx context.Context) []*models.ApiKey {
func (m *SQLStoreMock) GetAllOrgsAPIKeys(ctx context.Context) []*models.ApiKey {
return nil
}
@ -607,10 +603,6 @@ func (m *SQLStoreMock) AddAPIKey(ctx context.Context, cmd *models.AddApiKeyComma
return m.ExpectedError
}
func (m *SQLStoreMock) UpdateApikeyServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error {
return m.ExpectedError
}
func (m *SQLStoreMock) GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error {
return m.ExpectedError
}

View File

@ -24,7 +24,7 @@ func (ss *SQLStore) AddOrgUser(ctx context.Context, cmd *models.AddOrgUserComman
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
// check if user exists
var user models.User
if exists, err := sess.ID(cmd.UserId).Get(&user); err != nil {
if exists, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Get(&user); err != nil {
return err
} else if !exists {
return models.ErrUserNotFound
@ -114,9 +114,8 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
whereParams = append(whereParams, query.UserID)
}
// TODO: add to chore, for cleaning up after we have created
// service accounts table in the modelling
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = ?", dialect.Quote("user")))
whereParams = append(whereParams, dialect.BooleanStr(false))
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) && query.User != nil {
acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users", accesscontrol.ActionOrgUsersRead)
@ -179,9 +178,7 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU
whereConditions = append(whereConditions, "org_user.org_id = ?")
whereParams = append(whereParams, query.OrgID)
// TODO: add to chore, for cleaning up after we have created
// service accounts table in the modelling
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount))
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %s", x.Dialect().Quote("user"), ss.Dialect.BooleanStr(false)))
if ss.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
acFilter, err := accesscontrol.Filter(query.User, "org_user.user_id", "users", accesscontrol.ActionOrgUsersRead)
@ -248,7 +245,7 @@ func (ss *SQLStore) RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgUser
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
// check if user exists
var user models.User
if exists, err := sess.ID(cmd.UserId).Get(&user); err != nil {
if exists, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Get(&user); err != nil {
return err
} else if !exists {
return models.ErrUserNotFound
@ -301,7 +298,7 @@ func (ss *SQLStore) RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgUser
}
} else if cmd.ShouldDeleteOrphanedUser {
// no other orgs, delete the full user
if err := deleteUserInTransaction(sess, &models.DeleteUserCommand{UserId: user.Id}); err != nil {
if err := deleteUserInTransaction(ss, sess, &models.DeleteUserCommand{UserId: user.Id}); err != nil {
return err
}
@ -312,8 +309,8 @@ func (ss *SQLStore) RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgUser
})
}
// validate that there is an org admin user left
func validateOneAdminLeftInOrg(orgId int64, sess *DBSession) error {
// validate that there is an admin user left
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", orgId)
if err != nil {
return err

View File

@ -28,7 +28,6 @@ type Store interface {
GetOrgByNameHandler(ctx context.Context, query *models.GetOrgByNameQuery) error
CreateLoginAttempt(ctx context.Context, cmd *models.CreateLoginAttemptCommand) error
DeleteOldLoginAttempts(ctx context.Context, cmd *models.DeleteOldLoginAttemptsCommand) error
CreateServiceAccountForApikey(ctx context.Context, orgId int64, keyname string, role models.RoleType) (*models.User, error)
CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error)
GetUserById(ctx context.Context, query *models.GetUserByIdQuery) error
GetUserByLogin(ctx context.Context, query *models.GetUserByLoginQuery) error
@ -134,10 +133,9 @@ type Store interface {
SetAlertNotificationStateToPendingCommand(ctx context.Context, cmd *models.SetAlertNotificationStateToPendingCommand) error
GetOrCreateAlertNotificationState(ctx context.Context, cmd *models.GetOrCreateNotificationStateQuery) error
GetAPIKeys(ctx context.Context, query *models.GetApiKeysQuery) error
GetNonServiceAccountAPIKeys(ctx context.Context) []*models.ApiKey
GetAllOrgsAPIKeys(ctx context.Context) []*models.ApiKey
DeleteApiKey(ctx context.Context, cmd *models.DeleteApiKeyCommand) error
AddAPIKey(ctx context.Context, cmd *models.AddApiKeyCommand) error
UpdateApikeyServiceAccount(ctx context.Context, apikeyId int64, saccountId int64) error
GetApiKeyById(ctx context.Context, query *models.GetApiKeyByIdQuery) error
GetApiKeyByName(ctx context.Context, query *models.GetApiKeyByNameQuery) error
UpdateTempUserStatus(ctx context.Context, cmd *models.UpdateTempUserStatusCommand) error

View File

@ -184,24 +184,6 @@ func (ss *SQLStore) createUser(ctx context.Context, sess *DBSession, args userCr
return user, nil
}
func (ss *SQLStore) CreateServiceAccountForApikey(ctx context.Context, orgId int64, keyname string, role models.RoleType) (*models.User, error) {
prefix := "Service-Account-Autogen-"
cmd := models.CreateUserCommand{
Login: fmt.Sprintf("%v-%v-%v", prefix, orgId, keyname),
Name: prefix + keyname,
OrgId: orgId,
DefaultOrgRole: string(role),
IsServiceAccount: true,
}
newuser, err := ss.CreateUser(ctx, cmd)
if err != nil {
return nil, fmt.Errorf("failed to create user: %w", err)
}
return newuser, err
}
func (ss *SQLStore) CreateUser(ctx context.Context, cmd models.CreateUserCommand) (*models.User, error) {
var user *models.User
err := ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
@ -300,10 +282,19 @@ func (ss *SQLStore) CreateUser(ctx context.Context, cmd models.CreateUserCommand
return user, err
}
func (ss SQLStore) GetUserById(ctx context.Context, query *models.GetUserByIdQuery) error {
func notServiceAccountFilter(ss *SQLStore) string {
return fmt.Sprintf("%s.is_service_account = %s",
ss.Dialect.Quote("user"),
ss.Dialect.BooleanStr(false))
}
func (ss *SQLStore) GetUserById(ctx context.Context, query *models.GetUserByIdQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
user := new(models.User)
has, err := sess.ID(query.Id).Get(user)
has, err := sess.ID(query.Id).
Where(notServiceAccountFilter(ss)).
Get(user)
if err != nil {
return err
@ -326,7 +317,7 @@ func (ss *SQLStore) GetUserByLogin(ctx context.Context, query *models.GetUserByL
// Try and find the user by login first.
// It's not sufficient to assume that a LoginOrEmail with an "@" is an email.
user := &models.User{Login: query.LoginOrEmail}
has, err := sess.Get(user)
has, err := sess.Where(notServiceAccountFilter(ss)).Get(user)
if err != nil {
return err
@ -358,7 +349,7 @@ func (ss *SQLStore) GetUserByEmail(ctx context.Context, query *models.GetUserByE
}
user := &models.User{Email: query.Email}
has, err := sess.Get(user)
has, err := sess.Where(notServiceAccountFilter(ss)).Get(user)
if err != nil {
return err
@ -382,7 +373,7 @@ func (ss *SQLStore) UpdateUser(ctx context.Context, cmd *models.UpdateUserComman
Updated: time.Now(),
}
if _, err := sess.ID(cmd.UserId).Update(&user); err != nil {
if _, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Update(&user); err != nil {
return err
}
@ -405,7 +396,7 @@ func (ss *SQLStore) ChangeUserPassword(ctx context.Context, cmd *models.ChangeUs
Updated: time.Now(),
}
_, err := sess.ID(cmd.UserId).Update(&user)
_, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Update(&user)
return err
})
}
@ -456,7 +447,7 @@ func setUsingOrgInTransaction(sess *DBSession, userID int64, orgID int64) error
func (ss *SQLStore) GetUserProfile(ctx context.Context, query *models.GetUserProfileQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
var user models.User
has, err := sess.ID(query.UserId).Get(&user)
has, err := sess.ID(query.UserId).Where(notServiceAccountFilter(ss)).Get(&user)
if err != nil {
return err
@ -506,7 +497,9 @@ func (ss *SQLStore) GetUserOrgList(ctx context.Context, query *models.GetUserOrg
query.Result = make([]*models.UserOrgDTO, 0)
sess := x.Table("org_user")
sess.Join("INNER", "org", "org_user.org_id=org.id")
sess.Join("INNER", x.Dialect().Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", x.Dialect().Quote("user")))
sess.Where("org_user.user_id=?", query.UserId)
sess.Where(notServiceAccountFilter(ss))
sess.Cols("org.name", "org_user.role", "org_user.org_id")
sess.OrderBy("org.name")
err := sess.Find(&query.Result)
@ -612,8 +605,6 @@ func SearchUsers(ctx context.Context, query *models.SearchUsersQuery) error {
whereParams := make([]interface{}, 0)
sess := x.Table("user").Alias("u")
// TODO: add to chore, for cleaning up after we have created
// service accounts table in the modelling
whereConditions = append(whereConditions, "u.is_service_account = ?")
whereParams = append(whereParams, dialect.BooleanStr(false))
@ -710,7 +701,7 @@ func (ss *SQLStore) DisableUser(ctx context.Context, cmd *models.DisableUserComm
user := models.User{}
sess := x.Table("user")
if has, err := sess.ID(cmd.UserId).Get(&user); err != nil {
if has, err := sess.ID(cmd.UserId).Where(notServiceAccountFilter(ss)).Get(&user); err != nil {
return err
} else if !has {
return models.ErrUserNotFound
@ -739,32 +730,28 @@ func (ss *SQLStore) BatchDisableUsers(ctx context.Context, cmd *models.BatchDisa
disableParams = append(disableParams, v)
}
_, err := sess.Exec(disableParams...)
if err != nil {
return err
}
return nil
_, err := sess.Where(notServiceAccountFilter(ss)).Exec(disableParams...)
return err
})
}
func (ss *SQLStore) DeleteUser(ctx context.Context, cmd *models.DeleteUserCommand) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
return deleteUserInTransaction(sess, cmd)
return deleteUserInTransaction(ss, sess, cmd)
})
}
func deleteUserInTransaction(sess *DBSession, cmd *models.DeleteUserCommand) error {
func deleteUserInTransaction(ss *SQLStore, sess *DBSession, cmd *models.DeleteUserCommand) error {
// Check if user exists
user := models.User{Id: cmd.UserId}
has, err := sess.Get(&user)
has, err := sess.Where(notServiceAccountFilter(ss)).Get(&user)
if err != nil {
return err
}
if !has {
return models.ErrUserNotFound
}
for _, sql := range userDeletions() {
for _, sql := range UserDeletions() {
_, err := sess.Exec(sql, cmd.UserId)
if err != nil {
return err
@ -773,7 +760,7 @@ func deleteUserInTransaction(sess *DBSession, cmd *models.DeleteUserCommand) err
return nil
}
func userDeletions() []string {
func UserDeletions() []string {
deletes := []string{
"DELETE FROM star WHERE user_id = ?",
"DELETE FROM " + dialect.Quote("user") + " WHERE id = ?",
@ -788,18 +775,11 @@ func userDeletions() []string {
return deletes
}
func ServiceAccountDeletions() []string {
deletes := []string{
"DELETE FROM api_key WHERE service_account_id = ?",
}
deletes = append(deletes, userDeletions()...)
return deletes
}
// UpdateUserPermissions sets the user Server Admin flag
func (ss *SQLStore) UpdateUserPermissions(userID int64, isAdmin bool) error {
return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
var user models.User
if _, err := sess.ID(userID).Get(&user); err != nil {
if _, err := sess.ID(userID).Where(notServiceAccountFilter(ss)).Get(&user); err != nil {
return err
}
@ -833,8 +813,8 @@ func (ss *SQLStore) SetUserHelpFlag(ctx context.Context, cmd *models.SetUserHelp
})
}
// validateOneAdminLeft validate that there is an admin user left
func validateOneAdminLeft(sess *DBSession) error {
// validate that there is an admin user left
count, err := sess.Where("is_admin=?", true).Count(&models.User{})
if err != nil {
return err