Add delete user from other services/stores (#51912)

* Remove user from preferences, stars, orguser, team member

* Fix lint

* Add Delete user from org and dashboard acl

* Delete user from user auth

* Add DeleteUser to quota

* Add test files and adjust user auth store

* Rename package in wire for user auth

* Import Quota Service interface in other services

* do the same in tests

* fix lint tests

* Fix tests

* Add some tests

* Rename InsertUser and DeleteUser to InsertOrgUser and DeleteOrgUser

* Rename DeleteUser to DeleteByUser in quota

* changing a method name in few additional places

* Fix in other places

* Fix lint

* Fix tests

* Rename DeleteOrgUser to DeleteUserFromAll

* Update pkg/services/org/orgimpl/org_test.go

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

* Update pkg/services/preference/prefimpl/inmemory_test.go

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

* Rename Acl to ACL

* Fix wire after merge with main

* Move test to uni test

Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com>
This commit is contained in:
idafurjes
2022-07-15 18:06:44 +02:00
committed by GitHub
parent 5d052be6ff
commit 17ec9cac83
58 changed files with 666 additions and 243 deletions

View File

@ -38,7 +38,7 @@ import (
"github.com/grafana/grafana/pkg/services/login/loginservice" "github.com/grafana/grafana/pkg/services/login/loginservice"
"github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/preference/preftest" "github.com/grafana/grafana/pkg/services/preference/preftest"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/searchusers/filters"
@ -238,7 +238,7 @@ func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url strin
Live: newTestLive(t, store), Live: newTestLive(t, store),
License: &licensing.OSSLicensingService{}, License: &licensing.OSSLicensingService{},
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quotaimpl.Service{Cfg: cfg},
RouteRegister: routing.NewRouteRegister(), RouteRegister: routing.NewRouteRegister(),
AccessControl: accesscontrolmock.New().WithPermissions(permissions), AccessControl: accesscontrolmock.New().WithPermissions(permissions),
searchUsersService: searchusers.ProvideUsersService(store, filters.ProvideOSSSearchUserFilter()), searchUsersService: searchusers.ProvideUsersService(store, filters.ProvideOSSSearchUserFilter()),
@ -379,7 +379,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
Cfg: cfg, Cfg: cfg,
Features: features, Features: features,
Live: newTestLive(t, db), Live: newTestLive(t, db),
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quotaimpl.Service{Cfg: cfg},
RouteRegister: routeRegister, RouteRegister: routeRegister,
SQLStore: store, SQLStore: store,
License: &licensing.OSSLicensingService{}, License: &licensing.OSSLicensingService{},

View File

@ -34,7 +34,7 @@ import (
pref "github.com/grafana/grafana/pkg/services/preference" pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/preference/preftest" "github.com/grafana/grafana/pkg/services/preference/preftest"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
@ -1012,7 +1012,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
Cfg: cfg, Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, sqlstore.InitTestDB(t)), Live: newTestLive(t, sqlstore.InitTestDB(t)),
QuotaService: &quota.QuotaService{ QuotaService: &quotaimpl.Service{
Cfg: cfg, Cfg: cfg,
}, },
pluginStore: &fakePluginStore{}, pluginStore: &fakePluginStore{},
@ -1048,7 +1048,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
Cfg: cfg, Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, sqlstore.InitTestDB(t)), Live: newTestLive(t, sqlstore.InitTestDB(t)),
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quotaimpl.Service{Cfg: cfg},
LibraryPanelService: &mockLibraryPanelService{}, LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{}, LibraryElementService: &mockLibraryElementService{},
SQLStore: sqlmock, SQLStore: sqlmock,
@ -1085,7 +1085,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
Cfg: cfg, Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, sqlstore.InitTestDB(t)), Live: newTestLive(t, sqlstore.InitTestDB(t)),
QuotaService: &quota.QuotaService{Cfg: cfg}, QuotaService: &quotaimpl.Service{Cfg: cfg},
LibraryPanelService: &mockLibraryPanelService{}, LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{}, LibraryElementService: &mockLibraryElementService{},
DashboardService: mock, DashboardService: mock,

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest" "github.com/grafana/grafana/pkg/web/webtest"
@ -143,6 +144,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) { server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.folderService = folderService hs.folderService = folderService
hs.AccessControl = acmock.New() hs.AccessControl = acmock.New()
hs.QuotaService = quotatest.NewQuotaServiceFake()
}) })
t.Run("Should attach access control metadata to multiple folders", func(t *testing.T) { t.Run("Should attach access control metadata to multiple folders", func(t *testing.T) {

View File

@ -62,11 +62,11 @@ import (
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
pref "github.com/grafana/grafana/pkg/services/preference" pref "github.com/grafana/grafana/pkg/services/preference"
"github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/services/provisioning"
"github.com/grafana/grafana/pkg/services/quota"
publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api" publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
"github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers"
@ -80,6 +80,7 @@ import (
"github.com/grafana/grafana/pkg/services/teamguardian" "github.com/grafana/grafana/pkg/services/teamguardian"
"github.com/grafana/grafana/pkg/services/thumbs" "github.com/grafana/grafana/pkg/services/thumbs"
"github.com/grafana/grafana/pkg/services/updatechecker" "github.com/grafana/grafana/pkg/services/updatechecker"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
@ -103,7 +104,7 @@ type HTTPServer struct {
CacheService *localcache.CacheService CacheService *localcache.CacheService
DataSourceCache datasources.CacheService DataSourceCache datasources.CacheService
AuthTokenService models.UserTokenService AuthTokenService models.UserTokenService
QuotaService *quota.QuotaService QuotaService quota.Service
RemoteCacheService *remotecache.RemoteCache RemoteCacheService *remotecache.RemoteCache
ProvisioningService provisioning.ProvisioningService ProvisioningService provisioning.ProvisioningService
Login login.Service Login login.Service
@ -171,6 +172,7 @@ type HTTPServer struct {
CoremodelStaticRegistry *registry.Static CoremodelStaticRegistry *registry.Static
kvStore kvstore.KVStore kvStore kvstore.KVStore
secretsMigrator secrets.Migrator secretsMigrator secrets.Migrator
userService user.Service
} }
type ServerOptions struct { type ServerOptions struct {
@ -191,7 +193,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider, live *live.GrafanaLive, livePushGateway *pushhttp.Gateway, plugCtxProvider *plugincontext.Provider,
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager, contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service, alertNG *ngalert.AlertNG, libraryPanelService librarypanels.Service, libraryElementService libraryelements.Service,
quotaService *quota.QuotaService, socialService social.Service, tracer tracing.Tracer, exportService export.ExportService, quotaService quota.Service, socialService social.Service, tracer tracing.Tracer, exportService export.ExportService,
encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService, encryptionService encryption.Internal, grafanaUpdateChecker *updatechecker.GrafanaService,
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service, pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.Service,
dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service, dataSourcesService datasources.DataSourceService, secretsService secrets.Service, queryDataService *query.Service,
@ -205,8 +207,8 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static,
kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, publicDashboardsApi *publicdashboardsApi.Api, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck,
) (*HTTPServer, error) { publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) {
web.Env = cfg.Env web.Env = cfg.Env
m := web.New() m := web.New()
@ -292,6 +294,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
kvStore: kvStore, kvStore: kvStore,
PublicDashboardsApi: publicDashboardsApi, PublicDashboardsApi: publicDashboardsApi,
secretsMigrator: secretsMigrator, secretsMigrator: secretsMigrator,
userService: userService,
} }
if hs.Listener != nil { if hs.Listener != nil {
hs.log.Debug("Using provided listener") hs.log.Debug("Using provided listener")

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/web/webtest" "github.com/grafana/grafana/pkg/web/webtest"
"golang.org/x/oauth2" "golang.org/x/oauth2"
@ -87,10 +88,12 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) { serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds hs.queryDataService = qds
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, true) hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, true)
hs.QuotaService = quotatest.NewQuotaServiceFake()
}) })
serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) { serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds hs.queryDataService = qds
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, false) hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, false)
hs.QuotaService = quotatest.NewQuotaServiceFake()
}) })
t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) { t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) {
@ -133,6 +136,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
) )
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) { httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds hs.queryDataService = qds
hs.QuotaService = quotatest.NewQuotaServiceFake()
}) })
t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) { t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) {

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/plugindashboards" "github.com/grafana/grafana/pkg/services/plugindashboards"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/web/webtest" "github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -39,6 +40,7 @@ func TestGetPluginDashboards(t *testing.T) {
s := SetupAPITestServer(t, func(hs *HTTPServer) { s := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.pluginDashboardService = pluginDashboardService hs.pluginDashboardService = pluginDashboardService
hs.QuotaService = quotatest.NewQuotaServiceFake()
}) })
t.Run("Not signed in should return 404 Not Found", func(t *testing.T) { t.Run("Not signed in should return 404 Not Found", func(t *testing.T) {

View File

@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/log/logtest"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest" "github.com/grafana/grafana/pkg/web/webtest"
) )
@ -54,6 +55,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled, PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled,
} }
hs.pluginManager = pm hs.pluginManager = pm
hs.QuotaService = quotatest.NewQuotaServiceFake()
}) })
t.Run(testName("Install", tc), func(t *testing.T) { t.Run(testName("Install", tc), func(t *testing.T) {

View File

@ -31,6 +31,10 @@ func (t *TeamGuardianMock) CanAdmin(ctx context.Context, orgId int64, teamId int
return t.result return t.result
} }
func (t *TeamGuardianMock) DeleteByUser(ctx context.Context, userID int64) error {
return t.result
}
func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) { func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) {
const testOrgID int64 = 1 const testOrgID int64 = 1
var userCmd user.CreateUserCommand var userCmd user.CreateUserCommand

View File

@ -253,3 +253,7 @@ func (m *mockQuotaService) QuotaReached(c *models.ReqContext, target string) (bo
func (m *mockQuotaService) CheckQuotaReached(c context.Context, target string, params *quota.ScopeParameters) (bool, error) { func (m *mockQuotaService) CheckQuotaReached(c context.Context, target string, params *quota.ScopeParameters) (bool, error) {
return m.reached, m.err return m.reached, m.err
} }
func (m *mockQuotaService) DeleteByUser(c context.Context, userID int64) error {
return m.err
}

View File

@ -84,7 +84,7 @@ import (
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service" publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/query"
"github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/queryhistory"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/search"
"github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchV2"
@ -181,7 +181,7 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(shorturls.Service), new(*shorturls.ShortURLService)), wire.Bind(new(shorturls.Service), new(*shorturls.ShortURLService)),
queryhistory.ProvideService, queryhistory.ProvideService,
wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)), wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)),
quota.ProvideService, quotaimpl.ProvideService,
remotecache.ProvideService, remotecache.ProvideService,
loginservice.ProvideService, loginservice.ProvideService,
wire.Bind(new(login.Service), new(*loginservice.Implementation)), wire.Bind(new(login.Service), new(*loginservice.Implementation)),

