diff --git a/pkg/models/user.go b/pkg/models/user.go index 0a19bce31a6..062a1d9e7d5 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -34,8 +34,9 @@ type User struct { HelpFlags1 HelpFlags1 IsDisabled bool - IsAdmin bool - OrgId int64 + IsAdmin bool + IsServiceAccount bool + OrgId int64 Created time.Time Updated time.Time diff --git a/pkg/server/backgroundsvcs/background_services.go b/pkg/server/backgroundsvcs/background_services.go index 098a9ff777b..1f83afd1211 100644 --- a/pkg/server/backgroundsvcs/background_services.go +++ b/pkg/server/backgroundsvcs/background_services.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsettings" "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/serviceaccounts" "github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" "github.com/grafana/grafana/pkg/tsdb/cloudmonitoring" @@ -50,7 +51,7 @@ func ProvideBackgroundServiceRegistry( _ *influxdb.Service, _ *loki.Service, _ *opentsdb.Service, _ *prometheus.Service, _ *tempo.Service, _ *testdatasource.Service, _ *plugindashboards.Service, _ *dashboardsnapshots.Service, _ *postgres.Service, _ *mysql.Service, _ *mssql.Service, _ *grafanads.Service, _ *cloudmonitoring.Service, - _ *pluginsettings.Service, _ *alerting.AlertNotificationService, + _ *pluginsettings.Service, _ *alerting.AlertNotificationService, _ serviceaccounts.Service, ) *BackgroundServiceRegistry { return NewBackgroundServiceRegistry( httpServer, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 0d1387e6737..74c46b2cfee 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -54,6 +54,8 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + serviceaccountsmanager "github.com/grafana/grafana/pkg/services/serviceaccounts/manager" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/updatechecker" @@ -166,6 +168,8 @@ var wireBasicSet = wire.NewSet( datasources.ProvideService, pluginsettings.ProvideService, alerting.ProvideService, + serviceaccountsmanager.ProvideServiceAccountsService, + wire.Bind(new(serviceaccounts.Service), new(*serviceaccountsmanager.ServiceAccountsService)), expr.ProvideService, ) diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go new file mode 100644 index 00000000000..e5cbe825543 --- /dev/null +++ b/pkg/services/serviceaccounts/api/api.go @@ -0,0 +1,53 @@ +package api + +import ( + "net/http" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/setting" +) + +type ServiceAccountsAPI struct { + service serviceaccounts.Service + accesscontrol accesscontrol.AccessControl + RouterRegister routing.RouteRegister +} + +func NewServiceAccountsAPI( + service serviceaccounts.Service, + accesscontrol accesscontrol.AccessControl, + routerRegister routing.RouteRegister, +) *ServiceAccountsAPI { + return &ServiceAccountsAPI{ + service: service, + accesscontrol: accesscontrol, + RouterRegister: routerRegister, + } +} + +func (api *ServiceAccountsAPI) RegisterAPIEndpoints( + cfg *setting.Cfg, +) { + if !cfg.FeatureToggles["service-accounts"] { + return + } + auth := acmiddleware.Middleware(api.accesscontrol) + api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) { + serviceAccountsRoute.Delete("/:serviceAccountId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount)) + }) +} + +func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response { + scopeID := ctx.ParamsInt64(":serviceAccountId") + err := api.service.DeleteServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID) + if err != nil { + return response.Error(http.StatusInternalServerError, "Service account deletion error", err) + } + return response.Success("service account deleted") +} diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go new file mode 100644 index 00000000000..9d9b04c081f --- /dev/null +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -0,0 +1,120 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" + "gopkg.in/macaron.v1" +) + +var ( + serviceaccountIDPath = "/api/serviceaccounts/%s" +) + +// test the accesscontrol endpoints +// with permissions and without permissions +func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) { + store := sqlstore.InitTestDB(t) + svcmock := tests.ServiceAccountMock{} + + var requestResponse = func(server *macaron.Macaron, httpMethod, requestpath string) *httptest.ResponseRecorder { + req, err := http.NewRequest(httpMethod, requestpath, nil) + require.NoError(t, err) + recorder := httptest.NewRecorder() + server.ServeHTTP(recorder, req) + return recorder + } + t.Run("should be able to delete serviceaccount for with permissions", func(t *testing.T) { + testcase := struct { + user tests.TestUser + acmock *accesscontrolmock.Mock + expectedCode int + }{ + + user: tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true}, + acmock: tests.SetupMockAccesscontrol( + t, + func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) { + return []*accesscontrol.Permission{{Action: serviceaccounts.ActionDelete, Scope: serviceaccounts.ScopeAll}}, nil + }, + false, + ), + expectedCode: http.StatusOK, + } + serviceAccountDeletionScenario(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) + actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, fmt.Sprint(createduser.Id))).Code + require.Equal(t, testcase.expectedCode, actual) + }) + }) + + t.Run("should be forbidden to delete serviceaccount via accesscontrol on endpoint", func(t *testing.T) { + testcase := struct { + user tests.TestUser + acmock *accesscontrolmock.Mock + expectedCode int + }{ + user: tests.TestUser{Login: "servicetest2@admin", IsServiceAccount: true}, + acmock: tests.SetupMockAccesscontrol( + t, + func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) { + return []*accesscontrol.Permission{}, nil + }, + false, + ), + expectedCode: http.StatusForbidden, + } + serviceAccountDeletionScenario(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) + actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, fmt.Sprint(createduser.Id))).Code + require.Equal(t, testcase.expectedCode, actual) + }) + }) +} + +func serviceAccountDeletionScenario(t *testing.T, httpMethod string, endpoint string, user *tests.TestUser, fn func(httpmethod string, endpoint string, user *tests.TestUser)) { + t.Helper() + fn(httpMethod, endpoint, user) +} + +func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock) *macaron.Macaron { + a := NewServiceAccountsAPI( + svc, + acmock, + routerRegister, + ) + a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}}) + + m := macaron.New() + signedUser := &models.SignedInUser{ + OrgId: 1, + OrgRole: models.ROLE_ADMIN, + } + + m.Use(func(c *macaron.Context) { + ctx := &models.ReqContext{ + Context: c, + IsSignedIn: true, + SignedInUser: signedUser, + Logger: log.New("serviceaccounts-test"), + } + c.Map(ctx) + }) + a.RouterRegister.Register(m.Router) + return m +} diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go new file mode 100644 index 00000000000..e04dbd79980 --- /dev/null +++ b/pkg/services/serviceaccounts/database/database.go @@ -0,0 +1,43 @@ +package database + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type ServiceAccountsStoreImpl struct { + sqlStore *sqlstore.SQLStore +} + +func NewServiceAccountsStore(store *sqlstore.SQLStore) *ServiceAccountsStoreImpl { + return &ServiceAccountsStoreImpl{ + sqlStore: store, + } +} + +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 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) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go new file mode 100644 index 00000000000..a26dada9a13 --- /dev/null +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -0,0 +1,49 @@ +package database + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestStore_DeleteServiceAccount(t *testing.T) { + cases := []struct { + desc string + user tests.TestUser + expectedErr error + }{ + { + desc: "service accounts should exist and get deleted", + user: tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true}, + expectedErr: nil, + }, + { + desc: "service accounts is false should not delete the user", + user: tests.TestUser{Login: "test1@admin", IsServiceAccount: false}, + expectedErr: serviceaccounts.ErrServiceAccountNotFound, + }, + } + + for _, c := range cases { + t.Run(c.desc, func(t *testing.T) { + db, store := setupTestDatabase(t) + user := tests.SetupUserServiceAccount(t, db, c.user) + err := store.DeleteServiceAccount(context.Background(), user.OrgId, user.Id) + if c.expectedErr != nil { + require.ErrorIs(t, err, c.expectedErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreImpl) { + t.Helper() + db := sqlstore.InitTestDB(t) + return db, NewServiceAccountsStore(db) +} diff --git a/pkg/services/serviceaccounts/errors.go b/pkg/services/serviceaccounts/errors.go new file mode 100644 index 00000000000..8538928be31 --- /dev/null +++ b/pkg/services/serviceaccounts/errors.go @@ -0,0 +1,7 @@ +package serviceaccounts + +import "errors" + +var ( + ErrServiceAccountNotFound = errors.New("Service account not found") +) diff --git a/pkg/services/serviceaccounts/manager/roles.go b/pkg/services/serviceaccounts/manager/roles.go new file mode 100644 index 00000000000..2c1d9a8b6f4 --- /dev/null +++ b/pkg/services/serviceaccounts/manager/roles.go @@ -0,0 +1,23 @@ +package manager + +import ( + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/serviceaccounts" +) + +var ( + role = accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Version: 1, + Name: "fixed:serviceaccounts:writer", + Description: "", + Permissions: []accesscontrol.Permission{ + { + Action: serviceaccounts.ActionDelete, + Scope: serviceaccounts.ScopeAll, + }, + }, + }, + Grants: []string{"Admin"}, + } +) diff --git a/pkg/services/serviceaccounts/manager/service.go b/pkg/services/serviceaccounts/manager/service.go new file mode 100644 index 00000000000..a7e41de2cb8 --- /dev/null +++ b/pkg/services/serviceaccounts/manager/service.go @@ -0,0 +1,51 @@ +package manager + +import ( + "context" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/serviceaccounts/api" + "github.com/grafana/grafana/pkg/services/serviceaccounts/database" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +var ( + ServiceAccountFeatureToggleNotFound = "FeatureToggle service-accounts not found, try adding it to your custom.ini" +) + +type ServiceAccountsService struct { + store serviceaccounts.Store + cfg *setting.Cfg + log log.Logger +} + +func ProvideServiceAccountsService( + cfg *setting.Cfg, + store *sqlstore.SQLStore, + ac accesscontrol.AccessControl, + routeRegister routing.RouteRegister, +) (*ServiceAccountsService, error) { + s := &ServiceAccountsService{ + cfg: cfg, + store: database.NewServiceAccountsStore(store), + log: log.New("serviceaccounts"), + } + if err := ac.DeclareFixedRoles(role); err != nil { + return nil, err + } + serviceaccountsAPI := api.NewServiceAccountsAPI(s, ac, routeRegister) + serviceaccountsAPI.RegisterAPIEndpoints(cfg) + return s, nil +} + +func (s *ServiceAccountsService) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { + if !s.cfg.FeatureToggles["service-accounts"] { + s.log.Debug(ServiceAccountFeatureToggleNotFound) + return nil + } + return s.store.DeleteServiceAccount(ctx, orgID, serviceAccountID) +} diff --git a/pkg/services/serviceaccounts/manager/service_test.go b/pkg/services/serviceaccounts/manager/service_test.go new file mode 100644 index 00000000000..460e5d61e1d --- /dev/null +++ b/pkg/services/serviceaccounts/manager/service_test.go @@ -0,0 +1,38 @@ +package manager + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/serviceaccounts/tests" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProvideServiceAccount_DeleteServiceAccount(t *testing.T) { + t.Run("feature toggle present, should call store function", func(t *testing.T) { + cfg := setting.NewCfg() + storeMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}} + cfg.FeatureToggles = map[string]bool{"service-accounts": true} + svc := ServiceAccountsService{cfg: cfg, store: storeMock} + err := svc.DeleteServiceAccount(context.Background(), 1, 1) + require.NoError(t, err) + assert.Len(t, storeMock.Calls.DeleteServiceAccount, 1) + }) + + t.Run("no feature toggle present, should not call store function", func(t *testing.T) { + cfg := setting.NewCfg() + svcMock := &tests.ServiceAccountsStoreMock{Calls: tests.Calls{}} + cfg.FeatureToggles = map[string]bool{"service-accounts": false} + svc := ServiceAccountsService{ + cfg: cfg, + store: svcMock, + log: log.New("serviceaccounts-manager-test"), + } + err := svc.DeleteServiceAccount(context.Background(), 1, 1) + require.NoError(t, err) + assert.Len(t, svcMock.Calls.DeleteServiceAccount, 0) + }) +} diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go new file mode 100644 index 00000000000..6a77cd71210 --- /dev/null +++ b/pkg/services/serviceaccounts/models.go @@ -0,0 +1,12 @@ +package serviceaccounts + +import "github.com/grafana/grafana/pkg/services/accesscontrol" + +var ( + ScopeAll = "serviceaccounts:*" + ScopeID = accesscontrol.Scope("serviceaccounts", "id", accesscontrol.Parameter(":serviceaccountId")) +) + +const ( + ActionDelete = "serviceaccounts:delete" +) diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go new file mode 100644 index 00000000000..e173b0d3600 --- /dev/null +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -0,0 +1,10 @@ +package serviceaccounts + +import "context" + +type Service interface { + DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error +} +type Store interface { + DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error +} diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go new file mode 100644 index 00000000000..88dfdaeca4d --- /dev/null +++ b/pkg/services/serviceaccounts/tests/common.go @@ -0,0 +1,62 @@ +package tests + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/serviceaccounts" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +type TestUser struct { + Login string + IsServiceAccount bool +} + +func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser TestUser) *models.User { + u1, err := sqlStore.CreateUser(context.Background(), models.CreateUserCommand{ + Login: testUser.Login, + IsServiceAccount: testUser.IsServiceAccount, + }) + require.NoError(t, err) + return u1 +} + +// create mock for serviceaccountservice +type ServiceAccountMock struct{} + +func (s *ServiceAccountMock) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { + return nil +} + +func SetupMockAccesscontrol(t *testing.T, userpermissionsfunc func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error), disableAccessControl bool) *accesscontrolmock.Mock { + t.Helper() + acmock := accesscontrolmock.New() + if disableAccessControl { + acmock = acmock.WithDisabled() + } + acmock.GetUserPermissionsFunc = userpermissionsfunc + return acmock +} + +// this is a way to see +// that the Mock implements the store interface +var _ serviceaccounts.Store = new(ServiceAccountsStoreMock) + +type Calls struct { + DeleteServiceAccount []interface{} +} + +type ServiceAccountsStoreMock struct { + Calls Calls +} + +func (s *ServiceAccountsStoreMock) DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error { + // now we can test that the mock has these calls when we call the function + s.Calls.DeleteServiceAccount = append(s.Calls.DeleteServiceAccount, []interface{}{ctx, orgID, serviceAccountID}) + return nil +} diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index b7c78fa2a65..0dfd3063f48 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -126,6 +126,10 @@ func addUserMigrations(mg *Migrator) { mg.AddMigration("Add index user.login/user.email", NewAddIndexMigration(userV2, &Index{ Cols: []string{"login", "email"}, })) + + mg.AddMigration("Add is_service_account column to user", NewAddColumnMigration(userV2, &Column{ + Name: "is_service_account", Type: DB_Bool, Nullable: false, Default: "0", + })) } type AddMissingUserSaltAndRandsMigration struct { diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 46ea8f36d20..1015811ce85 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -225,17 +225,18 @@ func (ss *SQLStore) CreateUser(ctx context.Context, cmd models.CreateUserCommand // create user user = &models.User{ - Email: cmd.Email, - Name: cmd.Name, - Login: cmd.Login, - Company: cmd.Company, - IsAdmin: cmd.IsAdmin, - IsDisabled: cmd.IsDisabled, - OrgId: orgId, - EmailVerified: cmd.EmailVerified, - Created: time.Now(), - Updated: time.Now(), - LastSeenAt: time.Now().AddDate(-10, 0, 0), + Email: cmd.Email, + Name: cmd.Name, + Login: cmd.Login, + Company: cmd.Company, + IsAdmin: cmd.IsAdmin, + IsDisabled: cmd.IsDisabled, + OrgId: orgId, + EmailVerified: cmd.EmailVerified, + Created: time.Now(), + Updated: time.Now(), + LastSeenAt: time.Now().AddDate(-10, 0, 0), + IsServiceAccount: cmd.IsServiceAccount, } salt, err := util.GetRandomString(10) @@ -754,7 +755,16 @@ func deleteUserInTransaction(sess *DBSession, cmd *models.DeleteUserCommand) err if !has { return models.ErrUserNotFound } + for _, sql := range userDeletions() { + _, err := sess.Exec(sql, cmd.UserId) + if err != nil { + return err + } + } + return nil +} +func userDeletions() []string { deletes := []string{ "DELETE FROM star WHERE user_id = ?", "DELETE FROM " + dialect.Quote("user") + " WHERE id = ?", @@ -766,15 +776,15 @@ func deleteUserInTransaction(sess *DBSession, cmd *models.DeleteUserCommand) err "DELETE FROM user_auth_token WHERE user_id = ?", "DELETE FROM quota WHERE user_id = ?", } + return deletes +} - for _, sql := range deletes { - _, err := sess.Exec(sql, cmd.UserId) - if err != nil { - return err - } +func ServiceAccountDeletions() []string { + deletes := []string{ + "DELETE FROM api_key WHERE service_account_id = ?", } - - return nil + deletes = append(deletes, userDeletions()...) + return deletes } func (ss *SQLStore) UpdateUserPermissions(userID int64, isAdmin bool) error {