ServiceAccounts: Delete ServiceAccount (#40470)

* Add extra fields to OSS types to support enterprise

* WIP service accounts

* Update public/app/features/api-keys/ApiKeysForm.tsx

Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>

* Create a service account at the same time as the API key

* Use service account credentials when accessing API with APIkey

* Throw better error

* Use Boolean for "create service account button"

* Add GetRole to service, merge RoleDTO and Role structs

This patch merges the identical OSS and Enterprise data structures, which improves the code for two reasons:

1.  Makes switching between OSS and Enterprise easier
2.  Reduces the chance of incompatibilities developing between the same functions in OSS and Enterprise

* Start work cloning permissions onto service account

* If API key is not linked to a service account, continue login as usual

* Fallback to old auth if no service account linked to key

* Commented

* Add CloneUserToServiceAccount

* Update mock.go

* Put graphical bits behind a feature toggle

* Start adding LinkAPIKeyToServiceAccount

* Update pkg/models/user.go

Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com>

* Update pkg/api/apikey.go

Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com>

* Update pkg/api/apikey.go

Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com>

* Finish LinkAPIKeyToServiceAccount

* Update comment

* Handle api key link error

* Update pkg/services/sqlstore/apikey.go

Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>

* Feature toggle

* Update pkg/services/accesscontrol/accesscontrol.go

Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>

* Not needed (yet)

* Better error messages for OSS accesscontrol

* Set an invalid user id as default

* ServiceAccountId should be string

* Re-arrange field names

* ServiceAccountId is integer

* Update ossaccesscontrol.go

* Linter

* Remove fronend edits

* Remove console log

* Update ApiKeysForm.tsx

* feat: add serviceaccount deletion

* feat: make sure we do not accidently delete serviceaccount

* feat: ServiceAccount Type

* refactor: userDeletions function

* refactor: serviceaccount deletions\

* refactor: error name and removed attribute for userDeletecommand

* refactor:: remove serviceaccount type for now

* WIP

* add mocked function

* Remove unnecessary db query, move to right place

* Update pkg/services/accesscontrol/mock/mock.go

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

* Update pkg/services/accesscontrol/mock/mock.go

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

* Update pkg/services/accesscontrol/mock/mock.go

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

* Better error messages

* Better and correcter error messages

* add mocked function

* refactor: move function call, add error msg

* add IsServiceAccount and fix table

* add service accounts package

* WIP

* WIP

* working serviceaccountsapi registration

* WIP tests

* test

* test working

* test running for service

* moved the error out of the models package

* fixed own review

* linting errors

* Update pkg/services/serviceaccounts/database/database.go

Co-authored-by: Jeremy Price <Jeremy.price@grafana.com>

* tests running for api

* WIP

* WIP

* removed unused secrets background svc

* removed background svc for serviceaccount infavor or wire.go

* serviceaccounts manager tests

* registering as backend service

Co-authored-by: Jeremy Price <jeremy.price@grafana.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Eric Leijonmarck 2021-11-11 15:10:24 +00:00 committed by GitHub
parent cd01384d3a
commit 4fd3dd41bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 509 additions and 21 deletions

View File

@ -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

View File

@ -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,

View File

@ -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,
)

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
package serviceaccounts
import "errors"
var (
ErrServiceAccountNotFound = errors.New("Service account not found")
)

View File

@ -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"},
}
)

View File

@ -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)
}

View File

@ -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)
})
}

View File

@ -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"
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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 {

View File

@ -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 {