View File

@ -18,7 +18,7 @@ import (
) )
func ProvideService(routeRegister routing.RouteRegister, func ProvideService(routeRegister routing.RouteRegister,
quotaService *quota.QuotaService, quotaService quota.Service,
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store, pluginDashboardService plugindashboards.Service, pluginStore plugins.Store,
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService, libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService,
ac accesscontrol.AccessControl, ac accesscontrol.AccessControl,

View File

@ -70,6 +70,7 @@ type Store interface {
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
// ValidateDashboardBeforeSave validates a dashboard before save. // ValidateDashboardBeforeSave validates a dashboard before save.
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error) ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)
DeleteACLByUser(context.Context, int64) error
FolderStore FolderStore
} }

View File

@ -148,3 +148,11 @@ func (d *DashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Con
return nil return nil
}) })
} }
func (d *DashboardStore) DeleteACLByUser(ctx context.Context, userID int64) error {
return d.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM dashboard_acl WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}

View File

@ -247,6 +247,12 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) {
require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role) require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role)
require.False(t, query.Result[1].Inherited) require.False(t, query.Result[1].Inherited)
}) })
t.Run("Delete acl by user", func(t *testing.T) {
setup(t)
err := dashboardStore.DeleteACLByUser(context.Background(), currentUser.ID)
require.NoError(t, err)
})
} }
func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) user.User { func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) user.User {

View File

@ -593,3 +593,7 @@ func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context,
func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error { func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error {
return dr.dashboardStore.GetDashboardTags(ctx, query) return dr.dashboardStore.GetDashboardTags(ctx, query)
} }
func (dr *DashboardServiceImpl) DeleteACLByUser(ctx context.Context, userID int64) error {
return dr.dashboardStore.DeleteACLByUser(ctx, userID)
}

View File

@ -17,10 +17,7 @@ import (
"github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/setting"
) )
func TestIntegrationDashboardService(t *testing.T) { func TestDashboardService(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("Dashboard service tests", func(t *testing.T) { t.Run("Dashboard service tests", func(t *testing.T) {
fakeStore := dashboards.FakeDashboardStore{} fakeStore := dashboards.FakeDashboardStore{}
defer fakeStore.AssertExpectations(t) defer fakeStore.AssertExpectations(t)
@ -216,6 +213,14 @@ func TestIntegrationDashboardService(t *testing.T) {
err := service.DeleteDashboard(context.Background(), 1, 1) err := service.DeleteDashboard(context.Background(), 1, 1)
require.NoError(t, err) require.NoError(t, err)
}) })
// t.Run("Delete ACL by user", func(t *testing.T) {
// fakeStore := dashboards.FakeDashboardStore{}
// args := 1
// fakeStore.On("DeleteACLByUser", mock.Anything, args).Return(nil).Once()
// err := service.DeleteACLByUser(context.Background(), 1)
// require.NoError(t, err)
// })
}) })
}) })
} }

