mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 19:22:57 +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/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: "a.QuotaService{Cfg: cfg},
|
QuotaService: "aimpl.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: "a.QuotaService{Cfg: cfg},
|
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||||
RouteRegister: routeRegister,
|
RouteRegister: routeRegister,
|
||||||
SQLStore: store,
|
SQLStore: store,
|
||||||
License: &licensing.OSSLicensingService{},
|
License: &licensing.OSSLicensingService{},
|
||||||
|
@ -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: "a.QuotaService{
|
QuotaService: "aimpl.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: "a.QuotaService{Cfg: cfg},
|
QuotaService: "aimpl.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: "a.QuotaService{Cfg: cfg},
|
QuotaService: "aimpl.Service{Cfg: cfg},
|
||||||
LibraryPanelService: &mockLibraryPanelService{},
|
LibraryPanelService: &mockLibraryPanelService{},
|
||||||
LibraryElementService: &mockLibraryElementService{},
|
LibraryElementService: &mockLibraryElementService{},
|
||||||
DashboardService: mock,
|
DashboardService: mock,
|
||||||
|
@ -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) {
|
||||||
|
@ -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")
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)),
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
// })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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{}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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: "a.QuotaService{},
|
QuotaService: "aimpl.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: "a.QuotaService{},
|
QuotaService: "aimpl.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: "a.QuotaService{},
|
QuotaService: "aimpl.Service{},
|
||||||
AuthInfoService: authInfoMock,
|
AuthInfoService: authInfoMock,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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", "a.ScopeParameters{
|
limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", "a.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)
|
||||||
|
@ -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
|
||||||
|
@ -88,8 +88,8 @@ func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule model
|
|||||||
}
|
}
|
||||||
|
|
||||||
limitReached, err := service.quotas.CheckQuotaReached(ctx, "alert_rule", "a.ScopeParameters{
|
limitReached, err := service.quotas.CheckQuotaReached(ctx, "alert_rule", "a.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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
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 (
|
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
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