mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 15:32:49 +08:00
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:
@ -38,7 +38,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/login/loginservice"
|
||||
"github.com/grafana/grafana/pkg/services/login/logintest"
|
||||
"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/searchusers"
|
||||
"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),
|
||||
License: &licensing.OSSLicensingService{},
|
||||
Features: featuremgmt.WithFeatures(),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||
RouteRegister: routing.NewRouteRegister(),
|
||||
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
|
||||
searchUsersService: searchusers.ProvideUsersService(store, filters.ProvideOSSSearchUserFilter()),
|
||||
@ -379,7 +379,7 @@ func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessCo
|
||||
Cfg: cfg,
|
||||
Features: features,
|
||||
Live: newTestLive(t, db),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||
RouteRegister: routeRegister,
|
||||
SQLStore: store,
|
||||
License: &licensing.OSSLicensingService{},
|
||||
|
@ -34,7 +34,7 @@ import (
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
"github.com/grafana/grafana/pkg/services/preference/preftest"
|
||||
"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/mockstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -1012,7 +1012,7 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
|
||||
Cfg: cfg,
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||
QuotaService: "a.QuotaService{
|
||||
QuotaService: "aimpl.Service{
|
||||
Cfg: cfg,
|
||||
},
|
||||
pluginStore: &fakePluginStore{},
|
||||
@ -1048,7 +1048,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
|
||||
Cfg: cfg,
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
SQLStore: sqlmock,
|
||||
@ -1085,7 +1085,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
|
||||
Cfg: cfg,
|
||||
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
|
||||
Live: newTestLive(t, sqlstore.InitTestDB(t)),
|
||||
QuotaService: "a.QuotaService{Cfg: cfg},
|
||||
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||
LibraryPanelService: &mockLibraryPanelService{},
|
||||
LibraryElementService: &mockLibraryElementService{},
|
||||
DashboardService: mock,
|
||||
|
@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"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/setting"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
@ -143,6 +144,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
|
||||
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.folderService = folderService
|
||||
hs.AccessControl = acmock.New()
|
||||
hs.QuotaService = quotatest.NewQuotaServiceFake()
|
||||
})
|
||||
|
||||
t.Run("Should attach access control metadata to multiple folders", func(t *testing.T) {
|
||||
|
@ -62,11 +62,11 @@ import (
|
||||
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service"
|
||||
pref "github.com/grafana/grafana/pkg/services/preference"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
|
||||
publicdashboardsApi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
"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/search"
|
||||
"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/thumbs"
|
||||
"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/web"
|
||||
)
|
||||
@ -103,7 +104,7 @@ type HTTPServer struct {
|
||||
CacheService *localcache.CacheService
|
||||
DataSourceCache datasources.CacheService
|
||||
AuthTokenService models.UserTokenService
|
||||
QuotaService *quota.QuotaService
|
||||
QuotaService quota.Service
|
||||
RemoteCacheService *remotecache.RemoteCache
|
||||
ProvisioningService provisioning.ProvisioningService
|
||||
Login login.Service
|
||||
@ -171,6 +172,7 @@ type HTTPServer struct {
|
||||
CoremodelStaticRegistry *registry.Static
|
||||
kvStore kvstore.KVStore
|
||||
secretsMigrator secrets.Migrator
|
||||
userService user.Service
|
||||
}
|
||||
|
||||
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,
|
||||
contextHandler *contexthandler.ContextHandler, features *featuremgmt.FeatureManager,
|
||||
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,
|
||||
pluginsUpdateChecker *updatechecker.PluginsService, searchUsersService searchusers.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,
|
||||
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
|
||||
starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static,
|
||||
kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, publicDashboardsApi *publicdashboardsApi.Api,
|
||||
) (*HTTPServer, error) {
|
||||
kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck,
|
||||
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) {
|
||||
web.Env = cfg.Env
|
||||
m := web.New()
|
||||
|
||||
@ -292,6 +294,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
|
||||
kvStore: kvStore,
|
||||
PublicDashboardsApi: publicDashboardsApi,
|
||||
secretsMigrator: secretsMigrator,
|
||||
userService: userService,
|
||||
}
|
||||
if hs.Listener != nil {
|
||||
hs.log.Debug("Using provided listener")
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
@ -87,10 +88,12 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
||||
serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.queryDataService = qds
|
||||
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, true)
|
||||
hs.QuotaService = quotatest.NewQuotaServiceFake()
|
||||
})
|
||||
serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.queryDataService = qds
|
||||
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) {
|
||||
@ -133,6 +136,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
||||
)
|
||||
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
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) {
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/plugindashboards"
|
||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||
"github.com/grafana/grafana/pkg/web/webtest"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -39,6 +40,7 @@ func TestGetPluginDashboards(t *testing.T) {
|
||||
|
||||
s := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.pluginDashboardService = pluginDashboardService
|
||||
hs.QuotaService = quotatest.NewQuotaServiceFake()
|
||||
})
|
||||
|
||||
t.Run("Not signed in should return 404 Not Found", func(t *testing.T) {
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"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/web/webtest"
|
||||
)
|
||||
@ -54,6 +55,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
|
||||
PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled,
|
||||
}
|
||||
hs.pluginManager = pm
|
||||
hs.QuotaService = quotatest.NewQuotaServiceFake()
|
||||
})
|
||||
|
||||
t.Run(testName("Install", tc), func(t *testing.T) {
|
||||
|
@ -31,6 +31,10 @@ func (t *TeamGuardianMock) CanAdmin(ctx context.Context, orgId int64, teamId int
|
||||
return t.result
|
||||
}
|
||||
|
||||
func (t *TeamGuardianMock) DeleteByUser(ctx context.Context, userID int64) error {
|
||||
return t.result
|
||||
}
|
||||
|
||||
func setUpGetTeamMembersHandler(t *testing.T, sqlStore *sqlstore.SQLStore) {
|
||||
const testOrgID int64 = 1
|
||||
var userCmd user.CreateUserCommand
|
||||
|
@ -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) {
|
||||
return m.reached, m.err
|
||||
}
|
||||
|
||||
func (m *mockQuotaService) DeleteByUser(c context.Context, userID int64) error {
|
||||
return m.err
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ import (
|
||||
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
"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/search"
|
||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||
@ -181,7 +181,7 @@ var wireBasicSet = wire.NewSet(
|
||||
wire.Bind(new(shorturls.Service), new(*shorturls.ShortURLService)),
|
||||
queryhistory.ProvideService,
|
||||
wire.Bind(new(queryhistory.Service), new(*queryhistory.QueryHistoryService)),
|
||||
quota.ProvideService,
|
||||
quotaimpl.ProvideService,
|
||||
remotecache.ProvideService,
|
||||
loginservice.ProvideService,
|
||||
wire.Bind(new(login.Service), new(*loginservice.Implementation)),
|
||||
|
@ -18,7 +18,7 @@ import (
|
||||
)
|
||||
|
||||
func ProvideService(routeRegister routing.RouteRegister,
|
||||
quotaService *quota.QuotaService,
|
||||
quotaService quota.Service,
|
||||
pluginDashboardService plugindashboards.Service, pluginStore plugins.Store,
|
||||
libraryPanelService librarypanels.Service, dashboardService dashboards.DashboardService,
|
||||
ac accesscontrol.AccessControl,
|
||||
|
@ -70,6 +70,7 @@ type Store interface {
|
||||
UpdateDashboardACL(ctx context.Context, uid int64, items []*models.DashboardAcl) error
|
||||
// ValidateDashboardBeforeSave validates a dashboard before save.
|
||||
ValidateDashboardBeforeSave(dashboard *models.Dashboard, overwrite bool) (bool, error)
|
||||
DeleteACLByUser(context.Context, int64) error
|
||||
|
||||
FolderStore
|
||||
}
|
||||
|
@ -148,3 +148,11 @@ func (d *DashboardStore) HasAdminPermissionInDashboardsOrFolders(ctx context.Con
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -247,6 +247,12 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) {
|
||||
require.Equal(t, models.ROLE_EDITOR, *query.Result[1].Role)
|
||||
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 {
|
||||
|
@ -593,3 +593,7 @@ func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context,
|
||||
func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error {
|
||||
return dr.dashboardStore.GetDashboardTags(ctx, query)
|
||||
}
|
||||
|
||||
func (dr *DashboardServiceImpl) DeleteACLByUser(ctx context.Context, userID int64) error {
|
||||
return dr.dashboardStore.DeleteACLByUser(ctx, userID)
|
||||
}
|
||||
|
@ -17,10 +17,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func TestIntegrationDashboardService(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
func TestDashboardService(t *testing.T) {
|
||||
t.Run("Dashboard service tests", func(t *testing.T) {
|
||||
fakeStore := dashboards.FakeDashboardStore{}
|
||||
defer fakeStore.AssertExpectations(t)
|
||||
@ -216,6 +213,14 @@ func TestIntegrationDashboardService(t *testing.T) {
|
||||
err := service.DeleteDashboard(context.Background(), 1, 1)
|
||||
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)
|
||||
// })
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -435,6 +435,19 @@ func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(dashboard *models.Dash
|
||||
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.
|
||||
func NewFakeDashboardStore(t testing.TB) *FakeDashboardStore {
|
||||
mock := &FakeDashboardStore{}
|
||||
|
@ -19,7 +19,7 @@ var (
|
||||
func ProvideService(
|
||||
sqlStore sqlstore.Store,
|
||||
userService user.Service,
|
||||
quotaService *quota.QuotaService,
|
||||
quotaService quota.Service,
|
||||
authInfoService login.AuthInfoService,
|
||||
) *Implementation {
|
||||
s := &Implementation{
|
||||
@ -35,7 +35,7 @@ type Implementation struct {
|
||||
SQLStore sqlstore.Store
|
||||
userService user.Service
|
||||
AuthInfoService login.AuthInfoService
|
||||
QuotaService *quota.QuotaService
|
||||
QuotaService quota.Service
|
||||
TeamSync login.TeamSyncFunc
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@ import (
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"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/user"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -28,7 +28,7 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T)
|
||||
}
|
||||
|
||||
login := Implementation{
|
||||
QuotaService: "a.QuotaService{},
|
||||
QuotaService: "aimpl.Service{},
|
||||
AuthInfoService: authInfoMock,
|
||||
SQLStore: store,
|
||||
}
|
||||
@ -52,7 +52,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
|
||||
}
|
||||
|
||||
login := Implementation{
|
||||
QuotaService: "a.QuotaService{},
|
||||
QuotaService: "aimpl.Service{},
|
||||
AuthInfoService: authInfoMock,
|
||||
SQLStore: store,
|
||||
}
|
||||
@ -65,7 +65,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
|
||||
func Test_teamSync(t *testing.T) {
|
||||
authInfoMock := &logintest.AuthInfoServiceFake{}
|
||||
login := Implementation{
|
||||
QuotaService: "a.QuotaService{},
|
||||
QuotaService: "aimpl.Service{},
|
||||
AuthInfoService: authInfoMock,
|
||||
}
|
||||
|
||||
|
@ -64,7 +64,7 @@ type API struct {
|
||||
DatasourceCache datasources.CacheService
|
||||
RouteRegister routing.RouteRegister
|
||||
ExpressionService *expr.Service
|
||||
QuotaService *quota.QuotaService
|
||||
QuotaService quota.Service
|
||||
Schedule schedule.ScheduleService
|
||||
TransactionManager provisioning.TransactionManager
|
||||
ProvenanceStore provisioning.ProvisioningStore
|
||||
|
@ -34,7 +34,7 @@ type RulerSrv struct {
|
||||
provenanceStore provisioning.ProvisioningStore
|
||||
store store.RuleStore
|
||||
DatasourceCache datasources.CacheService
|
||||
QuotaService *quota.QuotaService
|
||||
QuotaService quota.Service
|
||||
scheduleService schedule.ScheduleService
|
||||
log log.Logger
|
||||
cfg *setting.UnifiedAlertingSettings
|
||||
@ -393,8 +393,8 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
|
||||
|
||||
if len(finalChanges.New) > 0 {
|
||||
limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", "a.ScopeParameters{
|
||||
OrgId: c.OrgId,
|
||||
UserId: c.UserId,
|
||||
OrgID: c.OrgId,
|
||||
UserID: c.UserId,
|
||||
}) // alert rule is table name
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get alert rules quota: %w", err)
|
||||
|
@ -37,7 +37,7 @@ import (
|
||||
|
||||
func ProvideService(cfg *setting.Cfg, dataSourceCache datasources.CacheService, routeRegister routing.RouteRegister,
|
||||
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,
|
||||
bus bus.Bus) (*AlertNG, error) {
|
||||
ng := &AlertNG{
|
||||
@ -80,7 +80,7 @@ type AlertNG struct {
|
||||
KVStore kvstore.KVStore
|
||||
ExpressionService *expr.Service
|
||||
DataProxy *datasourceproxy.DataSourceProxyService
|
||||
QuotaService *quota.QuotaService
|
||||
QuotaService quota.Service
|
||||
SecretsService secrets.Service
|
||||
Metrics *metrics.NGAlert
|
||||
NotificationService notifications.Service
|
||||
|
@ -88,8 +88,8 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule model
|
||||
}
|
||||
|
||||
limitReached, err := service.quotas.CheckQuotaReached(ctx, "alert_rule", "a.ScopeParameters{
|
||||
OrgId: rule.OrgID,
|
||||
UserId: userID,
|
||||
OrgID: rule.OrgID,
|
||||
UserID: userID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check alert rule quota: %w", err)
|
||||
|
@ -6,5 +6,6 @@ import (
|
||||
|
||||
type Service interface {
|
||||
GetIDForNewUser(context.Context, GetOrgIDForNewUserCommand) (int64, error)
|
||||
InsertUser(context.Context, *OrgUser) (int64, error)
|
||||
InsertOrgUser(context.Context, *OrgUser) (int64, error)
|
||||
DeleteUserFromAll(context.Context, int64) error
|
||||
}
|
||||
|
@ -73,6 +73,10 @@ func (s *Service) GetIDForNewUser(ctx context.Context, cmd org.GetOrgIDForNewUse
|
||||
return s.store.Insert(ctx, &orga)
|
||||
}
|
||||
|
||||
func (s *Service) InsertUser(ctx context.Context, orguser *org.OrgUser) (int64, error) {
|
||||
return s.store.InsertUser(ctx, orguser)
|
||||
func (s *Service) InsertOrgUser(ctx context.Context, orguser *org.OrgUser) (int64, error) {
|
||||
return s.store.InsertOrgUser(ctx, orguser)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteUserFromAll(ctx context.Context, userID int64) error {
|
||||
return s.store.DeleteUserFromAll(ctx, userID)
|
||||
}
|
||||
|
@ -46,6 +46,11 @@ func TestOrgService(t *testing.T) {
|
||||
|
||||
setting.AutoAssignOrg = false
|
||||
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 {
|
||||
@ -67,6 +72,10 @@ func (f *FakeOrgStore) Insert(ctx context.Context, org *org.Org) (int64, error)
|
||||
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
|
||||
}
|
||||
|
||||
func (f *FakeOrgStore) DeleteUserFromAll(ctx context.Context, userID int64) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
@ -15,7 +15,8 @@ const MainOrgName = "Main Org."
|
||||
type store interface {
|
||||
Get(context.Context, int64) (*org.Org, 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 {
|
||||
@ -67,7 +68,7 @@ func (ss *sqlStore) Insert(ctx context.Context, org *org.Org) (int64, error) {
|
||||
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 err 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
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -61,7 +61,7 @@ func TestIntegrationOrgUserDataAccess(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,
|
||||
OrgID: 1,
|
||||
UserID: 1,
|
||||
@ -70,4 +70,9 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("delete by user", func(t *testing.T) {
|
||||
err := orgUserStore.DeleteUserFromAll(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
@ -23,6 +23,10 @@ func (f *FakeOrgService) Insert(ctx context.Context, cmd *org.OrgUser) (int64, e
|
||||
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
|
||||
}
|
||||
|
||||
func (f *FakeOrgService) DeleteUserFromAll(ctx context.Context, userID int64) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
@ -10,4 +10,5 @@ type Service interface {
|
||||
Save(context.Context, *SavePreferenceCommand) error
|
||||
Patch(context.Context, *PatchPreferenceCommand) error
|
||||
GetDefaults() *Preference
|
||||
DeleteByUser(context.Context, int64) error
|
||||
}
|
||||
|
@ -120,3 +120,7 @@ func (s *inmemStore) Update(ctx context.Context, preference *pref.Preference) er
|
||||
s.preference[key] = *preference
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *inmemStore) DeleteByUser(ctx context.Context, userID int64) error {
|
||||
panic("not yet implemented")
|
||||
}
|
||||
|
@ -236,3 +236,7 @@ func (s *Service) GetDefaults() *pref.Preference {
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
|
||||
return s.store.DeleteByUser(ctx, userID)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ type store interface {
|
||||
List(context.Context, *pref.Preference) ([]*pref.Preference, error)
|
||||
Insert(context.Context, *pref.Preference) (int64, error)
|
||||
Update(context.Context, *pref.Preference) error
|
||||
DeleteByUser(context.Context, int64) error
|
||||
}
|
||||
|
||||
type sqlStore struct {
|
||||
@ -86,3 +87,11 @@ func (s *sqlStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, 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
|
||||
})
|
||||
}
|
||||
|
@ -34,3 +34,7 @@ func (f *FakePreferenceService) GetDefaults() *pref.Preference {
|
||||
func (f *FakePreferenceService) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
||||
func (f *FakePreferenceService) DeleteByUser(context.Context, int64) error {
|
||||
return f.ExpectedError
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func ProvideService(
|
||||
alertingService *alerting.AlertNotificationService,
|
||||
pluginSettings pluginsettings.Service,
|
||||
searchService searchV2.SearchService,
|
||||
quotaService *quota.QuotaService,
|
||||
quotaService quota.Service,
|
||||
) (*ProvisioningServiceImpl, error) {
|
||||
s := &ProvisioningServiceImpl{
|
||||
Cfg: cfg,
|
||||
|
10
pkg/services/quota/model.go
Normal file
10
pkg/services/quota/model.go
Normal file
@ -0,0 +1,10 @@
|
||||
package quota
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrInvalidQuotaTarget = errors.New("invalid quota target")
|
||||
|
||||
type ScopeParameters struct {
|
||||
OrgID int64
|
||||
UserID int64
|
||||
}
|
@ -2,201 +2,12 @@ package quota
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"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 {
|
||||
QuotaReached(c *models.ReqContext, target string) (bool, error)
|
||||
CheckQuotaReached(ctx context.Context, target string, scopeParams *ScopeParameters) (bool, 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
|
||||
}
|
||||
DeleteByUser(context.Context, int64) error
|
||||
}
|
||||
|
200
pkg/services/quota/quotaimpl/quota.go
Normal file
200
pkg/services/quota/quotaimpl/quota.go
Normal 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 = "a.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)
|
||||
}
|
28
pkg/services/quota/quotaimpl/quota_test.go
Normal file
28
pkg/services/quota/quotaimpl/quota_test.go
Normal 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
|
||||
}
|
24
pkg/services/quota/quotaimpl/store.go
Normal file
24
pkg/services/quota/quotaimpl/store.go
Normal 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
|
||||
})
|
||||
}
|
25
pkg/services/quota/quotaimpl/store_test.go
Normal file
25
pkg/services/quota/quotaimpl/store_test.go
Normal 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)
|
||||
})
|
||||
}
|
29
pkg/services/quota/quotatest/fake.go
Normal file
29
pkg/services/quota/quotatest/fake.go
Normal 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
|
||||
}
|
@ -5,8 +5,9 @@ import (
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Add(ctx context.Context, cmd *StarDashboardCommand) error
|
||||
Delete(ctx context.Context, cmd *UnstarDashboardCommand) error
|
||||
IsStarredByUser(ctx context.Context, query *IsStarredByUserQuery) (bool, error)
|
||||
GetByUser(ctx context.Context, cmd *GetUserStarsQuery) (*GetUserStarsResult, error)
|
||||
Add(context.Context, *StarDashboardCommand) error
|
||||
Delete(context.Context, *UnstarDashboardCommand) error
|
||||
DeleteByUser(context.Context, int64) error
|
||||
IsStarredByUser(context.Context, *IsStarredByUserQuery) (bool, error)
|
||||
GetByUser(context.Context, *GetUserStarsQuery) (*GetUserStarsResult, error)
|
||||
}
|
||||
|
@ -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) {
|
||||
return s.store.List(ctx, cmd)
|
||||
}
|
||||
|
||||
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
|
||||
return s.store.DeleteByUser(ctx, userID)
|
||||
}
|
||||
|
@ -9,10 +9,11 @@ import (
|
||||
)
|
||||
|
||||
type store interface {
|
||||
Get(ctx context.Context, query *star.IsStarredByUserQuery) (bool, error)
|
||||
Insert(ctx context.Context, cmd *star.StarDashboardCommand) error
|
||||
Delete(ctx context.Context, cmd *star.UnstarDashboardCommand) error
|
||||
List(ctx context.Context, query *star.GetUserStarsQuery) (*star.GetUserStarsResult, error)
|
||||
Get(context.Context, *star.IsStarredByUserQuery) (bool, error)
|
||||
Insert(context.Context, *star.StarDashboardCommand) error
|
||||
Delete(context.Context, *star.UnstarDashboardCommand) error
|
||||
DeleteByUser(context.Context, int64) error
|
||||
List(context.Context, *star.GetUserStarsQuery) (*star.GetUserStarsResult, error)
|
||||
}
|
||||
|
||||
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) {
|
||||
userStars := make(map[int64]bool)
|
||||
err := s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
|
@ -56,5 +56,10 @@ func TestIntegrationUserStarsDataAccess(t *testing.T) {
|
||||
require.False(t, isStarred)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("delete by user", func(t *testing.T) {
|
||||
err := starStore.DeleteByUser(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -28,6 +28,10 @@ func (f *FakeStarService) Delete(ctx context.Context, cmd *star.UnstarDashboardC
|
||||
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) {
|
||||
return f.ExpectedUserStars, f.ExpectedError
|
||||
}
|
||||
|
@ -22,3 +22,11 @@ func (t *TeamGuardianStoreImpl) GetTeamMembers(ctx context.Context, query models
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
|
@ -15,3 +15,8 @@ func (t *TeamGuardianStoreMock) GetTeamMembers(ctx context.Context, query models
|
||||
args := t.Called(ctx, query)
|
||||
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)
|
||||
}
|
||||
|
@ -44,3 +44,7 @@ func (s *Service) CanAdmin(ctx context.Context, orgId int64, teamId int64, user
|
||||
|
||||
return models.ErrNotAllowedToUpdateTeam
|
||||
}
|
||||
|
||||
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
|
||||
return s.store.DeleteByUser(ctx, userID)
|
||||
}
|
||||
|
@ -15,3 +15,8 @@ func (t *TeamGuardianMock) CanAdmin(ctx context.Context, orgId int64, teamId int
|
||||
args := t.Called(ctx, orgId, teamId, user)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
||||
func (t *TeamGuardianMock) DeleteByUser(context.Context, int64) error {
|
||||
args := t.Called(context.Background(), 0)
|
||||
return args.Error(0)
|
||||
}
|
||||
|
@ -7,9 +7,11 @@ import (
|
||||
)
|
||||
|
||||
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 {
|
||||
GetTeamMembers(ctx context.Context, query models.GetTeamMembersQuery) ([]*models.TeamMemberDTO, error)
|
||||
GetTeamMembers(context.Context, models.GetTeamMembersQuery) ([]*models.TeamMemberDTO, error)
|
||||
DeleteByUser(context.Context, int64) error
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*use
|
||||
orgUser.Role = org.RoleType(setting.AutoAssignOrgRole)
|
||||
}
|
||||
}
|
||||
_, err = s.orgService.InsertUser(ctx, &orgUser)
|
||||
_, err = s.orgService.InsertOrgUser(ctx, &orgUser)
|
||||
if err != nil {
|
||||
// HERE ADD DELETE USER
|
||||
return usr, err
|
||||
|
8
pkg/services/userauth/userauth.go
Normal file
8
pkg/services/userauth/userauth.go
Normal 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
|
||||
}
|
33
pkg/services/userauth/userauthimpl/store.go
Normal file
33
pkg/services/userauth/userauthimpl/store.go
Normal 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
|
||||
})
|
||||
}
|
30
pkg/services/userauth/userauthimpl/store_test.go
Normal file
30
pkg/services/userauth/userauthimpl/store_test.go
Normal 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)
|
||||
})
|
||||
}
|
28
pkg/services/userauth/userauthimpl/userauth.go
Normal file
28
pkg/services/userauth/userauthimpl/userauth.go
Normal 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)
|
||||
}
|
37
pkg/services/userauth/userauthimpl/userauth_test.go
Normal file
37
pkg/services/userauth/userauthimpl/userauth_test.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user