View File

@ -435,6 +435,19 @@ func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dash
return r0, r1 return r0, r1
} }
func (_m *FakeDashboardStore) DeleteACLByUser(ctx context.Context, userID int64) error{
ret := _m.Called(ctx, userID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// NewFakeDashboardStore creates a new instance of FakeDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. // NewFakeDashboardStore creates a new instance of FakeDashboardStore. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakeDashboardStore(t testing.TB) *FakeDashboardStore { func NewFakeDashboardStore(t testing.TB) *FakeDashboardStore {
mock := &FakeDashboardStore{} mock := &FakeDashboardStore{}

View File

@ -19,7 +19,7 @@ var (
func ProvideService( func ProvideService(
sqlStore sqlstore.Store, sqlStore sqlstore.Store,
userService user.Service, userService user.Service,
quotaService *quota.QuotaService, quotaService quota.Service,
authInfoService login.AuthInfoService, authInfoService login.AuthInfoService,
) *Implementation { ) *Implementation {
s := &Implementation{ s := &Implementation{
@ -35,7 +35,7 @@ type Implementation struct {
SQLStore sqlstore.Store SQLStore sqlstore.Store
userService user.Service userService user.Service
AuthInfoService login.AuthInfoService AuthInfoService login.AuthInfoService
QuotaService *quota.QuotaService QuotaService quota.Service
TeamSync login.TeamSyncFunc TeamSync login.TeamSyncFunc
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/go-kit/log/level" "github.com/go-kit/log/level"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/login/logintest" "github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -28,7 +28,7 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T)
} }
login := Implementation{ login := Implementation{
QuotaService: &quota.QuotaService{}, QuotaService: &quotaimpl.Service{},
AuthInfoService: authInfoMock, AuthInfoService: authInfoMock,
SQLStore: store, SQLStore: store,
} }
@ -52,7 +52,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
} }
login := Implementation{ login := Implementation{
QuotaService: &quota.QuotaService{}, QuotaService: &quotaimpl.Service{},
AuthInfoService: authInfoMock, AuthInfoService: authInfoMock,
SQLStore: store, SQLStore: store,
} }
@ -65,7 +65,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
func Test_teamSync(t *testing.T) { func Test_teamSync(t *testing.T) {
authInfoMock := &logintest.AuthInfoServiceFake{} authInfoMock := &logintest.AuthInfoServiceFake{}
login := Implementation{ login := Implementation{
QuotaService: &quota.QuotaService{}, QuotaService: &quotaimpl.Service{},
AuthInfoService: authInfoMock, AuthInfoService: authInfoMock,
} }

View File

@ -64,7 +64,7 @@ type API struct {
DatasourceCache datasources.CacheService DatasourceCache datasources.CacheService
RouteRegister routing.RouteRegister RouteRegister routing.RouteRegister
ExpressionService *expr.Service ExpressionService *expr.Service
QuotaService *quota.QuotaService QuotaService quota.Service
Schedule schedule.ScheduleService Schedule schedule.ScheduleService
TransactionManager provisioning.TransactionManager TransactionManager provisioning.TransactionManager
ProvenanceStore provisioning.ProvisioningStore ProvenanceStore provisioning.ProvisioningStore

View File

@ -34,7 +34,7 @@ type RulerSrv struct {
provenanceStore provisioning.ProvisioningStore provenanceStore provisioning.ProvisioningStore
store store.RuleStore store store.RuleStore
DatasourceCache datasources.CacheService DatasourceCache datasources.CacheService
QuotaService *quota.QuotaService QuotaService quota.Service
scheduleService schedule.ScheduleService scheduleService schedule.ScheduleService
log log.Logger log log.Logger
cfg *setting.UnifiedAlertingSettings cfg *setting.UnifiedAlertingSettings
@ -393,8 +393,8 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
if len(finalChanges.New) > 0 { if len(finalChanges.New) > 0 {
limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", &quota.ScopeParameters{ limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", &quota.ScopeParameters{
OrgId: c.OrgId, OrgID: c.OrgId,
UserId: c.UserId, UserID: c.UserId,
}) // alert rule is table name }) // alert rule is table name
if err != nil { if err != nil {
return fmt.Errorf("failed to get alert rules quota: %w", err) return fmt.Errorf("failed to get alert rules quota: %w", err)

View File

@ -37,7 +37,7 @@ import (
func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, routeRegister routing.RouteRegister, func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, routeRegister routing.RouteRegister,
sqlStore *sqlstore.SQLStore, kvStore kvstore.KVStore, expressionService *expr.Service, dataProxy *datasourceproxy.DataSourceProxyService, sqlStore *sqlstore.SQLStore, kvStore kvstore.KVStore, expressionService *expr.Service, dataProxy *datasourceproxy.DataSourceProxyService,
quotaService *quota.QuotaService, secretsService secrets.Service, notificationService notifications.Service, m *metrics.NGAlert, quotaService quota.Service, secretsService secrets.Service, notificationService notifications.Service, m *metrics.NGAlert,
folderService dashboards.FolderService, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, renderService rendering.Service, folderService dashboards.FolderService, ac accesscontrol.AccessControl, dashboardService dashboards.DashboardService, renderService rendering.Service,
bus bus.Bus) (*AlertNG, error) { bus bus.Bus) (*AlertNG, error) {
ng := &AlertNG{ ng := &AlertNG{
@ -80,7 +80,7 @@ type AlertNG struct {
KVStore kvstore.KVStore KVStore kvstore.KVStore
ExpressionService *expr.Service ExpressionService *expr.Service
DataProxy *datasourceproxy.DataSourceProxyService DataProxy *datasourceproxy.DataSourceProxyService
QuotaService *quota.QuotaService QuotaService quota.Service
SecretsService secrets.Service SecretsService secrets.Service
Metrics *metrics.NGAlert Metrics *metrics.NGAlert
NotificationService notifications.Service NotificationService notifications.Service

View File

@ -88,8 +88,8 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule model
} }
limitReached, err := service.quotas.CheckQuotaReached(ctx, "alert_rule", &quota.ScopeParameters{ limitReached, err := service.quotas.CheckQuotaReached(ctx, "alert_rule", &quota.ScopeParameters{
OrgId: rule.OrgID, OrgID: rule.OrgID,
UserId: userID, UserID: userID,
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to check alert rule quota: %w", err) return fmt.Errorf("failed to check alert rule quota: %w", err)

View File

@ -6,5 +6,6 @@ import (
type Service interface { type Service interface {
GetIDForNewUser(context.Context, GetOrgIDForNewUserCommand) (int64, error) GetIDForNewUser(context.Context, GetOrgIDForNewUserCommand) (int64, error)
InsertUser(context.Context, *OrgUser) (int64, error) InsertOrgUser(context.Context, *OrgUser) (int64, error)
DeleteUserFromAll(context.Context, int64) error
} }

View File

@ -73,6 +73,10 @@ func (s *Service) GetIDForNewUser(ctx context.Context, cmd org.GetOrgIDForNewUse
return s.store.Insert(ctx, &orga) return s.store.Insert(ctx, &orga)
} }
func (s *Service) InsertUser(ctx context.Context, orguser *org.OrgUser) (int64, error) { func (s *Service) InsertOrgUser(ctx context.Context, orguser *org.OrgUser) (int64, error) {
return s.store.InsertUser(ctx, orguser) return s.store.InsertOrgUser(ctx, orguser)
}
func (s *Service) DeleteUserFromAll(ctx context.Context, userID int64) error {
return s.store.DeleteUserFromAll(ctx, userID)
} }

View File

@ -46,6 +46,11 @@ func TestOrgService(t *testing.T) {
setting.AutoAssignOrg = false setting.AutoAssignOrg = false
setting.AutoAssignOrgId = 0 setting.AutoAssignOrgId = 0
t.Run("delete user from all orgs", func(t *testing.T) {
err := orgService.DeleteUserFromAll(context.Background(), 1)
require.NoError(t, err)
})
} }
type FakeOrgStore struct { type FakeOrgStore struct {
@ -67,6 +72,10 @@ func (f *FakeOrgStore) Insert(ctx context.Context, org *org.Org) (int64, error)
return f.ExpectedOrgID, f.ExpectedError return f.ExpectedOrgID, f.ExpectedError
} }
func (f *FakeOrgStore) InsertUser(ctx context.Context, org *org.OrgUser) (int64, error) { func (f *FakeOrgStore) InsertOrgUser(ctx context.Context, org *org.OrgUser) (int64, error) {
return f.ExpectedUserID, f.ExpectedError return f.ExpectedUserID, f.ExpectedError
} }
func (f *FakeOrgStore) DeleteUserFromAll(ctx context.Context, userID int64) error {
return f.ExpectedError
}

View File

@ -15,7 +15,8 @@ const MainOrgName = "Main Org."
type store interface { type store interface {
Get(context.Context, int64) (*org.Org, error) Get(context.Context, int64) (*org.Org, error)
Insert(context.Context, *org.Org) (int64, error) Insert(context.Context, *org.Org) (int64, error)
InsertUser(context.Context, *org.OrgUser) (int64, error) InsertOrgUser(context.Context, *org.OrgUser) (int64, error)
DeleteUserFromAll(context.Context, int64) error
} }
type sqlStore struct { type sqlStore struct {
@ -67,7 +68,7 @@ func (ss *sqlStore) Insert(ctx context.Context, org *org.Org) (int64, error) {
return orgID, nil return orgID, nil
} }
func (ss *sqlStore) InsertUser(ctx context.Context, cmd *org.OrgUser) (int64, error) { func (ss *sqlStore) InsertOrgUser(ctx context.Context, cmd *org.OrgUser) (int64, error) {
var orgID int64 var orgID int64
var err error var err error
err = ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { err = ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
@ -81,3 +82,12 @@ func (ss *sqlStore) InsertUser(ctx context.Context, cmd *org.OrgUser) (int64, er
} }
return orgID, nil return orgID, nil
} }
func (ss *sqlStore) DeleteUserFromAll(ctx context.Context, userID int64) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
if _, err := sess.Exec("DELETE FROM org_user WHERE user_id = ?", userID); err != nil {
return err
}
return nil
})
}

View File

@ -61,7 +61,7 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) {
} }
t.Run("org user inserted", func(t *testing.T) { t.Run("org user inserted", func(t *testing.T) {
_, err := orgUserStore.InsertUser(context.Background(), &org.OrgUser{ _, err := orgUserStore.InsertOrgUser(context.Background(), &org.OrgUser{
ID: 1, ID: 1,
OrgID: 1, OrgID: 1,
UserID: 1, UserID: 1,
@ -70,4 +70,9 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) {
}) })
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("delete by user", func(t *testing.T) {
err := orgUserStore.DeleteUserFromAll(context.Background(), 1)
require.NoError(t, err)
})
} }

View File

@ -23,6 +23,10 @@ func (f *FakeOrgService) Insert(ctx context.Context, cmd *org.OrgUser) (int64, e
return f.ExpectedOrgUserID, f.ExpectedError return f.ExpectedOrgUserID, f.ExpectedError
} }
func (f *FakeOrgService) InsertUser(ctx context.Context, cmd *org.OrgUser) (int64, error) { func (f *FakeOrgService) InsertOrgUser(ctx context.Context, cmd *org.OrgUser) (int64, error) {
return f.ExpectedOrgUserID, f.ExpectedError return f.ExpectedOrgUserID, f.ExpectedError
} }
func (f *FakeOrgService) DeleteUserFromAll(ctx context.Context, userID int64) error {
return f.ExpectedError
}

View File

@ -10,4 +10,5 @@ type Service interface {
Save(context.Context, *SavePreferenceCommand) error Save(context.Context, *SavePreferenceCommand) error
Patch(context.Context, *PatchPreferenceCommand) error Patch(context.Context, *PatchPreferenceCommand) error
GetDefaults() *Preference GetDefaults() *Preference
DeleteByUser(context.Context, int64) error
} }

View File

@ -120,3 +120,7 @@ func (s *inmemStore) Update(ctx context.Context, preference *pref.Preference) er
s.preference[key] = *preference s.preference[key] = *preference
return nil return nil
} }
func (s *inmemStore) DeleteByUser(ctx context.Context, userID int64) error {
panic("not yet implemented")
}

View File

@ -236,3 +236,7 @@ func (s *Service) GetDefaults() *pref.Preference {
return defaults return defaults
} }
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
return s.store.DeleteByUser(ctx, userID)
}

View File

@ -14,6 +14,7 @@ type store interface {
List(context.Context, *pref.Preference) ([]*pref.Preference, error) List(context.Context, *pref.Preference) ([]*pref.Preference, error)
Insert(context.Context, *pref.Preference) (int64, error) Insert(context.Context, *pref.Preference) (int64, error)
Update(context.Context, *pref.Preference) error Update(context.Context, *pref.Preference) error
DeleteByUser(context.Context, int64) error
} }
type sqlStore struct { type sqlStore struct {
@ -86,3 +87,11 @@ func (s *sqlStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, err
}) })
return ID, err return ID, err
} }
func (s *sqlStore) DeleteByUser(ctx context.Context, userID int64) error {
return s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM preferences WHERE user_id = ?"
_, err := dbSession.Exec(rawSQL, userID)
return err
})
}

View File

@ -34,3 +34,7 @@ func (f *FakePreferenceService) GetDefaults() *pref.Preference {
func (f *FakePreferenceService) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) error { func (f *FakePreferenceService) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) error {
return f.ExpectedError return f.ExpectedError
} }
func (f *FakePreferenceService) DeleteByUser(context.Context, int64) error {
return f.ExpectedError
}

View File

@ -44,7 +44,7 @@ func ProvideService(
alertingService *alerting.AlertNotificationService, alertingService *alerting.AlertNotificationService,
pluginSettings pluginsettings.Service, pluginSettings pluginsettings.Service,
searchService searchV2.SearchService, searchService searchV2.SearchService,
quotaService *quota.QuotaService, quotaService quota.Service,
) (*ProvisioningServiceImpl, error) { ) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{ s := &ProvisioningServiceImpl{
Cfg: cfg, Cfg: cfg,

View File

@ -0,0 +1,10 @@
package quota
import "errors"
var ErrInvalidQuotaTarget = errors.New("invalid quota target")
type ScopeParameters struct {
OrgID int64
UserID int64
}

View File

@ -2,201 +2,12 @@ package quota
import ( import (
"context" "context"
"errors"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
) )
var ErrInvalidQuotaTarget = errors.New("invalid quota target")
func ProvideService(cfg *setting.Cfg, tokenService models.UserTokenService, sqlStore *sqlstore.SQLStore) *QuotaService {
return &QuotaService{
Cfg: cfg,
AuthTokenService: tokenService,
SQLStore: sqlStore,
Logger: log.New("quota_service"),
}
}
type QuotaService struct {
AuthTokenService models.UserTokenService
Cfg *setting.Cfg
SQLStore sqlstore.Store
Logger log.Logger
}
type Service interface { type Service interface {
QuotaReached(c *models.ReqContext, target string) (bool, error) QuotaReached(c *models.ReqContext, target string) (bool, error)
CheckQuotaReached(ctx context.Context, target string, scopeParams *ScopeParameters) (bool, error) CheckQuotaReached(ctx context.Context, target string, scopeParams *ScopeParameters) (bool, error)
} DeleteByUser(context.Context, int64) error
type ScopeParameters struct {
OrgId int64
UserId int64
}
// QuotaReached checks that quota is reached for a target. Runs CheckQuotaReached and take context and scope parameters from the request context
func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool, error) {
if !qs.Cfg.Quota.Enabled {
return false, nil
}
// No request context means this is a background service, like LDAP Background Sync
if c == nil {
return false, nil
}
var params *ScopeParameters
if c.IsSignedIn {
params = &ScopeParameters{
OrgId: c.OrgId,
UserId: c.UserId,
}
}
return qs.CheckQuotaReached(c.Req.Context(), target, params)
}
// CheckQuotaReached check that quota is reached for a target. If ScopeParameters are not defined, only global scope is checked
func (qs *QuotaService) CheckQuotaReached(ctx context.Context, target string, scopeParams *ScopeParameters) (bool, error) {
if !qs.Cfg.Quota.Enabled {
return false, nil
}
// get the list of scopes that this target is valid for. Org, User, Global
scopes, err := qs.getQuotaScopes(target)
if err != nil {
return false, err
}
for _, scope := range scopes {
qs.Logger.Debug("Checking quota", "target", target, "scope", scope)
switch scope.Name {
case "global":
if scope.DefaultLimit < 0 {
continue
}
if scope.DefaultLimit == 0 {
return true, nil
}
if target == "session" {
usedSessions, err := qs.AuthTokenService.ActiveTokenCount(ctx)
if err != nil {
return false, err
}
if usedSessions > scope.DefaultLimit {
qs.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
return true, nil
}
continue
}
query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target, UnifiedAlertingEnabled: qs.Cfg.UnifiedAlerting.IsEnabled()}
if err := qs.SQLStore.GetGlobalQuotaByTarget(ctx, &query); err != nil {
return true, err
}
if query.Result.Used >= scope.DefaultLimit {
return true, nil
}
case "org":
if scopeParams == nil {
continue
}
query := models.GetOrgQuotaByTargetQuery{
OrgId: scopeParams.OrgId,
Target: scope.Target,
Default: scope.DefaultLimit,
UnifiedAlertingEnabled: qs.Cfg.UnifiedAlerting.IsEnabled(),
}
if err := qs.SQLStore.GetOrgQuotaByTarget(ctx, &query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
case "user":
if scopeParams == nil || scopeParams.UserId == 0 {
continue
}
query := models.GetUserQuotaByTargetQuery{UserId: scopeParams.UserId, Target: scope.Target, Default: scope.DefaultLimit, UnifiedAlertingEnabled: qs.Cfg.UnifiedAlerting.IsEnabled()}
if err := qs.SQLStore.GetUserQuotaByTarget(ctx, &query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
}
}
return false, nil
}
func (qs *QuotaService) getQuotaScopes(target string) ([]models.QuotaScope, error) {
scopes := make([]models.QuotaScope, 0)
switch target {
case "user":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.User},
models.QuotaScope{Name: "org", Target: "org_user", DefaultLimit: qs.Cfg.Quota.Org.User},
)
return scopes, nil
case "org":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Org},
models.QuotaScope{Name: "user", Target: "org_user", DefaultLimit: qs.Cfg.Quota.User.Org},
)
return scopes, nil
case "dashboard":
scopes = append(scopes,
models.QuotaScope{
Name: "global",
Target: target,
DefaultLimit: qs.Cfg.Quota.Global.Dashboard,
},
models.QuotaScope{
Name: "org",
Target: target,
DefaultLimit: qs.Cfg.Quota.Org.Dashboard,
},
)
return scopes, nil
case "data_source":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.DataSource},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.DataSource},
)
return scopes, nil
case "api_key":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.ApiKey},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.ApiKey},
)
return scopes, nil
case "session":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Session},
)
return scopes, nil
case "alert_rule": // target need to match the respective database name
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.AlertRule},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.AlertRule},
)
return scopes, nil
default:
return scopes, ErrInvalidQuotaTarget
}
} }

View File

@ -0,0 +1,200 @@
package quotaimpl
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
"github.com/grafana/grafana/pkg/setting"
)
type Service struct {
store store
AuthTokenService models.UserTokenService
Cfg *setting.Cfg
SQLStore sqlstore.Store
Logger log.Logger
}
func ProvideService(db db.DB, cfg *setting.Cfg, tokenService models.UserTokenService, ss *sqlstore.SQLStore) quota.Service {
return &Service{
store: &sqlStore{db: db},
Cfg: cfg,
AuthTokenService: tokenService,
SQLStore: ss,
Logger: log.New("quota_service"),
}
}
// QuotaReached checks that quota is reached for a target. Runs CheckQuotaReached and take context and scope parameters from the request context
func (s *Service) QuotaReached(c *models.ReqContext, target string) (bool, error) {
if !s.Cfg.Quota.Enabled {
return false, nil
}
// No request context means this is a background service, like LDAP Background Sync
if c == nil {
return false, nil
}
var params *quota.ScopeParameters
if c.IsSignedIn {
params = &quota.ScopeParameters{
OrgID: c.OrgId,
UserID: c.UserId,
}
}
return s.CheckQuotaReached(c.Req.Context(), target, params)
}
// CheckQuotaReached check that quota is reached for a target. If ScopeParameters are not defined, only global scope is checked
func (s *Service) CheckQuotaReached(ctx context.Context, target string, scopeParams *quota.ScopeParameters) (bool, error) {
if !s.Cfg.Quota.Enabled {
return false, nil
}
// get the list of scopes that this target is valid for. Org, User, Global
scopes, err := s.getQuotaScopes(target)
if err != nil {
return false, err
}
for _, scope := range scopes {
s.Logger.Debug("Checking quota", "target", target, "scope", scope)
switch scope.Name {
case "global":
if scope.DefaultLimit < 0 {
continue
}
if scope.DefaultLimit == 0 {
return true, nil
}
if target == "session" {
usedSessions, err := s.AuthTokenService.ActiveTokenCount(ctx)
if err != nil {
return false, err
}
if usedSessions > scope.DefaultLimit {
s.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
return true, nil
}
continue
}
query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target, UnifiedAlertingEnabled: s.Cfg.UnifiedAlerting.IsEnabled()}
// TODO : move GetGlobalQuotaByTarget to a global quota service
if err := s.SQLStore.GetGlobalQuotaByTarget(ctx, &query); err != nil {
return true, err
}
if query.Result.Used >= scope.DefaultLimit {
return true, nil
}
case "org":
if scopeParams == nil {
continue
}
query := models.GetOrgQuotaByTargetQuery{
OrgId: scopeParams.OrgID,
Target: scope.Target,
Default: scope.DefaultLimit,
UnifiedAlertingEnabled: s.Cfg.UnifiedAlerting.IsEnabled(),
}
// TODO: move GetOrgQuotaByTarget from sqlstore to quota store
if err := s.SQLStore.GetOrgQuotaByTarget(ctx, &query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
case "user":
if scopeParams == nil || scopeParams.UserID == 0 {
continue
}
query := models.GetUserQuotaByTargetQuery{UserId: scopeParams.UserID, Target: scope.Target, Default: scope.DefaultLimit, UnifiedAlertingEnabled: s.Cfg.UnifiedAlerting.IsEnabled()}
// TODO: move GetUserQuotaByTarget from sqlstore to quota store
if err := s.SQLStore.GetUserQuotaByTarget(ctx, &query); err != nil {
return true, err
}
if query.Result.Limit < 0 {
continue
}
if query.Result.Limit == 0 {
return true, nil
}
if query.Result.Used >= query.Result.Limit {
return true, nil
}
}
}
return false, nil
}
func (s *Service) getQuotaScopes(target string) ([]models.QuotaScope, error) {
scopes := make([]models.QuotaScope, 0)
switch target {
case "user":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.User},
models.QuotaScope{Name: "org", Target: "org_user", DefaultLimit: s.Cfg.Quota.Org.User},
)
return scopes, nil
case "org":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.Org},
models.QuotaScope{Name: "user", Target: "org_user", DefaultLimit: s.Cfg.Quota.User.Org},
)
return scopes, nil
case "dashboard":
scopes = append(scopes,
models.QuotaScope{
Name: "global",
Target: target,
DefaultLimit: s.Cfg.Quota.Global.Dashboard,
},
models.QuotaScope{
Name: "org",
Target: target,
DefaultLimit: s.Cfg.Quota.Org.Dashboard,
},
)
return scopes, nil
case "data_source":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.DataSource},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: s.Cfg.Quota.Org.DataSource},
)
return scopes, nil
case "api_key":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.ApiKey},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: s.Cfg.Quota.Org.ApiKey},
)
return scopes, nil
case "session":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.Session},
)
return scopes, nil
case "alert_rule": // target need to match the respective database name
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.AlertRule},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: s.Cfg.Quota.Org.AlertRule},
)
return scopes, nil
default:
return scopes, quota.ErrInvalidQuotaTarget
}
}
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
return s.store.DeleteByUser(ctx, userID)
}

View File

@ -0,0 +1,28 @@
package quotaimpl
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestQuotaService(t *testing.T) {
quotaStore := &FakeQuotaStore{}
quotaService := Service{
store: quotaStore,
}
t.Run("delete quota", func(t *testing.T) {
err := quotaService.DeleteByUser(context.Background(), 1)
require.NoError(t, err)
})
}
type FakeQuotaStore struct {
ExpectedError error
}
func (f *FakeQuotaStore) DeleteByUser(ctx context.Context, userID int64) error {
return f.ExpectedError
}

View File

@ -0,0 +1,24 @@
package quotaimpl
import (
"context"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
)
type store interface {
DeleteByUser(context.Context, int64) error
}
type sqlStore struct {
db db.DB
}
func (ss *sqlStore) DeleteByUser(ctx context.Context, userID int64) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM quota WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}

View File

@ -0,0 +1,25 @@
package quotaimpl
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/require"
)
func TestIntegrationQuotaDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ss := sqlstore.InitTestDB(t)
quotaStore := sqlStore{
db: ss,
}
t.Run("quota deleted", func(t *testing.T) {
err := quotaStore.DeleteByUser(context.Background(), 1)
require.NoError(t, err)
})
}

View File

@ -0,0 +1,29 @@
package quotatest
import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
)
type FakeQuotaService struct {
reached bool
err error
}
func NewQuotaServiceFake() *FakeQuotaService {
return &FakeQuotaService{}
}
func (f *FakeQuotaService) QuotaReached(c *models.ReqContext, target string) (bool, error) {
return f.reached, f.err
}
func (f *FakeQuotaService) CheckQuotaReached(c context.Context, target string, params *quota.ScopeParameters) (bool, error) {
return f.reached, f.err
}
func (f *FakeQuotaService) DeleteByUser(c context.Context, userID int64) error {
return f.err
}

View File

@ -5,8 +5,9 @@ import (
) )
type Service interface { type Service interface {
Add(ctx context.Context, cmd *StarDashboardCommand) error Add(context.Context, *StarDashboardCommand) error
Delete(ctx context.Context, cmd *UnstarDashboardCommand) error Delete(context.Context, *UnstarDashboardCommand) error
IsStarredByUser(ctx context.Context, query *IsStarredByUserQuery) (bool, error) DeleteByUser(context.Context, int64) error
GetByUser(ctx context.Context, cmd *GetUserStarsQuery) (*GetUserStarsResult, error) IsStarredByUser(context.Context, *IsStarredByUserQuery) (bool, error)
GetByUser(context.Context, *GetUserStarsQuery) (*GetUserStarsResult, error)
} }

View File

@ -40,3 +40,7 @@ func (s *Service) IsStarredByUser(ctx context.Context, query *star.IsStarredByUs
func (s *Service) GetByUser(ctx context.Context, cmd *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) { func (s *Service) GetByUser(ctx context.Context, cmd *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) {
return s.store.List(ctx, cmd) return s.store.List(ctx, cmd)
} }
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
return s.store.DeleteByUser(ctx, userID)
}

View File

@ -9,10 +9,11 @@ import (
) )
type store interface { type store interface {
Get(ctx context.Context, query *star.IsStarredByUserQuery) (bool, error) Get(context.Context, *star.IsStarredByUserQuery) (bool, error)
Insert(ctx context.Context, cmd *star.StarDashboardCommand) error Insert(context.Context, *star.StarDashboardCommand) error
Delete(ctx context.Context, cmd *star.UnstarDashboardCommand) error Delete(context.Context, *star.UnstarDashboardCommand) error
List(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) DeleteByUser(context.Context, int64) error
List(context.Context, *star.GetUserStarsQuery) (*star.GetUserStarsResult, error)
} }
type sqlStore struct { type sqlStore struct {
@ -55,6 +56,14 @@ func (s *sqlStore) Delete(ctx context.Context, cmd *star.UnstarDashboardCommand)
}) })
} }
func (s *sqlStore) DeleteByUser(ctx context.Context, userID int64) error {
return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM star WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}
func (s *sqlStore) List(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) { func (s *sqlStore) List(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) {
userStars := make(map[int64]bool) userStars := make(map[int64]bool)
err := s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { err := s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {

View File

@ -56,5 +56,10 @@ func TestIntegrationUserStarsDataAccess(t *testing.T) {
require.False(t, isStarred) require.False(t, isStarred)
}) })
}) })
t.Run("delete by user", func(t *testing.T) {
err := starStore.DeleteByUser(context.Background(), 1)
require.NoError(t, err)
})
}) })
} }

View File

@ -28,6 +28,10 @@ func (f *FakeStarService) Delete(ctx context.Context, cmd *star.UnstarDashboardC
return f.ExpectedError return f.ExpectedError
} }
func (f *FakeStarService) DeleteByUser(ctx context.Context, userID int64) error {
return f.ExpectedError
}
func (f *FakeStarService) GetByUser(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) { func (f *FakeStarService) GetByUser(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error) {
return f.ExpectedUserStars, f.ExpectedError return f.ExpectedUserStars, f.ExpectedError
} }

View File

@ -22,3 +22,11 @@ func (t *TeamGuardianStoreImpl) GetTeamMembers(ctx context.Context, query models
return query.Result, nil return query.Result, nil
} }
func (t *TeamGuardianStoreImpl) DeleteByUser(ctx context.Context, userID int64) error {
return t.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM team_member WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}

View File

@ -15,3 +15,8 @@ func (t *TeamGuardianStoreMock) GetTeamMembers(ctx context.Context, query models
args := t.Called(ctx, query) args := t.Called(ctx, query)
return args.Get(0).([]*models.TeamMemberDTO), args.Error(1) return args.Get(0).([]*models.TeamMemberDTO), args.Error(1)
} }
func (t *TeamGuardianStoreMock) DeleteByUser(ctx context.Context, userID int64) error {
args := t.Called(ctx, userID)
return args.Get(0).(error)
}

View File

@ -44,3 +44,7 @@ func (s *Service) CanAdmin(ctx context.Context, orgId int64, teamId int64, user
return models.ErrNotAllowedToUpdateTeam return models.ErrNotAllowedToUpdateTeam
} }
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
return s.store.DeleteByUser(ctx, userID)
}

View File

@ -15,3 +15,8 @@ func (t *TeamGuardianMock) CanAdmin(ctx context.Context, orgId int64, teamId int
args := t.Called(ctx, orgId, teamId, user) args := t.Called(ctx, orgId, teamId, user)
return args.Error(0) return args.Error(0)
} }
func (t *TeamGuardianMock) DeleteByUser(context.Context, int64) error {
args := t.Called(context.Background(), 0)
return args.Error(0)
}

View File

@ -7,9 +7,11 @@ import (
) )
type TeamGuardian interface { type TeamGuardian interface {
CanAdmin(ctx context.Context, orgId int64, teamId int64, user *models.SignedInUser) error CanAdmin(context.Context, int64, int64, *models.SignedInUser) error
DeleteByUser(context.Context, int64) error
} }
type Store interface { type Store interface {
GetTeamMembers(ctx context.Context, query models.GetTeamMembersQuery) ([]*models.TeamMemberDTO, error) GetTeamMembers(context.Context, models.GetTeamMembersQuery) ([]*models.TeamMemberDTO, error)
DeleteByUser(context.Context, int64) error
} }

View File

@ -110,7 +110,7 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use
orgUser.Role = org.RoleType(setting.AutoAssignOrgRole) orgUser.Role = org.RoleType(setting.AutoAssignOrgRole)
} }
} }
_, err = s.orgService.InsertUser(ctx, &orgUser) _, err = s.orgService.InsertOrgUser(ctx, &orgUser)
if err != nil { if err != nil {
// HERE ADD DELETE USER // HERE ADD DELETE USER
return usr, err return usr, err

View File

@ -0,0 +1,8 @@
package userauth
import "context"
type Service interface {
Delete(ctx context.Context, userID int64) error
DeleteToken(ctx context.Context, userID int64) error
}

View File

@ -0,0 +1,33 @@
package userauthimpl
import (
"context"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
)
type store interface {
Delete(context.Context, int64) error
DeleteToken(context.Context, int64) error
}
type sqlStore struct {
db db.DB
}
func (ss *sqlStore) Delete(ctx context.Context, userID int64) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM user_auth WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}
func (ss *sqlStore) DeleteToken(ctx context.Context, userID int64) error {
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
var rawSQL = "DELETE FROM user_auth_token WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}

View File

@ -0,0 +1,30 @@
package userauthimpl
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/stretchr/testify/require"
)
func TestIntegrationUserAuthDataAccess(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ss := sqlstore.InitTestDB(t)
userAuthStore := sqlStore{
db: ss,
}
t.Run("delete user auth", func(t *testing.T) {
err := userAuthStore.Delete(context.Background(), 1)
require.NoError(t, err)
})
t.Run("delete user auth token", func(t *testing.T) {
err := userAuthStore.DeleteToken(context.Background(), 1)
require.NoError(t, err)
})
}

View File

@ -0,0 +1,28 @@
package userauthimpl
import (
"context"
"github.com/grafana/grafana/pkg/services/sqlstore/db"
"github.com/grafana/grafana/pkg/services/userauth"
)
type Service struct {
store store
}
func ProvideService(db db.DB) userauth.Service {
return &Service{
store: &sqlStore{
db: db,
},
}
}
func (s *Service) Delete(ctx context.Context, userID int64) error {
return s.store.Delete(ctx, userID)
}
func (s *Service) DeleteToken(ctx context.Context, userID int64) error {
return s.store.DeleteToken(ctx, userID)
}

View File

@ -0,0 +1,37 @@
package userauthimpl
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestUserAuthService(t *testing.T) {
userAuthStore := &FakeUserAuthStore{}
userAuthService := Service{
store: userAuthStore,
}
t.Run("delete user", func(t *testing.T) {
err := userAuthService.Delete(context.Background(), 1)
require.NoError(t, err)
})
t.Run("delete token", func(t *testing.T) {
err := userAuthService.DeleteToken(context.Background(), 1)
require.NoError(t, err)
})
}
type FakeUserAuthStore struct {
ExpectedError error
}
func (f *FakeUserAuthStore) Delete(ctx context.Context, userID int64) error {
return f.ExpectedError
}
func (f *FakeUserAuthStore) DeleteToken(ctx context.Context, userID int64) error {
return f.ExpectedError
}