Chore: Refactor quota service (#57586)

* Chore: refactore quota service

* Apply suggestions from code review
This commit is contained in:
Sofia Papagiannaki 2022-11-08 10:25:34 +02:00 committed by GitHub
parent faa0fda6eb
commit 326ea86a57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2595 additions and 1397 deletions

View File

@ -245,7 +245,7 @@ func (hs *HTTPServer) AdminDeleteUser(c *models.ReqContext) response.Response {
return nil
})
g.Go(func() error {
if err := hs.QuotaService.DeleteByUser(ctx, cmd.UserID); err != nil {
if err := hs.QuotaService.DeleteQuotaForUser(ctx, cmd.UserID); err != nil {
return err
}
return nil

View File

@ -36,12 +36,16 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
)
@ -69,8 +73,8 @@ func (hs *HTTPServer) registerRoutes() {
// not logged in views
r.Get("/logout", hs.Logout)
r.Post("/login", quota("session"), routing.Wrap(hs.LoginPost))
r.Get("/login/:name", quota("session"), hs.OAuthLogin)
r.Post("/login", quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginPost))
r.Get("/login/:name", quota(string(auth.QuotaTargetSrv)), hs.OAuthLogin)
r.Get("/login", hs.LoginView)
r.Get("/invite/:code", hs.Index)
@ -173,7 +177,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/verify", hs.Index)
r.Get("/signup", hs.Index)
r.Get("/api/user/signup/options", routing.Wrap(GetSignUpOptions))
r.Post("/api/user/signup", quota("user"), routing.Wrap(hs.SignUp))
r.Post("/api/user/signup", quota(user.QuotaTargetSrv), quota(org.QuotaTargetSrv), routing.Wrap(hs.SignUp))
r.Post("/api/user/signup/step2", routing.Wrap(hs.SignUpStep2))
// invited
@ -192,7 +196,7 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
// api renew session based on cookie
r.Get("/api/login/ping", quota("session"), routing.Wrap(hs.LoginAPIPing))
r.Get("/api/login/ping", quota(string(auth.QuotaTargetSrv)), routing.Wrap(hs.LoginAPIPing))
// expose plugin file system assets
r.Get("/public/plugins/:pluginId/*", hs.getPluginAssets)
@ -298,13 +302,13 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Put("/address", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgsWrite)), routing.Wrap(hs.UpdateCurrentOrgAddress))
orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrg))
orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsersWithPaging))
orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), routing.Wrap(hs.AddOrgUserToCurrentOrg))
orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota(user.QuotaTargetSrv), quota(org.QuotaTargetSrv), routing.Wrap(hs.AddOrgUserToCurrentOrg))
orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersWrite, userIDScope)), routing.Wrap(hs.UpdateOrgUserForCurrentOrg))
orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg))
// invites
orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.GetPendingOrgInvites))
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), quota("user"), routing.Wrap(hs.AddOrgInvite))
orgRoute.Post("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), quota(user.QuotaTargetSrv), quota(user.QuotaTargetSrv), routing.Wrap(hs.AddOrgInvite))
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd)), routing.Wrap(hs.RevokeInvite))
// prefs
@ -331,7 +335,7 @@ func (hs *HTTPServer) registerRoutes() {
})
// create new org
apiRoute.Post("/orgs", authorizeInOrg(reqSignedIn, ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsCreate)), quota("org"), routing.Wrap(hs.CreateOrg))
apiRoute.Post("/orgs", authorizeInOrg(reqSignedIn, ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsCreate)), quota(org.QuotaTargetSrv), routing.Wrap(hs.CreateOrg))
// search all orgs
apiRoute.Get("/orgs", authorizeInOrg(reqGrafanaAdmin, ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.SearchOrgs))
@ -358,7 +362,7 @@ func (hs *HTTPServer) registerRoutes() {
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
apikeyIDScope := ac.Scope("apikeys", "id", ac.Parameter(":id"))
keysRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyRead)), routing.Wrap(hs.GetAPIKeys))
keysRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyCreate)), quota("api_key"), routing.Wrap(hs.AddAPIKey))
keysRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyCreate)), quota(string(apikey.QuotaTargetSrv)), routing.Wrap(hs.AddAPIKey))
keysRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionAPIKeyDelete, apikeyIDScope)), routing.Wrap(hs.DeleteAPIKey))
})
@ -373,7 +377,7 @@ func (hs *HTTPServer) registerRoutes() {
uidScope := datasources.ScopeProvider.GetResourceScopeUID(ac.Parameter(":uid"))
nameScope := datasources.ScopeProvider.GetResourceScopeName(ac.Parameter(":name"))
datasourceRoute.Get("/", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionRead)), routing.Wrap(hs.GetDataSources))
datasourceRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionCreate)), quota("data_source"), routing.Wrap(hs.AddDataSource))
datasourceRoute.Post("/", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionCreate)), quota(string(datasources.QuotaTargetSrv)), routing.Wrap(hs.AddDataSource))
datasourceRoute.Put("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, idScope)), routing.Wrap(hs.UpdateDataSourceByID))
datasourceRoute.Put("/uid/:uid", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionWrite, uidScope)), routing.Wrap(hs.UpdateDataSourceByUID))
datasourceRoute.Delete("/:id", authorize(reqOrgAdmin, ac.EvalPermission(datasources.ActionDelete, idScope)), routing.Wrap(hs.DeleteDataSourceById))

View File

@ -45,7 +45,6 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/preference/preftest"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/rendering"
"github.com/grafana/grafana/pkg/services/search"
@ -249,15 +248,13 @@ func (s *fakeRenderService) Init() error {
}
func setupAccessControlScenarioContext(t *testing.T, cfg *setting.Cfg, url string, permissions []accesscontrol.Permission) (*scenarioContext, *HTTPServer) {
cfg.Quota.Enabled = false
store := db.InitTestDB(t)
store := sqlstore.InitTestDB(t)
hs := &HTTPServer{
Cfg: cfg,
Live: newTestLive(t, store),
License: &licensing.OSSLicensingService{},
Features: featuremgmt.WithFeatures(),
QuotaService: &quotaimpl.Service{Cfg: cfg},
QuotaService: quotatest.New(false, nil),
RouteRegister: routing.NewRouteRegister(),
AccessControl: accesscontrolmock.New().WithPermissions(permissions),
searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()),
@ -376,7 +373,9 @@ func setupHTTPServerWithCfgDb(
routeRegister := routing.NewRouteRegister()
teamService := teamimpl.ProvideService(db, cfg)
cfg.IsFeatureToggleEnabled = features.IsEnabled
dashboardsStore := dashboardsstore.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db, cfg))
quotaService := quotatest.New(false, nil)
dashboardsStore, err := dashboardsstore.ProvideDashboardStore(db, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db, cfg), quotaService)
require.NoError(t, err)
var acmock *accesscontrolmock.Mock
var ac accesscontrol.AccessControl
@ -402,7 +401,8 @@ func setupHTTPServerWithCfgDb(
acService, err = acimpl.ProvideService(cfg, db, routeRegister, localcache.ProvideService(), featuremgmt.WithFeatures())
require.NoError(t, err)
ac = acimpl.ProvideAccessControl(cfg)
userSvc = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService())
userSvc, err = userimpl.ProvideService(db, nil, cfg, teamimpl.ProvideService(db, cfg), localcache.ProvideService(), quotatest.New(false, nil))
require.NoError(t, err)
}
teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license, acService, teamService, userSvc)
require.NoError(t, err)
@ -412,7 +412,7 @@ func setupHTTPServerWithCfgDb(
Cfg: cfg,
Features: features,
Live: newTestLive(t, db),
QuotaService: &quotaimpl.Service{Cfg: cfg},
QuotaService: quotaService,
RouteRegister: routeRegister,
SQLStore: store,
License: &licensing.OSSLicensingService{},
@ -497,7 +497,7 @@ func SetupAPITestServer(t *testing.T, opts ...APITestServerOption) *webtest.Serv
RouteRegister: routing.NewRouteRegister(),
License: &licensing.OSSLicensingService{},
Features: featuremgmt.WithFeatures(),
QuotaService: quotatest.NewQuotaServiceFake(),
QuotaService: quotatest.New(false, nil),
searchUsersService: &searchusers.OSSService{},
}

View File

@ -407,7 +407,7 @@ func (hs *HTTPServer) postDashboard(c *models.ReqContext, cmd models.SaveDashboa
dash := cmd.GetDashboardModel()
newDashboard := dash.Id == 0
if newDashboard {
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
limitReached, err := hs.QuotaService.QuotaReached(c, dashboards.QuotaTargetSrv)
if err != nil {
return response.Error(500, "failed to get quota", err)
}

View File

@ -39,7 +39,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/quotaimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
@ -150,6 +150,7 @@ func TestDashboardAPIEndpoint(t *testing.T) {
DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService,
Coremodels: registry.NewBase(nil),
QuotaService: quotatest.New(false, nil),
}
setUp := func() {
@ -990,9 +991,12 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
provisioningService = provisioning.NewProvisioningServiceMock(context.Background())
}
var err error
if dashboardStore == nil {
sql := db.InitTestDB(t)
dashboardStore = database.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err = database.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService)
require.NoError(t, err)
}
libraryPanelsService := mockLibraryPanelService{}
@ -1031,7 +1035,7 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
require.Equal(sc.t, 200, sc.resp.Code)
dash := dtos.DashboardFullWithMeta{}
err := json.NewDecoder(sc.resp.Body).Decode(&dash)
err = json.NewDecoder(sc.resp.Body).Decode(&dash)
require.NoError(sc.t, err)
return dash
@ -1077,12 +1081,10 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) {
cfg := setting.NewCfg()
hs := HTTPServer{
Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, db.InitTestDB(t)),
QuotaService: &quotaimpl.Service{
Cfg: cfg,
},
Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, db.InitTestDB(t)),
QuotaService: quotatest.New(false, nil),
pluginStore: &plugins.FakePluginStore{},
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
@ -1116,7 +1118,7 @@ func postValidateScenario(t *testing.T, desc string, url string, routePattern st
Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, db.InitTestDB(t)),
QuotaService: &quotaimpl.Service{Cfg: cfg},
QuotaService: quotatest.New(false, nil),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: sqlmock,
@ -1152,7 +1154,7 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, db.InitTestDB(t)),
QuotaService: &quotaimpl.Service{Cfg: cfg},
QuotaService: quotatest.New(false, nil),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
SQLStore: sqlmock,
@ -1190,7 +1192,7 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
Cfg: cfg,
ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()),
Live: newTestLive(t, db.InitTestDB(t)),
QuotaService: &quotaimpl.Service{Cfg: cfg},
QuotaService: quotatest.New(false, nil),
LibraryPanelService: &mockLibraryPanelService{},
LibraryElementService: &mockLibraryElementService{},
DashboardService: mock,

View File

@ -146,7 +146,7 @@ func TestHTTPServer_FolderMetadata(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.folderService = folderService
hs.AccessControl = acmock.New()
hs.QuotaService = quotatest.NewQuotaServiceFake()
hs.QuotaService = quotatest.New(false, nil)
})
t.Run("Should attach access control metadata to multiple folders", func(t *testing.T) {

View File

@ -94,12 +94,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()
hs.QuotaService = quotatest.New(false, nil)
})
serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds
hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, false)
hs.QuotaService = quotatest.NewQuotaServiceFake()
hs.QuotaService = quotatest.New(false, nil)
})
t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) {
@ -142,7 +142,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
)
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds
hs.QuotaService = quotatest.NewQuotaServiceFake()
hs.QuotaService = quotatest.New(false, nil)
})
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) {
@ -294,7 +294,7 @@ func TestDataSourceQueryError(t *testing.T) {
pluginClient.ProvideService(r, &config.Cfg{}),
&fakeOAuthTokenService{},
)
hs.QuotaService = quotatest.NewQuotaServiceFake()
hs.QuotaService = quotatest.New(false, nil)
})
req := srv.NewPostRequest("/api/ds/query", strings.NewReader(tc.request))
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/grafana/grafana/pkg/setting"
@ -104,7 +105,8 @@ func TestAPIEndpoint_PutCurrentOrg_LegacyAccessControl(t *testing.T) {
})
setInitCtxSignedInOrgAdmin(sc.initCtx)
sc.hs.orgService = orgimpl.ProvideService(sc.db, sc.cfg)
sc.hs.orgService, err = orgimpl.ProvideService(sc.db, sc.cfg, quotatest.New(false, nil))
require.NoError(t, err)
t.Run("Admin can update current org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
@ -118,7 +120,8 @@ func TestAPIEndpoint_PutCurrentOrg_AccessControl(t *testing.T) {
_, err := sc.db.CreateOrgWithMember("TestOrg", sc.initCtx.UserID)
require.NoError(t, err)
sc.hs.orgService = orgimpl.ProvideService(sc.db, sc.cfg)
sc.hs.orgService, err = orgimpl.ProvideService(sc.db, sc.cfg, quotatest.New(false, nil))
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating current org with correct permissions", func(t *testing.T) {
@ -436,7 +439,9 @@ func TestAPIEndpoint_PutOrg_LegacyAccessControl(t *testing.T) {
cfg.RBACEnabled = false
sc := setupHTTPServerWithCfg(t, true, cfg)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.orgService = orgimpl.ProvideService(sc.db, sc.cfg)
var err error
sc.hs.orgService, err = orgimpl.ProvideService(sc.db, sc.cfg, quotatest.New(false, nil))
require.NoError(t, err)
// Create two orgs, to update another one than the logged in one
setupOrgsDBForAccessControlTests(t, sc.db, sc, 2)
@ -456,7 +461,9 @@ func TestAPIEndpoint_PutOrg_LegacyAccessControl(t *testing.T) {
func TestAPIEndpoint_PutOrg_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
sc.hs.orgService = orgimpl.ProvideService(sc.db, sc.cfg)
var err error
sc.hs.orgService, err = orgimpl.ProvideService(sc.db, sc.cfg, quotatest.New(false, nil))
require.NoError(t, err)
// Create two orgs, to update another one than the logged in one
setupOrgsDBForAccessControlTests(t, sc.db, sc, 2)

View File

@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
@ -389,11 +390,13 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tc.enableAccessControl
var err error
sc := setupHTTPServerWithCfg(t, false, cfg, func(hs *HTTPServer) {
hs.userService = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(),
)
hs.orgService = orgimpl.ProvideService(hs.SQLStore, cfg)
hs.userService, err = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(), quotatest.New(false, nil))
require.NoError(t, err)
hs.orgService, err = orgimpl.ProvideService(hs.SQLStore, cfg, quotatest.New(false, nil))
require.NoError(t, err)
})
setupOrgUsersDBForAccessControlTests(t, sc.db)
setInitCtxSignedInUser(sc.initCtx, tc.user)
@ -403,7 +406,7 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) {
require.Equal(t, tc.expectedCode, response.Code)
var userList []*models.OrgUserDTO
err := json.NewDecoder(response.Body).Decode(&userList)
err = json.NewDecoder(response.Body).Decode(&userList)
require.NoError(t, err)
if tc.expectedMetadata != nil {
@ -493,11 +496,14 @@ func TestGetOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tc.enableAccessControl
var err error
sc := setupHTTPServerWithCfg(t, false, cfg, func(hs *HTTPServer) {
hs.userService = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(),
)
hs.orgService = orgimpl.ProvideService(hs.SQLStore, cfg)
quotaService := quotatest.New(false, nil)
hs.userService, err = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(), quotaService)
require.NoError(t, err)
hs.orgService, err = orgimpl.ProvideService(hs.SQLStore, cfg, quotaService)
require.NoError(t, err)
})
setInitCtxSignedInUser(sc.initCtx, tc.user)
setupOrgUsersDBForAccessControlTests(t, sc.db)
@ -598,10 +604,11 @@ func TestPostOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tc.enableAccessControl
var err error
sc := setupHTTPServerWithCfg(t, false, cfg, func(hs *HTTPServer) {
hs.userService = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(),
)
hs.userService, err = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(), quotatest.New(false, nil))
require.NoError(t, err)
})
setupOrgUsersDBForAccessControlTests(t, sc.db)
@ -716,11 +723,12 @@ func TestOrgUsersAPIEndpointWithSetPerms_AccessControl(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
var err error
sc := setupHTTPServer(t, true, func(hs *HTTPServer) {
hs.tempUserService = tempuserimpl.ProvideService(hs.SQLStore)
hs.userService = userimpl.ProvideService(
hs.SQLStore, nil, setting.NewCfg(), teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), setting.NewCfg()), localcache.ProvideService(),
)
hs.userService, err = userimpl.ProvideService(
hs.SQLStore, nil, setting.NewCfg(), teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), setting.NewCfg()), localcache.ProvideService(), quotatest.New(false, nil))
require.NoError(t, err)
})
setInitCtxSignedInViewer(sc.initCtx)
setupOrgUsersDBForAccessControlTests(t, sc.db)
@ -835,11 +843,14 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tc.enableAccessControl
var err error
sc := setupHTTPServerWithCfg(t, false, cfg, func(hs *HTTPServer) {
hs.userService = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(),
)
hs.orgService = orgimpl.ProvideService(hs.SQLStore, cfg)
quotaService := quotatest.New(false, nil)
hs.userService, err = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(), quotaService)
require.NoError(t, err)
hs.orgService, err = orgimpl.ProvideService(hs.SQLStore, cfg, quotaService)
require.NoError(t, err)
})
setupOrgUsersDBForAccessControlTests(t, sc.db)
setInitCtxSignedInUser(sc.initCtx, tc.user)
@ -962,11 +973,14 @@ func TestDeleteOrgUsersAPIEndpoint_AccessControl(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = tc.enableAccessControl
var err error
sc := setupHTTPServerWithCfg(t, false, cfg, func(hs *HTTPServer) {
hs.userService = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(),
)
hs.orgService = orgimpl.ProvideService(hs.SQLStore, cfg)
quotaService := quotatest.New(false, nil)
hs.userService, err = userimpl.ProvideService(
hs.SQLStore, nil, cfg, teamimpl.ProvideService(hs.SQLStore.(*sqlstore.SQLStore), cfg), localcache.ProvideService(), quotaService)
require.NoError(t, err)
hs.orgService, err = orgimpl.ProvideService(hs.SQLStore, cfg, quotaService)
require.NoError(t, err)
})
setupOrgUsersDBForAccessControlTests(t, sc.db)
setInitCtxSignedInUser(sc.initCtx, tc.user)

View File

@ -41,7 +41,7 @@ func TestGetPluginDashboards(t *testing.T) {
s := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.pluginDashboardService = pluginDashboardService
hs.QuotaService = quotatest.NewQuotaServiceFake()
hs.QuotaService = quotatest.New(false, nil)
})
t.Run("Not signed in should return 404 Not Found", func(t *testing.T) {

View File

@ -32,6 +32,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
@ -138,7 +139,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider,
&oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -151,7 +154,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic url", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/common/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[3]
@ -163,7 +168,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path with no url", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[4]
@ -174,7 +181,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("When matching route path and has dynamic body", func(t *testing.T) {
ctx, req := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/body", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
proxy.matchedRoute = routes[5]
@ -188,7 +197,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("Validating request", func(t *testing.T) {
t.Run("plugin route with valid role", func(t *testing.T) {
ctx, _ := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/v4/some/method", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
err = proxy.validateRequest()
@ -197,7 +208,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is editor", func(t *testing.T) {
ctx, _ := setUp()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
err = proxy.validateRequest()
@ -207,7 +220,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
t.Run("plugin route with admin role and user is admin", func(t *testing.T) {
ctx, _ := setUp()
ctx.SignedInUser.OrgRole = org.RoleAdmin
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "api/admin", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
err = proxy.validateRequest()
@ -298,7 +313,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
},
}
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@ -314,7 +331,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
req, err := http.NewRequest("GET", "http://localhost/asd", nil)
require.NoError(t, err)
client = newFakeHTTPClient(t, json2)
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken2", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[1], dsInfo, cfg)
@ -331,7 +350,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
require.NoError(t, err)
client = newFakeHTTPClient(t, []byte{})
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "pathwithtoken1", cfg, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
ApplyRoute(proxy.ctx.Req.Context(), req, proxy.proxyPath, routes[0], dsInfo, cfg)
@ -355,7 +376,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{BuildVersion: "5.3.0"}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -382,7 +405,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -408,7 +433,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -438,7 +465,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, pluginRoutes, ctx, "", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -463,7 +492,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -514,7 +545,9 @@ func TestDataSourceProxy_routeRule(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/to/folder/", &setting.Cfg{}, httpClientProvider, &mockAuthToken, dsService, tracer)
require.NoError(t, err)
req, err = http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -651,7 +684,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -671,7 +706,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -687,7 +724,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -711,7 +750,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/render", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -738,7 +779,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -764,7 +807,9 @@ func TestDataSourceProxy_requestHandling(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "/path/%2Ftest%2Ftest%2F", &setting.Cfg{}, httpClientProvider, &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -790,8 +835,11 @@ func TestNewDataSourceProxy_InvalidURL(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
_, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
quotaService := quotatest.New(false, nil)
var err error
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.Error(t, err)
assert.True(t, strings.HasPrefix(err.Error(), `validation of data source URL "://host/root" failed`))
}
@ -812,8 +860,10 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
_, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
_, err = NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
}
@ -856,7 +906,9 @@ func TestNewDataSourceProxy_MSSQL(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
p, err := NewDataSourceProxy(&ds, routes, &ctx, "api/method", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
if tc.err == nil {
require.NoError(t, err)
@ -884,7 +936,9 @@ func getDatasourceProxiedRequest(t *testing.T, ctx *models.ReqContext, cfg *sett
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(ds, routes, ctx, "", cfg, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil)
@ -1001,7 +1055,9 @@ func runDatasourceAuthTest(t *testing.T, secretsService secrets.Service, secrets
tracer := tracing.InitializeTracerForTest()
var routes []*plugins.Route
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(test.datasource, routes, ctx, "", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)
@ -1045,7 +1101,9 @@ func Test_PathCheck(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
proxy, err := NewDataSourceProxy(&datasources.DataSource{}, routes, ctx, "b", &setting.Cfg{}, httpclient.NewProvider(), &oauthtoken.Service{}, dsService, tracer)
require.NoError(t, err)

View File

@ -60,7 +60,7 @@ func Test_PluginsInstallAndUninstall(t *testing.T) {
PluginAdminExternalManageEnabled: tc.pluginAdminExternalManageEnabled,
}
hs.pluginInstaller = inst
hs.QuotaService = quotatest.NewQuotaServiceFake()
hs.QuotaService = quotatest.New(false, nil)
})
t.Run(testName("Install", tc), func(t *testing.T) {

View File

@ -6,10 +6,22 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/web"
)
// swagger:route GET /org/quotas getCurrentOrg getCurrentOrgQuota
//
// Fetch Organization quota.
//
// If you are running Grafana Enterprise and have Fine-grained access control enabled, you need to have a permission with action `orgs.quotas:read` and scope `org:id:1` (orgIDScope).
//
// Responses:
// 200: getQuotaResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Response {
return hs.getOrgQuotasHelper(c, c.OrgID)
}
@ -29,22 +41,17 @@ func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Respons
func (hs *HTTPServer) GetOrgQuotas(c *models.ReqContext) response.Response {
orgId, err := strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
return response.Err(quota.ErrBadRequest.Errorf("orgId is invalid: %w", err))
}
return hs.getOrgQuotasHelper(c, orgId)
}
func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) response.Response {
if !hs.Cfg.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
q, err := hs.QuotaService.GetQuotasByScope(c.Req.Context(), quota.OrgScope, orgID)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to get quota", err)
}
query := models.GetOrgQuotasQuery{OrgId: orgID}
if err := hs.SQLStore.GetOrgQuotas(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get org quotas", err)
}
return response.JSON(http.StatusOK, query.Result)
return response.JSON(http.StatusOK, q)
}
// swagger:route PUT /orgs/{org_id}/quotas/{quota_target} orgs updateOrgQuota
@ -63,26 +70,19 @@ func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) resp
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response {
cmd := models.UpdateOrgQuotaCmd{}
cmd := quota.UpdateQuotaCmd{}
var err error
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
return response.Err(quota.ErrBadRequest.Errorf("bad request data: %w", err))
}
if !hs.Cfg.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
}
cmd.OrgId, err = strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
cmd.OrgID, err = strconv.ParseInt(web.Params(c.Req)[":orgId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "orgId is invalid", err)
return response.Err(quota.ErrBadRequest.Errorf("orgId is invalid: %w", err))
}
cmd.Target = web.Params(c.Req)[":target"]
if _, ok := hs.Cfg.Quota.Org.ToMap()[cmd.Target]; !ok {
return response.Error(404, "Invalid quota target", nil)
}
if err := hs.SQLStore.UpdateOrgQuota(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update org quotas", err)
if err := hs.QuotaService.Update(c.Req.Context(), &cmd); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update org quotas", err)
}
return response.Success("Organization quota updated")
}
@ -114,22 +114,17 @@ func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext) response.Response {
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response {
if !setting.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
}
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
return response.Err(quota.ErrBadRequest.Errorf("id is invalid: %w", err))
}
query := models.GetUserQuotasQuery{UserId: id}
if err := hs.SQLStore.GetUserQuotas(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get org quotas", err)
q, err := hs.QuotaService.GetQuotasByScope(c.Req.Context(), quota.UserScope, id)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to get org quotas", err)
}
return response.JSON(http.StatusOK, query.Result)
return response.JSON(http.StatusOK, q)
}
// swagger:route PUT /admin/users/{user_id}/quotas/{quota_target} admin_users updateUserQuota
@ -148,26 +143,19 @@ func (hs *HTTPServer) GetUserQuotas(c *models.ReqContext) response.Response {
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) UpdateUserQuota(c *models.ReqContext) response.Response {
cmd := models.UpdateUserQuotaCmd{}
cmd := quota.UpdateQuotaCmd{}
var err error
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
return response.Err(quota.ErrBadRequest.Errorf("bad request data: %w", err))
}
if !setting.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
}
cmd.UserId, err = strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
cmd.UserID, err = strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
return response.Err(quota.ErrBadRequest.Errorf("id is invalid: %w", err))
}
cmd.Target = web.Params(c.Req)[":target"]
if _, ok := setting.Quota.User.ToMap()[cmd.Target]; !ok {
return response.Error(404, "Invalid quota target", nil)
}
if err := hs.SQLStore.UpdateUserQuota(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update org quotas", err)
if err := hs.QuotaService.Update(c.Req.Context(), &cmd); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update org quotas", err)
}
return response.Success("Organization quota updated")
}
@ -176,7 +164,7 @@ func (hs *HTTPServer) UpdateUserQuota(c *models.ReqContext) response.Response {
type UpdateUserQuotaParams struct {
// in:body
// required:true
Body models.UpdateUserQuotaCmd `json:"body"`
Body quota.UpdateQuotaCmd `json:"body"`
// in:path
// required:true
QuotaTarget string `json:"quota_target"`
@ -203,7 +191,7 @@ type GetOrgQuotaParams struct {
type UpdateOrgQuotaParam struct {
// in:body
// required:true
Body models.UpdateOrgQuotaCmd `json:"body"`
Body quota.UpdateQuotaCmd `json:"body"`
// in:path
// required:true
QuotaTarget string `json:"quota_target"`
@ -215,5 +203,5 @@ type UpdateOrgQuotaParam struct {
// swagger:response getQuotaResponse
type GetQuotaResponseResponse struct {
// in:body
Body []*models.UserQuotaDTO `json:"body"`
Body []*quota.QuotaDTO `json:"body"`
}

View File

@ -32,17 +32,13 @@ var testOrgQuota = setting.OrgQuota{
func setupDBAndSettingsForAccessControlQuotaTests(t *testing.T, sc accessControlScenarioContext) {
t.Helper()
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Required while sqlstore quota.go relies on setting global variables
setting.Quota = sc.hs.Cfg.Quota
// Create two orgs with the context user
setupOrgsDBForAccessControlTests(t, sc.db, sc, 2)
}
func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) {
cfg := setting.NewCfg()
cfg.Quota.Enabled = true
cfg.RBACEnabled = false
sc := setupHTTPServerWithCfg(t, true, cfg)
setInitCtxSignedInViewer(sc.initCtx)
@ -62,7 +58,9 @@ func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) {
}
func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
cfg := setting.NewCfg()
cfg.Quota.Enabled = true
sc := setupHTTPServerWithCfg(t, true, cfg)
setInitCtxSignedInViewer(sc.initCtx)
setupDBAndSettingsForAccessControlQuotaTests(t, sc)
@ -86,6 +84,7 @@ func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) {
func TestAPIEndpoint_GetOrgQuotas_LegacyAccessControl(t *testing.T) {
cfg := setting.NewCfg()
cfg.Quota.Enabled = true
cfg.RBACEnabled = false
sc := setupHTTPServerWithCfg(t, true, cfg)
setInitCtxSignedInViewer(sc.initCtx)
@ -105,7 +104,9 @@ func TestAPIEndpoint_GetOrgQuotas_LegacyAccessControl(t *testing.T) {
}
func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
cfg := setting.NewCfg()
cfg.Quota.Enabled = true
sc := setupHTTPServerWithCfg(t, true, cfg)
setupDBAndSettingsForAccessControlQuotaTests(t, sc)
t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) {
@ -130,6 +131,7 @@ func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) {
func TestAPIEndpoint_PutOrgQuotas_LegacyAccessControl(t *testing.T) {
cfg := setting.NewCfg()
cfg.Quota.Enabled = true
cfg.RBACEnabled = false
sc := setupHTTPServerWithCfg(t, true, cfg)
setInitCtxSignedInViewer(sc.initCtx)
@ -151,7 +153,20 @@ func TestAPIEndpoint_PutOrgQuotas_LegacyAccessControl(t *testing.T) {
}
func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
cfg := setting.NewCfg()
cfg.Quota = setting.QuotaSettings{
Enabled: true,
Global: setting.GlobalQuota{
Org: 5,
},
Org: setting.OrgQuota{
User: 5,
},
User: setting.UserQuota{
Org: 5,
},
}
sc := setupHTTPServerWithCfg(t, true, cfg)
setupDBAndSettingsForAccessControlQuotaTests(t, sc)
input := strings.NewReader(testUpdateOrgQuotaCmd)

View File

@ -20,6 +20,7 @@ import (
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/login/authinfoservice"
authinfostore "github.com/grafana/grafana/pkg/services/login/authinfoservice/database"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/searchusers"
"github.com/grafana/grafana/pkg/services/searchusers/filters"
"github.com/grafana/grafana/pkg/services/secrets/database"
@ -68,7 +69,8 @@ func TestUserAPIEndpoint_userLoggedIn(t *testing.T) {
}
user, err := sqlStore.CreateUser(context.Background(), createUserCmd)
require.Nil(t, err)
hs.userService = userimpl.ProvideService(sqlStore, nil, sc.cfg, nil, nil)
hs.userService, err = userimpl.ProvideService(sqlStore, nil, sc.cfg, nil, nil, quotatest.New(false, nil))
require.NoError(t, err)
sc.handlerFunc = hs.GetUserByID

View File

@ -254,7 +254,7 @@ var wireSet = wire.NewSet(
wire.Bind(new(social.Service), new(*social.SocialService)),
oauthtoken.ProvideService,
auth.ProvideActiveAuthTokenService,
wire.Bind(new(models.ActiveTokenService), new(*auth.ActiveAuthTokenService)),
wire.Bind(new(auth.ActiveTokenService), new(*auth.ActiveAuthTokenService)),
wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)),
tempo.ProvideService,
loki.ProvideService,

View File

@ -14,15 +14,15 @@ func Quota(quotaService quota.Service) func(string) web.Handler {
panic("quotaService is nil")
}
//https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky
return func(target string) web.Handler {
return func(targetSrv string) web.Handler {
return func(c *models.ReqContext) {
limitReached, err := quotaService.QuotaReached(c, target)
limitReached, err := quotaService.QuotaReached(c, quota.TargetSrv(targetSrv))
if err != nil {
c.JsonApiErr(500, "Failed to get quota", err)
return
}
if limitReached {
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", targetSrv), nil)
return
}
}

View File

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
@ -30,8 +30,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Global.User = 4
})
middlewareScenario(t, "and global session quota not reached", func(t *testing.T, sc *scenarioContext) {
@ -41,8 +39,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Global.Session = 10
})
middlewareScenario(t, "and global session quota reached", func(t *testing.T, sc *scenarioContext) {
@ -52,13 +48,10 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Global.Session = 1
})
})
t.Run("with user logged in", func(t *testing.T) {
const quotaUsed = 4
setUp := func(sc *scenarioContext) {
sc.withTokenSessionCookie("token")
sc.userService.ExpectedSignedInUser = &user.SignedInUser{UserID: 12}
@ -79,8 +72,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Global.DataSource = quotaUsed
})
middlewareScenario(t, "user Org quota not reached", func(t *testing.T, sc *scenarioContext) {
@ -93,8 +84,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.User.Org = quotaUsed + 1
})
middlewareScenario(t, "user Org quota reached", func(t *testing.T, sc *scenarioContext) {
@ -106,8 +95,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.User.Org = quotaUsed
})
middlewareScenario(t, "org dashboard quota not reached", func(t *testing.T, sc *scenarioContext) {
@ -119,8 +106,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.Dashboard = quotaUsed + 1
})
middlewareScenario(t, "org dashboard quota reached", func(t *testing.T, sc *scenarioContext) {
@ -132,8 +117,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.Dashboard = quotaUsed
})
middlewareScenario(t, "org dashboard quota reached, but quotas disabled", func(t *testing.T, sc *scenarioContext) {
@ -145,9 +128,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.Dashboard = quotaUsed
cfg.Quota.Enabled = false
})
middlewareScenario(t, "org alert quota reached and unified alerting is enabled", func(t *testing.T, sc *scenarioContext) {
@ -162,7 +142,6 @@ func TestMiddlewareQuota(t *testing.T) {
cfg.UnifiedAlerting.Enabled = new(bool)
*cfg.UnifiedAlerting.Enabled = true
cfg.Quota.Org.AlertRule = quotaUsed
})
middlewareScenario(t, "org alert quota not reached and unified alerting is enabled", func(t *testing.T, sc *scenarioContext) {
@ -177,7 +156,6 @@ func TestMiddlewareQuota(t *testing.T) {
cfg.UnifiedAlerting.Enabled = new(bool)
*cfg.UnifiedAlerting.Enabled = true
cfg.Quota.Org.AlertRule = quotaUsed + 1
})
middlewareScenario(t, "org alert quota reached but ngalert disabled", func(t *testing.T, sc *scenarioContext) {
@ -190,8 +168,6 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.AlertRule = quotaUsed
})
middlewareScenario(t, "org alert quota not reached but ngalert disabled", func(t *testing.T, sc *scenarioContext) {
@ -203,58 +179,15 @@ func TestMiddlewareQuota(t *testing.T) {
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.AlertRule = quotaUsed + 1
})
})
}
func getQuotaHandler(reached bool, target string) web.Handler {
qs := &mockQuotaService{
reached: reached,
}
qs := quotatest.New(reached, nil)
return Quota(qs)(target)
}
func configure(cfg *setting.Cfg) {
cfg.AnonymousEnabled = false
cfg.Quota = setting.QuotaSettings{
Enabled: true,
Org: &setting.OrgQuota{
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
AlertRule: 5,
},
User: &setting.UserQuota{
Org: 5,
},
Global: &setting.GlobalQuota{
Org: 5,
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
Session: 5,
AlertRule: 5,
},
}
}
type mockQuotaService struct {
reached bool
err error
}
func (m *mockQuotaService) QuotaReached(c *models.ReqContext, target string) (bool, error) {
return m.reached, m.err
}
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
}

View File

@ -1,91 +0,0 @@
package models
import (
"errors"
"time"
)
var ErrInvalidQuotaTarget = errors.New("invalid quota target")
type Quota struct {
Id int64
OrgId int64
UserId int64
Target string
Limit int64
Created time.Time
Updated time.Time
}
type QuotaScope struct {
Name string
Target string
DefaultLimit int64
}
type OrgQuotaDTO struct {
OrgId int64 `json:"org_id"`
Target string `json:"target"`
Limit int64 `json:"limit"`
Used int64 `json:"used"`
}
type UserQuotaDTO struct {
UserId int64 `json:"user_id"`
Target string `json:"target"`
Limit int64 `json:"limit"`
Used int64 `json:"used"`
}
type GlobalQuotaDTO struct {
Target string `json:"target"`
Limit int64 `json:"limit"`
Used int64 `json:"used"`
}
type GetOrgQuotaByTargetQuery struct {
Target string
OrgId int64
Default int64
UnifiedAlertingEnabled bool
Result *OrgQuotaDTO
}
type GetOrgQuotasQuery struct {
OrgId int64
UnifiedAlertingEnabled bool
Result []*OrgQuotaDTO
}
type GetUserQuotaByTargetQuery struct {
Target string
UserId int64
Default int64
UnifiedAlertingEnabled bool
Result *UserQuotaDTO
}
type GetUserQuotasQuery struct {
UserId int64
UnifiedAlertingEnabled bool
Result []*UserQuotaDTO
}
type GetGlobalQuotaByTargetQuery struct {
Target string
Default int64
UnifiedAlertingEnabled bool
Result *GlobalQuotaDTO
}
type UpdateOrgQuotaCmd struct {
Target string `json:"target"`
Limit int64 `json:"limit"`
OrgId int64 `json:"-"`
}
type UpdateUserQuotaCmd struct {
Target string `json:"target"`
Limit int64 `json:"limit"`
UserId int64 `json:"-"`
}

View File

@ -76,10 +76,6 @@ type UserTokenService interface {
GetUserRevokedTokens(ctx context.Context, userId int64) ([]*UserToken, error)
}
type ActiveTokenService interface {
ActiveTokenCount(ctx context.Context) (int64, error)
}
type UserTokenBackgroundService interface {
registry.BackgroundService
}

View File

@ -272,7 +272,7 @@ var wireBasicSet = wire.NewSet(
wire.Bind(new(social.Service), new(*social.SocialService)),
oauthtoken.ProvideService,
auth.ProvideActiveAuthTokenService,
wire.Bind(new(models.ActiveTokenService), new(*auth.ActiveAuthTokenService)),
wire.Bind(new(auth.ActiveTokenService), new(*auth.ActiveAuthTokenService)),
wire.Bind(new(oauthtoken.OAuthTokenService), new(*oauthtoken.Service)),
tempo.ProvideService,
loki.ProvideService,

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
@ -225,7 +226,8 @@ func setupTestEnvironment(t *testing.T, permissions []accesscontrol.Permission,
sql := db.InitTestDB(t)
cfg := setting.NewCfg()
teamSvc := teamimpl.ProvideService(sql, cfg)
userSvc := userimpl.ProvideService(sql, nil, cfg, teamimpl.ProvideService(sql, cfg), nil)
userSvc, err := userimpl.ProvideService(sql, nil, cfg, teamimpl.ProvideService(sql, cfg), nil, quotatest.New(false, nil))
require.NoError(t, err)
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
mock := accesscontrolmock.New().WithPermissions(permissions)

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardstore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -56,7 +57,9 @@ func TestIntegrationAnnotations(t *testing.T) {
assert.NoError(t, err)
})
dashboardStore := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService)
require.NoError(t, err)
testDashboard1 := models.SaveDashboardCommand{
UserId: 1,
@ -453,7 +456,9 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) {
var maximumTagsLength int64 = 60
repo := xormRepositoryImpl{db: sql, cfg: setting.NewCfg(), log: log.New("annotation.test"), tagService: tagimpl.ProvideService(sql, sql.Cfg), maximumTagsLength: maximumTagsLength}
dashboardStore := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardstore.ProvideDashboardStore(sql, sql.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql, sql.Cfg), quotaService)
require.NoError(t, err)
testDashboard1 := models.SaveDashboardCommand{
UserId: 1,

View File

@ -6,6 +6,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
)
@ -13,16 +14,34 @@ type Service struct {
store store
}
func ProvideService(db db.DB, cfg *setting.Cfg) apikey.Service {
func ProvideService(db db.DB, cfg *setting.Cfg, quotaService quota.Service) (apikey.Service, error) {
s := &Service{}
if cfg.IsFeatureToggleEnabled(featuremgmt.FlagNewDBLibrary) {
return &Service{
store: &sqlxStore{
sess: db.GetSqlxSession(),
cfg: cfg,
},
s.store = &sqlxStore{
sess: db.GetSqlxSession(),
cfg: cfg,
}
}
return &Service{store: &sqlStore{db: db, cfg: cfg}}
s.store = &sqlStore{db: db, cfg: cfg}
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return s, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: apikey.QuotaTargetSrv,
DefaultLimits: defaultLimits,
Reporter: s.Usage,
}); err != nil {
return s, err
}
return s, nil
}
func (s *Service) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
return s.store.Count(ctx, scopeParams)
}
func (s *Service) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error {
@ -49,3 +68,24 @@ func (s *Service) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error {
func (s *Service) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error {
return s.store.UpdateAPIKeyLastUsedDate(ctx, tokenID)
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, quota.GlobalScope)
if err != nil {
return limits, err
}
orgQuotaTag, err := quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, quota.OrgScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.ApiKey)
limits.Set(orgQuotaTag, cfg.Quota.Org.ApiKey)
return limits, nil
}

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore/session"
"github.com/grafana/grafana/pkg/setting"
)
@ -142,3 +143,35 @@ func (ss *sqlxStore) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64
_, err := ss.sess.Exec(ctx, `UPDATE api_key SET last_used_at=? WHERE id=?`, &now, tokenID)
return err
}
func (ss *sqlxStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
type result struct {
Count int64
}
r := result{}
if err := ss.sess.Get(ctx, &r, `SELECT COUNT(*) AS count FROM api_key`); err != nil {
return u, err
} else {
tag, err := quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, quota.GlobalScope)
if err != nil {
return nil, err
}
u.Set(tag, r.Count)
}
if scopeParams.OrgID != 0 {
if err := ss.sess.Get(ctx, &r, `SELECT COUNT(*) AS count FROM api_key WHERE org_id = ?`, scopeParams.OrgID); err != nil {
return u, err
} else {
tag, err := quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, quota.OrgScope)
if err != nil {
return nil, err
}
u.Set(tag, r.Count)
}
}
return u, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/quota"
)
type store interface {
@ -15,4 +16,6 @@ type store interface {
GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQuery) error
GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error)
UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error
Count(context.Context, *quota.ScopeParameters) (*quota.Map, error)
}

View File

@ -11,6 +11,8 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
)
@ -174,3 +176,47 @@ func (ss *sqlStore) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64)
return nil
})
}
func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
type result struct {
Count int64
}
r := result{}
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) AS count FROM api_key"
if _, err := sess.SQL(rawSQL).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, quota.GlobalScope)
if err != nil {
return nil, err
}
u.Set(tag, r.Count)
}
if scopeParams.OrgID != 0 {
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) AS count FROM api_key WHERE org_id = ?"
if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, quota.OrgScope)
if err != nil {
return nil, err
}
u.Set(tag, r.Count)
}
}
return u, nil
}

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
)
@ -64,3 +65,8 @@ type GetByIDQuery struct {
ApiKeyId int64
Result *APIKey
}
const (
QuotaTargetSrv quota.TargetSrv = "api_key"
QuotaTarget quota.Target = "api_key"
)

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -41,19 +42,38 @@ type UserAuthTokenService struct {
log log.Logger
}
type ActiveTokenService interface {
ActiveTokenCount(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error)
}
type ActiveAuthTokenService struct {
cfg *setting.Cfg
sqlStore db.DB
}
func ProvideActiveAuthTokenService(cfg *setting.Cfg, sqlStore db.DB) *ActiveAuthTokenService {
return &ActiveAuthTokenService{
func ProvideActiveAuthTokenService(cfg *setting.Cfg, sqlStore db.DB, quotaService quota.Service) (*ActiveAuthTokenService, error) {
s := &ActiveAuthTokenService{
cfg: cfg,
sqlStore: sqlStore,
}
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return s, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: QuotaTargetSrv,
DefaultLimits: defaultLimits,
Reporter: s.ActiveTokenCount,
}); err != nil {
return s, err
}
return s, nil
}
func (a *ActiveAuthTokenService) ActiveTokenCount(ctx context.Context) (int64, error) {
func (a *ActiveAuthTokenService) ActiveTokenCount(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) {
var count int64
var err error
err = a.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
@ -66,7 +86,14 @@ func (a *ActiveAuthTokenService) ActiveTokenCount(ctx context.Context) (int64, e
return err
})
return count, err
tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
if err != nil {
return nil, err
}
u := &quota.Map{}
u.Set(tag, count)
return u, err
}
func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*models.UserToken, error) {
@ -472,3 +499,19 @@ func hashToken(token string) string {
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
return hex.EncodeToString(hashBytes[:])
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.Session)
return limits, nil
}

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"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/user"
"github.com/grafana/grafana/pkg/setting"
)
@ -40,8 +41,12 @@ func TestUserAuthToken(t *testing.T) {
userToken := createToken()
t.Run("Can count active tokens", func(t *testing.T) {
count, err := ctx.activeTokenService.ActiveTokenCount(context.Background())
m, err := ctx.activeTokenService.ActiveTokenCount(context.Background(), &quota.ScopeParameters{})
require.Nil(t, err)
tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
require.NoError(t, err)
count, ok := m.Get(tag)
require.True(t, ok)
require.Equal(t, int64(1), count)
})
@ -208,8 +213,12 @@ func TestUserAuthToken(t *testing.T) {
require.Nil(t, notGood)
t.Run("should not find active token when expired", func(t *testing.T) {
count, err := ctx.activeTokenService.ActiveTokenCount(context.Background())
m, err := ctx.activeTokenService.ActiveTokenCount(context.Background(), &quota.ScopeParameters{})
require.Nil(t, err)
tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
require.NoError(t, err)
count, ok := m.Get(tag)
require.True(t, ok)
require.Equal(t, int64(0), count)
})
})

View File

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
)
type userAuthToken struct {
@ -71,3 +72,8 @@ func (uat *userAuthToken) toUserToken(ut *models.UserToken) error {
return nil
}
const (
QuotaTargetSrv quota.TargetSrv = "auth"
QuotaTarget quota.Target = "session"
)

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/web"
)
@ -64,9 +65,9 @@ func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Re
return response.Error(http.StatusUnprocessableEntity, "Dashboard must be set", nil)
}
limitReached, err := api.quotaService.QuotaReached(c, "dashboard")
limitReached, err := api.quotaService.QuotaReached(c, dashboards.QuotaTargetSrv)
if err != nil {
return response.Error(500, "failed to get quota", err)
return response.Err(err)
}
if limitReached {
@ -83,12 +84,12 @@ func (api *ImportDashboardAPI) ImportDashboard(c *models.ReqContext) response.Re
}
type QuotaService interface {
QuotaReached(c *models.ReqContext, target string) (bool, error)
QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error)
}
type quotaServiceFunc func(c *models.ReqContext, target string) (bool, error)
type quotaServiceFunc func(c *models.ReqContext, target quota.TargetSrv) (bool, error)
func (fn quotaServiceFunc) QuotaReached(c *models.ReqContext, target string) (bool, error) {
func (fn quotaServiceFunc) QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) {
return fn(c, target)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/models"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/dashboardimport"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/require"
@ -165,10 +166,10 @@ func (s *serviceMock) ImportDashboard(ctx context.Context, req *dashboardimport.
return nil, nil
}
func quotaReached(c *models.ReqContext, target string) (bool, error) {
func quotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) {
return true, nil
}
func quotaNotReached(c *models.ReqContext, target string) (bool, error) {
func quotaNotReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) {
return false, nil
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
)
// DashboardService is a service for operating on dashboards.
@ -77,6 +78,7 @@ type Store interface {
ValidateDashboardBeforeSave(ctx context.Context, dashboard *models.Dashboard, overwrite bool) (bool, error)
DeleteACLByUser(context.Context, int64) error
Count(context.Context, *quota.ScopeParameters) (*quota.Map, error)
// CountDashboardsInFolder returns the number of dashboards associated with
// the given parent folder ID.
CountDashboardsInFolder(ctx context.Context, request *CountDashboardsInFolderRequest) (int64, error)

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
@ -26,7 +27,10 @@ func TestIntegrationDashboardACLDataAccess(t *testing.T) {
setup := func(t *testing.T) {
sqlStore = db.InitTestDB(t)
dashboardStore = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
currentUser = createUser(t, sqlStore, "viewer", "Viewer", false)
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp")

View File

@ -16,6 +16,8 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
dashver "github.com/grafana/grafana/pkg/services/dashboardversion"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/sqlstore/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
@ -42,8 +44,23 @@ type DashboardTag struct {
// DashboardStore implements the Store interface
var _ dashboards.Store = (*DashboardStore)(nil)
func ProvideDashboardStore(sqlStore db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tagService tag.Service) *DashboardStore {
return &DashboardStore{store: sqlStore, cfg: cfg, log: log.New("dashboard-store"), features: features, tagService: tagService}
func ProvideDashboardStore(sqlStore db.DB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, tagService tag.Service, quotaService quota.Service) (*DashboardStore, error) {
s := &DashboardStore{store: sqlStore, cfg: cfg, log: log.New("dashboard-store"), features: features, tagService: tagService}
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return nil, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: dashboards.QuotaTargetSrv,
DefaultLimits: defaultLimits,
Reporter: s.Count,
}); err != nil {
return nil, err
}
return s, nil
}
func (d *DashboardStore) emitEntityEvent() bool {
@ -291,6 +308,50 @@ func (d *DashboardStore) DeleteOrphanedProvisionedDashboards(ctx context.Context
})
}
func (d *DashboardStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
type result struct {
Count int64
}
r := result{}
if err := d.store.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM dashboard WHERE is_folder=%s", d.store.GetDialect().BooleanStr(false))
if _, err := sess.SQL(rawSQL).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
if err != nil {
return nil, err
}
u.Set(tag, r.Count)
}
if scopeParams.OrgID != 0 {
if err := d.store.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM dashboard WHERE org_id=? AND is_folder=%s", d.store.GetDialect().BooleanStr(false))
if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
if err != nil {
return nil, err
}
u.Set(tag, r.Count)
}
}
return u, nil
}
func getExistingDashboardByIdOrUidForUpdate(sess *db.Session, dash *models.Dashboard, dialect migrator.Dialect, overwrite bool) (bool, error) {
dashWithIdExists := false
isParentFolderChanged := false
@ -1018,6 +1079,27 @@ func (d *DashboardStore) GetDashboardTags(ctx context.Context, query *models.Get
})
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope)
if err != nil {
return &quota.Map{}, err
}
orgQuotaTag, err := quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope)
if err != nil {
return &quota.Map{}, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.Dashboard)
limits.Set(orgQuotaTag, cfg.Quota.Org.Dashboard)
return limits, nil
}
// This will be updated to take CountDashboardsInFolderQuery as an argument and
// lookup dashboards using the ParentFolderUID when the NestedFolder
// implementation is complete.

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
@ -33,7 +34,10 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
setup := func() {
sqlStore = db.InitTestDB(t)
sqlStore.Cfg.RBACEnabled = false
dashboardStore = ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod", "webapp")
childDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, folder.Id, false, "prod", "webapp")
@ -186,7 +190,9 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
setup2 := func() {
sqlStore = db.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod")
folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod")
dashInRoot = insertTestDashboard(t, dashboardStore, "test dash 67", 1, 0, false, "prod")
@ -291,7 +297,9 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
setup3 := func() {
sqlStore = db.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folder1 = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod")
folder2 = insertTestDashboard(t, dashboardStore, "2 test dash folder", 1, 0, true, "prod")
insertTestDashboard(t, dashboardStore, "folder in another org", 2, 0, true, "prod")
@ -473,7 +481,9 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
var sqlStore *sqlstore.SQLStore
var folder1, folder2 *models.Dashboard
sqlStore = db.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folder2 = insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod")
_ = insertTestDashboard(t, dashboardStore, title, orgId, folder2.Id, false, "prod")
folder1 = insertTestDashboard(t, dashboardStore, title, orgId, 0, true, "prod")
@ -488,7 +498,9 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
t.Run("GetFolderByUID", func(t *testing.T) {
var orgId int64 = 1
sqlStore := db.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod")
dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod")
@ -512,7 +524,9 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) {
t.Run("GetFolderByID", func(t *testing.T) {
var orgId int64 = 1
sqlStore := db.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folder := insertTestDashboard(t, dashboardStore, "TEST", orgId, 0, true, "prod")
dash := insertTestDashboard(t, dashboardStore, "Very Unique Name", orgId, folder.Id, false, "prod")

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
)
@ -18,7 +19,9 @@ func TestIntegrationDashboardProvisioningTest(t *testing.T) {
t.Skip("skipping integration test")
}
sqlStore := db.InitTestDB(t)
dashboardStore := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
folderCmd := models.SaveDashboardCommand{
OrgId: 1,

View File

@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/publicdashboards/database"
publicDashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/star"
@ -42,7 +43,10 @@ func TestIntegrationDashboardDataAccess(t *testing.T) {
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
starService = starimpl.ProvideService(sqlStore, cfg)
dashboardStore = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
var err error
dashboardStore, err = ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp")
savedDash = insertTestDashboard(t, dashboardStore, "test dash 23", 1, savedFolder.Id, false, "prod", "webapp")
insertTestDashboard(t, dashboardStore, "test dash 45", 1, savedFolder.Id, false, "prod")
@ -585,7 +589,9 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
dashboardStore := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
pluginId := "test-app"
appFolder := insertTestDashboardForPlugin(t, dashboardStore, "app-test", 1, 0, true, pluginId)
@ -597,7 +603,7 @@ func TestIntegrationDashboardDataAccessGivenPluginWithImportedDashboards(t *test
OrgId: 1,
}
err := dashboardStore.GetDashboardsByPluginID(context.Background(), &query)
err = dashboardStore.GetDashboardsByPluginID(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, len(query.Result), 2)
}
@ -609,7 +615,9 @@ func TestIntegrationDashboard_SortingOptions(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
dashboardStore := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, &setting.Cfg{}, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false)
dashA := insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false)
@ -660,7 +668,9 @@ func TestIntegrationDashboard_Filter(t *testing.T) {
sqlStore := db.InitTestDB(t)
cfg := setting.NewCfg()
cfg.IsFeatureToggleEnabled = func(key string) bool { return false }
dashboardStore := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := ProvideDashboardStore(sqlStore, cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
insertTestDashboard(t, dashboardStore, "Alfa", 1, 0, false)
dashB := insertTestDashboard(t, dashboardStore, "Beta", 1, 0, false)
qNoFilter := &models.FindPersistedDashboardsQuery{

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
)
@ -30,6 +31,11 @@ type DashboardSearchProjection struct {
SortMeta int64
}
const (
QuotaTargetSrv quota.TargetSrv = "dashboard"
QuotaTarget quota.Target = "dashboard"
)
type CountDashboardsInFolderQuery struct {
FolderUID string
}

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/services/user"
@ -42,7 +43,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
}),
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardNotFound, err)
})
@ -62,7 +63,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: false,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardNotFound, err)
})
@ -104,7 +105,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sqlStore)
err := callSaveWithError(t, cmd, sqlStore)
assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, int64(0), sc.dashboardGuardianMock.DashId)
@ -124,7 +125,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.otherSavedFolder.Id, sc.dashboardGuardianMock.DashId)
@ -144,7 +145,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
@ -165,7 +166,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
@ -186,7 +187,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
@ -207,7 +208,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
@ -228,7 +229,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
@ -249,7 +250,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
@ -270,7 +271,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInGeneralFolder.Id, sc.dashboardGuardianMock.DashId)
@ -291,7 +292,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: true,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err)
assert.Equal(t, sc.savedDashInFolder.Id, sc.dashboardGuardianMock.DashId)
@ -432,7 +433,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardFolderNotFound, err)
})
@ -448,7 +449,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err)
})
@ -488,7 +489,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err)
})
@ -527,7 +528,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardWithSameNameInFolderExists, err)
})
@ -543,7 +544,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardWithSameNameInFolderExists, err)
})
@ -559,7 +560,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardWithSameNameInFolderExists, err)
})
})
@ -647,7 +648,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardWithSameUIDExists, err)
})
@ -711,7 +712,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err)
})
@ -727,7 +728,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err)
})
@ -743,7 +744,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err)
})
@ -759,7 +760,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err)
})
@ -774,7 +775,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardWithSameNameAsFolder, err)
})
@ -789,7 +790,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) {
Overwrite: shouldOverwrite,
}
err := callSaveWithError(cmd, sc.sqlStore)
err := callSaveWithError(t, cmd, sc.sqlStore)
assert.Equal(t, dashboards.ErrDashboardFolderWithSameNameAsDashboard, err)
})
})
@ -821,7 +822,9 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc
cfg.RBACEnabled = false
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
sqlStore := db.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
service := ProvideDashboardService(
cfg, dashboardStore, &dummyDashAlertExtractor{},
featuremgmt.WithFeatures(),
@ -878,7 +881,9 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore
cfg := setting.NewCfg()
cfg.RBACEnabled = false
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
service := ProvideDashboardService(
cfg, dashboardStore, &dummyDashAlertExtractor{},
featuremgmt.WithFeatures(),
@ -892,12 +897,14 @@ func callSaveWithResult(t *testing.T, cmd models.SaveDashboardCommand, sqlStore
return res
}
func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore db.DB) error {
func callSaveWithError(t *testing.T, cmd models.SaveDashboardCommand, sqlStore db.DB) error {
dto := toSaveDashboardDto(cmd)
cfg := setting.NewCfg()
cfg.RBACEnabled = false
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
service := ProvideDashboardService(
cfg, dashboardStore, &dummyDashAlertExtractor{},
featuremgmt.WithFeatures(),
@ -905,7 +912,7 @@ func callSaveWithError(cmd models.SaveDashboardCommand, sqlStore db.DB) error {
accesscontrolmock.NewMockedPermissionsService(),
accesscontrolmock.New(),
)
_, err := service.SaveDashboard(context.Background(), &dto, false)
_, err = service.SaveDashboard(context.Background(), &dto, false)
return err
}
@ -934,7 +941,9 @@ func saveTestDashboard(t *testing.T, title string, orgID, folderID int64, sqlSto
cfg := setting.NewCfg()
cfg.RBACEnabled = false
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
service := ProvideDashboardService(
cfg, dashboardStore, &dummyDashAlertExtractor{},
featuremgmt.WithFeatures(),
@ -972,7 +981,9 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *mo
cfg := setting.NewCfg()
cfg.RBACEnabled = false
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
service := ProvideDashboardService(
cfg, dashboardStore, &dummyDashAlertExtractor{},
featuremgmt.WithFeatures(),

View File

@ -6,6 +6,7 @@ import (
context "context"
models "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/quota"
mock "github.com/stretchr/testify/mock"
)
@ -473,6 +474,10 @@ type mockConstructorTestingTNewFakeDashboardStore interface {
Cleanup(func())
}
func (_m *FakeDashboardStore) Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) {
return nil, nil
}
// NewFakeDashboardStore creates a new instance of FakeDashboardStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewFakeDashboardStore(t mockConstructorTestingTNewFakeDashboardStore) *FakeDashboardStore {
mock := &FakeDashboardStore{}

View File

@ -4,6 +4,7 @@ import (
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
)
@ -193,3 +194,8 @@ type DatasourcesPermissionFilterQuery struct {
Datasources []*DataSource
Result []*DataSource
}
const (
QuotaTargetSrv quota.TargetSrv = "data_source"
QuotaTarget quota.Target = "data_source"
)

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/kvstore"
"github.com/grafana/grafana/pkg/setting"
@ -52,7 +53,8 @@ type cachedRoundTripper struct {
func ProvideService(
db db.DB, secretsService secrets.Service, secretsStore kvstore.SecretsKVStore, cfg *setting.Cfg,
features featuremgmt.FeatureToggles, ac accesscontrol.AccessControl, datasourcePermissionsService accesscontrol.DatasourcePermissionsService,
) *Service {
quotaService quota.Service,
) (*Service, error) {
dslogger := log.New("datasources")
store := &SqlStore{db: db, logger: dslogger}
s := &Service{
@ -73,7 +75,23 @@ func ProvideService(
ac.RegisterScopeAttributeResolver(NewNameScopeResolver(store))
ac.RegisterScopeAttributeResolver(NewIDScopeResolver(store))
return s
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return nil, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: datasources.QuotaTargetSrv,
DefaultLimits: defaultLimits,
Reporter: s.Usage,
}); err != nil {
return nil, err
}
return s, nil
}
func (s *Service) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
return s.SQLStore.Count(ctx, scopeParams)
}
// DataSourceRetriever interface for retrieving a datasource.
@ -591,3 +609,24 @@ func (s *Service) fillWithSecureJSONData(ctx context.Context, cmd *datasources.U
return nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, quota.GlobalScope)
if err != nil {
return limits, err
}
orgQuotaTag, err := quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, quota.OrgScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.DataSource)
limits.Set(orgQuotaTag, cfg.Quota.Org.DataSource)
return limits, nil
}

View File

@ -21,6 +21,7 @@ import (
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"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/services/secrets"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
@ -200,7 +201,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
rt1, err := dsService.GetHTTPTransport(context.Background(), &ds, provider)
require.NoError(t, err)
@ -235,7 +238,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Id: 1,
@ -284,7 +289,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Id: 1,
@ -330,7 +337,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Id: 1,
@ -373,7 +382,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Id: 1,
@ -406,7 +417,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Id: 1,
@ -473,7 +486,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Id: 1,
Url: "http://k8s:8001",
@ -507,7 +522,9 @@ func TestService_GetHttpTransport(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
ds := datasources.DataSource{
Type: datasources.DS_ES,
@ -544,7 +561,9 @@ func TestService_getTimeout(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
for _, tc := range testCases {
ds := &datasources.DataSource{
@ -565,7 +584,9 @@ func TestService_GetDecryptedValues(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
jsonData := map[string]string{
"password": "securePassword",
@ -591,7 +612,9 @@ func TestService_GetDecryptedValues(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
dsService := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
jsonData := map[string]string{
"password": "securePassword",

View File

@ -16,6 +16,8 @@ import (
"github.com/grafana/grafana/pkg/infra/metrics"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
)
@ -29,6 +31,8 @@ type Store interface {
AddDataSource(context.Context, *datasources.AddDataSourceCommand) error
UpdateDataSource(context.Context, *datasources.UpdateDataSourceCommand) error
GetAllDataSources(ctx context.Context, query *datasources.GetAllDataSourcesQuery) error
Count(context.Context, *quota.ScopeParameters) (*quota.Map, error)
}
type SqlStore struct {
@ -171,6 +175,50 @@ func (ss *SqlStore) DeleteDataSource(ctx context.Context, cmd *datasources.Delet
})
}
func (ss *SqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
type result struct {
Count int64
}
r := result{}
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) AS count FROM data_source"
if _, err := sess.SQL(rawSQL).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, quota.GlobalScope)
if err != nil {
return u, err
}
u.Set(tag, r.Count)
}
if scopeParams.OrgID != 0 {
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) AS count FROM data_source WHERE org_id=?"
if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, quota.OrgScope)
if err != nil {
return u, err
}
u.Set(tag, r.Count)
}
}
return u, nil
}
func (ss *SqlStore) AddDataSource(ctx context.Context, cmd *datasources.AddDataSourceCommand) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
existing := datasources.DataSource{OrgId: cmd.OrgId, Name: cmd.Name}

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/util"
"github.com/stretchr/testify/assert"
@ -583,7 +584,8 @@ func TestIntegrationGetChildren(t *testing.T) {
func CreateOrg(t *testing.T, db *sqlstore.SQLStore) int64 {
t.Helper()
orgService := orgimpl.ProvideService(db, db.Cfg)
orgService, err := orgimpl.ProvideService(db, db.Cfg, quotatest.New(false, nil))
require.NoError(t, err)
orgID, err := orgService.GetOrCreate(context.Background(), "test-org")
require.NoError(t, err)
t.Cleanup(func() {

View File

@ -19,6 +19,7 @@ import (
dashdb "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing/licensingtest"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/team/teamimpl"
"github.com/grafana/grafana/pkg/services/user"
@ -591,7 +592,9 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce
toSave.SetUid(uid)
// seed dashboard
dashStore := dashdb.ProvideDashboardStore(store, store.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(store, store.Cfg))
quotaService := quotatest.New(false, nil)
dashStore, err := dashdb.ProvideDashboardStore(store, store.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(store, store.Cfg), quotaService)
require.NoError(t, err)
dash, err := dashStore.SaveDashboard(context.Background(), models.SaveDashboardCommand{
Dashboard: toSave.Data,
UserId: 1,
@ -603,7 +606,8 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce
license := licensingtest.NewFakeLicensing()
license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe()
teamSvc := teamimpl.ProvideService(store, store.Cfg)
userSvc := userimpl.ProvideService(store, nil, store.Cfg, nil, nil)
userSvc, err := userimpl.ProvideService(store, nil, store.Cfg, nil, nil, quotatest.New(false, nil))
require.NoError(t, err)
folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions(
setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac, teamSvc, userSvc)

View File

@ -28,6 +28,7 @@ import (
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/services/user"
@ -278,7 +279,9 @@ func createDashboard(t *testing.T, sqlStore db.DB, user user.SignedInUser, dash
cfg.RBACEnabled = false
features := featuremgmt.WithFeatures()
cfg.IsFeatureToggleEnabled = features.IsEnabled
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashAlertExtractor := alerting.ProvideDashAlertExtractorService(nil, nil, nil)
ac := acmock.New()
folderPermissions := acmock.NewMockedPermissionsService()
@ -304,7 +307,9 @@ func createFolderWithACL(t *testing.T, sqlStore db.DB, title string, user user.S
ac := acmock.New()
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
d := dashboardservice.ProvideDashboardService(
cfg, dashboardStore, nil,
@ -405,7 +410,9 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
orgID := int64(1)
role := org.RoleAdmin
sqlStore := db.InitTestDB(t)
dashboardStore := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
features := featuremgmt.WithFeatures()
ac := acmock.New().WithDisabled()
// TODO: Update tests to work with rbac
@ -442,7 +449,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
Login: userInDbName,
}
_, err := sqlStore.CreateUser(context.Background(), cmd)
_, err = sqlStore.CreateUser(context.Background(), cmd)
require.NoError(t, err)
sc := scenarioContext{

View File

@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/services/guardian"
"github.com/grafana/grafana/pkg/services/libraryelements"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/team/teamtest"
"github.com/grafana/grafana/pkg/services/user"
@ -691,7 +692,9 @@ func createDashboard(t *testing.T, sqlStore db.DB, user *user.SignedInUser, dash
cfg := setting.NewCfg()
cfg.RBACEnabled = false
cfg.IsFeatureToggleEnabled = featuremgmt.WithFeatures().IsEnabled
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashAlertService := alerting.ProvideDashAlertExtractorService(nil, nil, nil)
ac := acmock.New()
service := dashboardservice.ProvideDashboardService(
@ -715,7 +718,9 @@ func createFolderWithACL(t *testing.T, sqlStore db.DB, title string, user *user.
features := featuremgmt.WithFeatures()
folderPermissions := acmock.NewMockedPermissionsService()
dashboardPermissions := acmock.NewMockedPermissionsService()
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
d := dashboardservice.ProvideDashboardService(cfg, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ac)
s := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), cfg, d, dashboardStore, features, folderPermissions, nil)
@ -808,7 +813,9 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
orgID := int64(1)
role := org.RoleAdmin
sqlStore, cfg := db.InitTestDBwithCfg(t)
dashboardStore := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
features := featuremgmt.WithFeatures()
ac := acmock.New()
@ -847,7 +854,7 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo
Login: userInDbName,
}
_, err := sqlStore.CreateUser(context.Background(), cmd)
_, err = sqlStore.CreateUser(context.Background(), cmd)
require.NoError(t, err)
sc := scenarioContext{

View File

@ -71,13 +71,17 @@ func (ls *Implementation) UpsertUser(ctx context.Context, cmd *models.UpsertUser
return login.ErrSignupNotAllowed
}
limitReached, errLimit := ls.QuotaService.QuotaReached(cmd.ReqContext, "user")
if errLimit != nil {
cmd.ReqContext.Logger.Warn("Error getting user quota.", "error", errLimit)
return login.ErrGettingUserQuota
}
if limitReached {
return login.ErrUsersQuotaReached
// we may insert in both user and org_user tables
// therefore we need to query check quota for both user and org services
for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} {
limitReached, errLimit := ls.QuotaService.QuotaReached(cmd.ReqContext, quota.TargetSrv(srv))
if errLimit != nil {
cmd.ReqContext.Logger.Warn("Error getting user quota.", "error", errLimit)
return login.ErrGettingUserQuota
}
if limitReached {
return login.ErrUsersQuotaReached
}
}
result, errCreateUser := ls.createUser(extUser)

View File

@ -13,7 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/login/logintest"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/usertest"
"github.com/stretchr/testify/assert"
@ -26,7 +26,7 @@ func Test_syncOrgRoles_doesNotBreakWhenTryingToRemoveLastOrgAdmin(t *testing.T)
authInfoMock := &logintest.AuthInfoServiceFake{}
login := Implementation{
QuotaService: &quotaimpl.Service{},
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
SQLStore: nil,
userService: usertest.NewUserServiceFake(),
@ -51,7 +51,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
orgService.ExpectedOrgListResponse = createResponseWithOneErrLastOrgAdminItem()
login := Implementation{
QuotaService: &quotaimpl.Service{},
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
SQLStore: nil,
userService: usertest.NewUserServiceFake(),
@ -66,7 +66,7 @@ func Test_syncOrgRoles_whenTryingToRemoveLastOrgLogsError(t *testing.T) {
func Test_teamSync(t *testing.T) {
authInfoMock := &logintest.AuthInfoServiceFake{}
login := Implementation{
QuotaService: &quotaimpl.Service{},
QuotaService: quotatest.New(false, nil),
AuthInfoService: authInfoMock,
}

View File

@ -145,3 +145,28 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
alertRules: api.AlertRules,
}), m)
}
func (api *API) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
if orgUsage, err := api.RuleStore.Count(ctx, scopeParams.OrgID); err != nil {
return u, err
} else {
tag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.OrgScope)
if err != nil {
return u, err
}
u.Set(tag, orgUsage)
}
if globalUsage, err := api.RuleStore.Count(ctx, 0); err != nil {
return u, err
} else {
tag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.GlobalScope)
if err != nil {
return u, err
}
u.Set(tag, globalUsage)
}
return u, nil
}

View File

@ -393,7 +393,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
}
if len(finalChanges.New) > 0 {
limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, "alert_rule", &quota.ScopeParameters{
limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, ngmodels.QuotaTargetSrv, &quota.ScopeParameters{
OrgID: c.OrgID,
UserID: c.UserID,
}) // alert rule is table name

View File

@ -23,4 +23,6 @@ type RuleStore interface {
// IncreaseVersionForAllRulesInNamespace Increases version for all rules that have specified namespace. Returns all rules that belong to the namespace
IncreaseVersionForAllRulesInNamespace(ctx context.Context, orgID int64, namespaceUID string) ([]ngmodels.AlertRuleKeyWithVersion, error)
Count(ctx context.Context, orgID int64) (int64, error)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/util/cmputil"
)
@ -460,6 +461,11 @@ func (g RulesGroup) SortByGroupIndex() {
})
}
const (
QuotaTargetSrv quota.TargetSrv = "ngalert"
QuotaTarget quota.Target = "alert_rule"
)
type ruleKeyContextKey struct{}
func WithRuleKey(ctx context.Context, ruleKey AlertRuleKey) context.Context {

View File

@ -239,6 +239,19 @@ func (ng *AlertNG) init() error {
}
api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics())
defaultLimits, err := readQuotaConfig(ng.Cfg)
if err != nil {
return err
}
if err := ng.QuotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: models.QuotaTargetSrv,
DefaultLimits: defaultLimits,
Reporter: api.Usage,
}); err != nil {
return err
}
log.RegisterContextualLogProvider(func(ctx context.Context) ([]interface{}, bool) {
key, ok := models.RuleKeyFromContext(ctx)
if !ok {
@ -308,3 +321,32 @@ func (ng *AlertNG) IsDisabled() bool {
}
return !ng.Cfg.UnifiedAlerting.IsEnabled()
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
var alertOrgQuota int64
var alertGlobalQuota int64
if cfg.UnifiedAlerting.IsEnabled() {
alertOrgQuota = cfg.Quota.Org.AlertRule
alertGlobalQuota = cfg.Quota.Global.AlertRule
}
globalQuotaTag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.GlobalScope)
if err != nil {
return limits, err
}
orgQuotaTag, err := quota.NewTag(models.QuotaTargetSrv, models.QuotaTarget, quota.OrgScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, alertGlobalQuota)
limits.Set(orgQuotaTag, alertOrgQuota)
return limits, nil
}

View File

@ -48,7 +48,7 @@ type RuleStore interface {
//
//go:generate mockery --name QuotaChecker --structname MockQuotaChecker --inpackage --filename quota_checker_mock.go --with-expecter
type QuotaChecker interface {
CheckQuotaReached(ctx context.Context, target string, scopeParams *quota.ScopeParameters) (bool, error)
CheckQuotaReached(ctx context.Context, target quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error)
}
// PersistConfig validates to config before eventually persisting it if no error occurs

View File

@ -1,4 +1,4 @@
// Code generated by mockery v2.12.0. DO NOT EDIT.
// Code generated by mockery v2.14.0. DO NOT EDIT.
package provisioning
@ -7,8 +7,6 @@ import (
quota "github.com/grafana/grafana/pkg/services/quota"
mock "github.com/stretchr/testify/mock"
testing "testing"
)
// MockQuotaChecker is an autogenerated mock type for the QuotaChecker type
@ -25,18 +23,18 @@ func (_m *MockQuotaChecker) EXPECT() *MockQuotaChecker_Expecter {
}
// CheckQuotaReached provides a mock function with given fields: ctx, target, scopeParams
func (_m *MockQuotaChecker) CheckQuotaReached(ctx context.Context, target string, scopeParams *quota.ScopeParameters) (bool, error) {
func (_m *MockQuotaChecker) CheckQuotaReached(ctx context.Context, target quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error) {
ret := _m.Called(ctx, target, scopeParams)
var r0 bool
if rf, ok := ret.Get(0).(func(context.Context, string, *quota.ScopeParameters) bool); ok {
if rf, ok := ret.Get(0).(func(context.Context, quota.TargetSrv, *quota.ScopeParameters) bool); ok {
r0 = rf(ctx, target, scopeParams)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, *quota.ScopeParameters) error); ok {
if rf, ok := ret.Get(1).(func(context.Context, quota.TargetSrv, *quota.ScopeParameters) error); ok {
r1 = rf(ctx, target, scopeParams)
} else {
r1 = ret.Error(1)
@ -51,16 +49,16 @@ type MockQuotaChecker_CheckQuotaReached_Call struct {
}
// CheckQuotaReached is a helper method to define mock.On call
// - ctx context.Context
// - target string
// - scopeParams *quota.ScopeParameters
// - ctx context.Context
// - target quota.TargetSrv
// - scopeParams *quota.ScopeParameters
func (_e *MockQuotaChecker_Expecter) CheckQuotaReached(ctx interface{}, target interface{}, scopeParams interface{}) *MockQuotaChecker_CheckQuotaReached_Call {
return &MockQuotaChecker_CheckQuotaReached_Call{Call: _e.mock.On("CheckQuotaReached", ctx, target, scopeParams)}
}
func (_c *MockQuotaChecker_CheckQuotaReached_Call) Run(run func(ctx context.Context, target string, scopeParams *quota.ScopeParameters)) *MockQuotaChecker_CheckQuotaReached_Call {
func (_c *MockQuotaChecker_CheckQuotaReached_Call) Run(run func(ctx context.Context, target quota.TargetSrv, scopeParams *quota.ScopeParameters)) *MockQuotaChecker_CheckQuotaReached_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(*quota.ScopeParameters))
run(args[0].(context.Context), args[1].(quota.TargetSrv), args[2].(*quota.ScopeParameters))
})
return _c
}
@ -70,8 +68,13 @@ func (_c *MockQuotaChecker_CheckQuotaReached_Call) Return(_a0 bool, _a1 error) *
return _c
}
// NewMockQuotaChecker creates a new instance of MockQuotaChecker. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockQuotaChecker(t testing.TB) *MockQuotaChecker {
type mockConstructorTestingTNewMockQuotaChecker interface {
mock.TestingT
Cleanup(func())
}
// NewMockQuotaChecker creates a new instance of MockQuotaChecker. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewMockQuotaChecker(t mockConstructorTestingTNewMockQuotaChecker) *MockQuotaChecker {
mock := &MockQuotaChecker{}
mock.Mock.Test(t)

View File

@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/guardian"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
@ -268,6 +269,29 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
})
}
// Count returns either the number of the alert rules under a specific org (if orgID is not zero)
// or the number of all the alert rules
func (st DBstore) Count(ctx context.Context, orgID int64) (int64, error) {
type result struct {
Count int64
}
r := result{}
err := st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) as count from alert_rule"
args := make([]interface{}, 0)
if orgID != 0 {
rawSQL += " WHERE org_id=?"
args = append(args, orgID)
}
if _, err := sess.SQL(rawSQL, args...).Get(&r); err != nil {
return err
}
return nil
})
return r.Count, err
}
func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) {
var interval int64 = 0
return interval, st.SQLStore.WithDbSession(ctx, func(sess *db.Session) error {

View File

@ -339,3 +339,7 @@ func (f *RuleStore) IncreaseVersionForAllRulesInNamespace(_ context.Context, org
}
return result, nil
}
func (f *RuleStore) Count(ctx context.Context, orgID int64) (int64, error) {
return 0, nil
}

View File

@ -31,6 +31,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets/database"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
@ -75,7 +76,9 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG,
m := metrics.NewNGAlert(prometheus.NewRegistry())
sqlStore := db.InitTestDB(tb)
secretsService := secretsManager.SetupTestService(tb, database.ProvideSecretsStore(sqlStore))
dashboardStore := databasestore.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := databasestore.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(tb, err)
ac := acmock.New()
features := featuremgmt.WithFeatures()
@ -92,7 +95,7 @@ func SetupTestEnv(tb testing.TB, baseInterval time.Duration) (*ngalert.AlertNG,
folderService := folderimpl.ProvideService(ac, bus, cfg, dashboardService, dashboardStore, features, folderPermissions, nil)
ng, err := ngalert.ProvideService(
cfg, &FakeFeatures{}, nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, nil,
cfg, &FakeFeatures{}, nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotatest.New(false, nil),
secretsService, nil, m, folderService, ac, &dashboards.FakeDashboardService{}, nil, bus, ac, annotationstest.NewFakeAnnotationsRepo(),
)
require.NoError(tb, err)

View File

@ -204,3 +204,9 @@ func (o ByOrgName) Less(i, j int) bool {
return o[i].Name < o[j].Name
}
const (
QuotaTargetSrv string = "org"
OrgQuotaTarget string = "org"
OrgUserQuotaTarget string = "org_user"
)

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@ -18,9 +19,9 @@ type Service struct {
log log.Logger
}
func ProvideService(db db.DB, cfg *setting.Cfg) org.Service {
func ProvideService(db db.DB, cfg *setting.Cfg, quotaService quota.Service) (org.Service, error) {
log := log.New("org service")
return &Service{
s := &Service{
store: &sqlStore{
db: db,
dialect: db.GetDialect(),
@ -30,6 +31,24 @@ func ProvideService(db db.DB, cfg *setting.Cfg) org.Service {
cfg: cfg,
log: log,
}
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return s, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: quota.TargetSrv(org.QuotaTargetSrv),
DefaultLimits: defaultLimits,
Reporter: s.Usage,
}); err != nil {
return s, nil
}
return s, nil
}
func (s *Service) Usage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
return s.store.Count(ctx, scopeParams)
}
func (s *Service) GetIDForNewUser(ctx context.Context, cmd org.GetOrgIDForNewUserCommand) (int64, error) {
@ -179,3 +198,31 @@ func (s *Service) GetOrgUsers(ctx context.Context, query *org.GetOrgUsersQuery)
func (s *Service) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error) {
return s.store.SearchOrgUsers(ctx, query)
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgQuotaTarget), quota.GlobalScope)
if err != nil {
return limits, err
}
orgQuotaTag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.OrgScope)
if err != nil {
return limits, err
}
userTag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.UserScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.Org)
// users per org
limits.Set(orgQuotaTag, cfg.Quota.Org.User)
// orgs per user
limits.Set(userTag, cfg.Quota.User.Org)
return limits, nil
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
@ -135,3 +136,7 @@ func (f *FakeOrgStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgU
func (f *FakeOrgStore) RemoveOrgUser(ctx context.Context, cmd *org.RemoveOrgUserCommand) error {
return f.ExpectedError
}
func (f *FakeOrgStore) Count(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) {
return nil, nil
}

View File

@ -14,6 +14,8 @@ import (
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -42,6 +44,8 @@ type store interface {
GetByName(context.Context, *org.GetOrgByNameQuery) (*org.Org, error)
SearchOrgUsers(context.Context, *org.SearchOrgUsersQuery) (*org.SearchOrgUsersQueryResult, error)
RemoveOrgUser(context.Context, *org.RemoveOrgUserCommand) error
Count(context.Context, *quota.ScopeParameters) (*quota.Map, error)
}
type sqlStore struct {
@ -395,6 +399,72 @@ func (ss *sqlStore) AddOrgUser(ctx context.Context, cmd *org.AddOrgUserCommand)
})
}
func (ss *sqlStore) Count(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
type result struct {
Count int64
}
r := result{}
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) as count from org"
if _, err := sess.SQL(rawSQL).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgQuotaTarget), quota.GlobalScope)
if err != nil {
return u, err
}
u.Set(tag, r.Count)
}
if scopeParams.OrgID != 0 {
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM (SELECT user_id FROM org_user WHERE org_id=? AND user_id IN (SELECT id AS user_id FROM %s WHERE is_service_account=%s)) as subq",
ss.db.GetDialect().Quote("user"),
ss.db.GetDialect().BooleanStr(false),
)
if _, err := sess.SQL(rawSQL, scopeParams.OrgID).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.OrgScope)
if err != nil {
return u, err
}
u.Set(tag, r.Count)
}
}
if scopeParams.UserID != 0 {
if err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
// should we exclude service accounts?
rawSQL := "SELECT COUNT(*) AS count FROM org_user WHERE user_id=?"
if _, err := sess.SQL(rawSQL, scopeParams.UserID).Get(&r); err != nil {
return err
}
return nil
}); err != nil {
return u, err
} else {
tag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.UserScope)
if err != nil {
return u, err
}
u.Set(tag, r.Count)
}
}
return u, nil
}
func setUsingOrgInTransaction(sess *db.Session, userID int64, orgID int64) error {
user := user.User{
ID: userID,

View File

@ -28,6 +28,7 @@ import (
publicdashboardsStore "github.com/grafana/grafana/pkg/services/publicdashboards/database"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
publicdashboardsService "github.com/grafana/grafana/pkg/services/publicdashboards/service"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -300,7 +301,8 @@ func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T)
}
// create dashboard
dashboardStoreService := dashboardStore.ProvideDashboardStore(db, db.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db, db.Cfg))
dashboardStoreService, err := dashboardStore.ProvideDashboardStore(db, db.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(db, db.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
dashboard, err := dashboardStoreService.SaveDashboard(context.Background(), saveDashboardCmd)
require.NoError(t, err)

View File

@ -13,6 +13,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
@ -35,7 +36,9 @@ func TestIntegrationListPublicDashboard(t *testing.T) {
t.Skip("skipping integration test")
}
sqlStore, cfg := db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := ProvideStore(sqlStore)
var orgId int64 = 1
@ -78,7 +81,10 @@ func TestIntegrationFindDashboard(t *testing.T) {
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -105,7 +111,10 @@ func TestIntegrationExistsEnabledByAccessToken(t *testing.T) {
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -175,7 +184,10 @@ func TestIntegrationExistsEnabledByDashboardUid(t *testing.T) {
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -237,7 +249,10 @@ func TestIntegrationFindByDashboardUid(t *testing.T) {
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -299,10 +314,12 @@ func TestIntegrationFindByAccessToken(t *testing.T) {
var dashboardStore *dashboardsDB.DashboardStore
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
var err error
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -369,7 +386,10 @@ func TestIntegrationCreatePublicDashboard(t *testing.T) {
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
store, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
dashboardStore = store
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedDashboard2 = insertTestDashboard(t, dashboardStore, "testDashie2", 1, 0, true)
@ -436,10 +456,13 @@ func TestIntegrationUpdatePublicDashboard(t *testing.T) {
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
var anotherSavedDashboard *models.Dashboard
var err error
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagPublicDashboards}})
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
anotherSavedDashboard = insertTestDashboard(t, dashboardStore, "test another Dashie", 1, 0, true)
@ -529,10 +552,13 @@ func TestIntegrationGetOrgIdByAccessToken(t *testing.T) {
var dashboardStore *dashboardsDB.DashboardStore
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
var err error
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotaService)
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
}
@ -599,10 +625,12 @@ func TestIntegrationDelete(t *testing.T) {
var publicdashboardStore *PublicDashboardStoreImpl
var savedDashboard *models.Dashboard
var savedPublicDashboard *PublicDashboard
var err error
setup := func() {
sqlStore, cfg = db.InitTestDBwithCfg(t)
dashboardStore = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg))
dashboardStore, err = dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore = ProvideStore(sqlStore)
savedDashboard = insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true)
savedPublicDashboard = insertPublicDashboard(t, publicdashboardStore, savedDashboard.Uid, savedDashboard.OrgId, true)

View File

@ -20,6 +20,7 @@ import (
"github.com/grafana/grafana/pkg/services/publicdashboards/database"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/setting"
@ -355,7 +356,8 @@ const (
func TestGetQueryDataResponse(t *testing.T) {
sqlStore := sqlstore.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
service := &PublicDashboardServiceImpl{
@ -738,7 +740,8 @@ func TestGetAnnotations(t *testing.T) {
func TestGetMetricRequest(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
publicDashboard := &PublicDashboard{
@ -811,7 +814,8 @@ func TestGetUniqueDashboardDatasourceUids(t *testing.T) {
func TestBuildMetricRequest(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
publicDashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -1022,7 +1026,8 @@ func TestBuildMetricRequest(t *testing.T) {
func TestBuildAnonymousUser(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
//publicdashboardStore := database.ProvideStore(sqlStore)
//service := &PublicDashboardServiceImpl{

View File

@ -21,6 +21,7 @@ import (
"github.com/grafana/grafana/pkg/services/publicdashboards/database"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
@ -125,7 +126,9 @@ func TestGetPublicDashboard(t *testing.T) {
func TestCreatePublicDashboard(t *testing.T) {
t.Run("Create public dashboard", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -147,7 +150,7 @@ func TestCreatePublicDashboard(t *testing.T) {
},
}
_, err := service.Create(context.Background(), SignedInUser, dto)
_, err = service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid)
@ -171,7 +174,9 @@ func TestCreatePublicDashboard(t *testing.T) {
t.Run("Validate pubdash has default time setting value", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -191,7 +196,7 @@ func TestCreatePublicDashboard(t *testing.T) {
},
}
_, err := service.Create(context.Background(), SignedInUser, dto)
_, err = service.Create(context.Background(), SignedInUser, dto)
require.NoError(t, err)
pubdash, err := service.FindByDashboardUid(context.Background(), dashboard.OrgId, dashboard.Uid)
@ -201,7 +206,9 @@ func TestCreatePublicDashboard(t *testing.T) {
t.Run("Validate pubdash whose dashboard has template variables returns error", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
templateVars := make([]map[string]interface{}, 1)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, templateVars, nil)
@ -222,7 +229,7 @@ func TestCreatePublicDashboard(t *testing.T) {
},
}
_, err := service.Create(context.Background(), SignedInUser, dto)
_, err = service.Create(context.Background(), SignedInUser, dto)
require.Error(t, err)
})
@ -265,7 +272,8 @@ func TestCreatePublicDashboard(t *testing.T) {
t.Run("Returns error if public dashboard exists", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotatest.New(false, nil))
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -316,7 +324,9 @@ func TestCreatePublicDashboard(t *testing.T) {
func TestUpdatePublicDashboard(t *testing.T) {
t.Run("Updating public dashboard", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)
@ -378,7 +388,9 @@ func TestUpdatePublicDashboard(t *testing.T) {
t.Run("Updating set empty time settings", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
dashboardStore := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg))
quotaService := quotatest.New(false, nil)
dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
publicdashboardStore := database.ProvideStore(sqlStore)
dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, true, []map[string]interface{}{}, nil)

View File

@ -23,6 +23,7 @@ import (
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
dsSvc "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
@ -389,7 +390,9 @@ func setup(t *testing.T) *testContext {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
ss := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
ssvc := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
ds := dsSvc.ProvideService(nil, ssvc, ss, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
ds, err := dsSvc.ProvideService(nil, ssvc, ss, nil, featuremgmt.WithFeatures(), acmock.New(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
fakeDatasourceService := &fakeDatasources.FakeDataSourceService{
DataSources: nil,
SimulatePluginFailure: false,

View File

@ -0,0 +1,42 @@
package quota
import (
"context"
"sync"
)
type Context struct {
context.Context
TargetToSrv *TargetToSrv
}
func FromContext(ctx context.Context, targetToSrv *TargetToSrv) Context {
if targetToSrv == nil {
targetToSrv = NewTargetToSrv()
}
return Context{Context: ctx, TargetToSrv: targetToSrv}
}
type TargetToSrv struct {
mutex sync.RWMutex
m map[Target]TargetSrv
}
func NewTargetToSrv() *TargetToSrv {
return &TargetToSrv{m: make(map[Target]TargetSrv)}
}
func (m *TargetToSrv) Get(target Target) (TargetSrv, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
srv, ok := m.m[target]
return srv, ok
}
func (m *TargetToSrv) Set(target Target, srv TargetSrv) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.m[target] = srv
}

View File

@ -1,10 +1,216 @@
package quota
import "errors"
import (
"strings"
"sync"
"time"
var ErrInvalidQuotaTarget = errors.New("invalid quota target")
"github.com/grafana/grafana/pkg/util/errutil"
)
var ErrBadRequest = errutil.NewBase(errutil.StatusBadRequest, "quota.bad-request")
var ErrInvalidTargetSrv = errutil.NewBase(errutil.StatusBadRequest, "quota.invalid-target")
var ErrInvalidScope = errutil.NewBase(errutil.StatusBadRequest, "quota.invalid-scope")
var ErrInvalidTarget = errutil.NewBase(errutil.StatusInternal, "quota.invalid-target-table")
var ErrTargetSrvConflict = errutil.NewBase(errutil.StatusBadRequest, "quota.target-srv-conflict")
var ErrDisabled = errutil.NewBase(errutil.StatusForbidden, "quota.disabled", errutil.WithPublicMessage("Quotas not enabled"))
var ErrInvalidTagFormat = errutil.NewBase(errutil.StatusInternal, "quota.invalid-invalid-tag-format")
type ScopeParameters struct {
OrgID int64
UserID int64
}
type Scope string
const (
GlobalScope Scope = "global"
OrgScope Scope = "org"
UserScope Scope = "user"
)
func (s Scope) Validate() error {
switch s {
case GlobalScope, OrgScope, UserScope:
return nil
default:
return ErrInvalidScope.Errorf("bad scope: %s", s)
}
}
type TargetSrv string
type Target string
const delimiter = ":"
// Tag is a string with the format <srv>:<target>:<scope>
type Tag string
func NewTag(srv TargetSrv, t Target, scope Scope) (Tag, error) {
if err := scope.Validate(); err != nil {
return "", err
}
tag := Tag(strings.Join([]string{string(srv), string(t), string(scope)}, delimiter))
return tag, nil
}
func (t Tag) split() ([]string, error) {
parts := strings.SplitN(string(t), delimiter, -1)
if len(parts) != 3 {
return nil, ErrInvalidTagFormat.Errorf("tag format should be ^(?<srv>\\w):(?<target>\\w):(?<scope>\\w)$")
}
return parts, nil
}
func (t Tag) GetSrv() (TargetSrv, error) {
parts, err := t.split()
if err != nil {
return "", err
}
return TargetSrv(parts[0]), nil
}
func (t Tag) GetTarget() (Target, error) {
parts, err := t.split()
if err != nil {
return "", err
}
return Target(parts[1]), nil
}
func (t Tag) GetScope() (Scope, error) {
parts, err := t.split()
if err != nil {
return "", err
}
return Scope(parts[2]), nil
}
type Item struct {
Tag Tag
Value int64
}
type Map struct {
mutex sync.RWMutex
m map[Tag]int64
}
func (m *Map) Set(tag Tag, limit int64) {
m.mutex.Lock()
defer m.mutex.Unlock()
if len(m.m) == 0 {
m.m = make(map[Tag]int64, 0)
}
m.m[tag] = limit
}
func (m *Map) Get(tag Tag) (int64, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
limit, ok := m.m[tag]
return limit, ok
}
func (m *Map) Merge(l2 *Map) {
l2.mutex.RLock()
defer l2.mutex.RUnlock()
for k, v := range l2.m {
// TODO check for conflicts?
m.Set(k, v)
}
}
func (m *Map) Iter() <-chan Item {
m.mutex.RLock()
defer m.mutex.RUnlock()
ch := make(chan Item)
go func() {
defer close(ch)
for t, v := range m.m {
ch <- Item{Tag: t, Value: v}
}
}()
return ch
}
func (m *Map) Scopes() (map[Scope]struct{}, error) {
res := make(map[Scope]struct{})
for item := range m.Iter() {
scope, err := item.Tag.GetScope()
if err != nil {
return nil, err
}
res[scope] = struct{}{}
}
return res, nil
}
func (m *Map) Services() (map[TargetSrv]struct{}, error) {
res := make(map[TargetSrv]struct{})
for item := range m.Iter() {
srv, err := item.Tag.GetSrv()
if err != nil {
return nil, err
}
res[srv] = struct{}{}
}
return res, nil
}
func (m *Map) Targets() (map[Target]struct{}, error) {
res := make(map[Target]struct{})
for item := range m.Iter() {
target, err := item.Tag.GetTarget()
if err != nil {
return nil, err
}
res[target] = struct{}{}
}
return res, nil
}
type Quota struct {
Id int64
OrgId int64
UserId int64
Target string
Limit int64
Created time.Time
Updated time.Time
}
type QuotaDTO struct {
OrgId int64 `json:"org_id,omitempty"`
UserId int64 `json:"user_id,omitempty"`
Target string `json:"target"`
Limit int64 `json:"limit"`
Used int64 `json:"used"`
Service string `json:"-"`
Scope string `json:"-"`
}
func (dto QuotaDTO) Tag() (Tag, error) {
return NewTag(TargetSrv(dto.Service), Target(dto.Target), Scope(dto.Scope))
}
type UpdateQuotaCmd struct {
Target string `json:"target"`
Limit int64 `json:"limit"`
OrgID int64 `json:"-"`
UserID int64 `json:"-"`
}
type NewUsageReporter struct {
TargetSrv TargetSrv
DefaultLimits *Map
Reporter UsageReporterFunc
}

View File

@ -7,7 +7,24 @@ import (
)
type Service interface {
QuotaReached(c *models.ReqContext, target string) (bool, error)
CheckQuotaReached(ctx context.Context, target string, scopeParams *ScopeParameters) (bool, error)
DeleteByUser(context.Context, int64) error
// GetQuotasByScope returns the quota for the specific scope (global, organization, user)
// If the scope is organization, the ID is expected to be the organisation ID.
// If the scope is user, the id is expected to be the user ID.
GetQuotasByScope(ctx context.Context, scope Scope, ID int64) ([]QuotaDTO, error)
// Update overrides the quota for a specific scope (global, organization, user).
// If the cmd.OrgID is set, then the organization quota are updated.
// If the cmd.UseID is set, then the user quota are updated.
Update(ctx context.Context, cmd *UpdateQuotaCmd) error
// QuotaReached is called by the quota middleware for applying quota enforcement to API handlers
QuotaReached(c *models.ReqContext, targetSrv TargetSrv) (bool, error)
// CheckQuotaReached checks if the quota limitations have been reached for a specific service
CheckQuotaReached(ctx context.Context, targetSrv TargetSrv, scopeParams *ScopeParameters) (bool, error)
// DeleteQuotaForUser deletes custom quota limitations for the user
DeleteQuotaForUser(ctx context.Context, userID int64) error
// DeleteByOrg(ctx context.Context, orgID int64) error
// RegisterQuotaReporter registers a service UsageReporterFunc, targets and their default limits
RegisterQuotaReporter(e *NewUsageReporter) error
}
type UsageReporterFunc func(ctx context.Context, scopeParams *ScopeParameters) (*Map, error)

View File

@ -2,38 +2,81 @@ package quotaimpl
import (
"context"
"fmt"
"sync"
"github.com/grafana/grafana/pkg/infra/db"
"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/setting"
"golang.org/x/sync/errgroup"
)
type Service struct {
store store
authTokenService models.ActiveTokenService
Cfg *setting.Cfg
SQLStore sqlstore.Store
Logger log.Logger
type serviceDisabled struct {
}
func ProvideService(db db.DB, cfg *setting.Cfg, tokenService models.ActiveTokenService, ss *sqlstore.SQLStore) quota.Service {
return &Service{
store: &sqlStore{db: db},
Cfg: cfg,
authTokenService: tokenService,
SQLStore: ss,
Logger: log.New("quota_service"),
func (s *serviceDisabled) QuotaReached(c *models.ReqContext, targetSrv quota.TargetSrv) (bool, error) {
return false, nil
}
func (s *serviceDisabled) GetQuotasByScope(ctx context.Context, scope quota.Scope, id int64) ([]quota.QuotaDTO, error) {
return nil, quota.ErrDisabled
}
func (s *serviceDisabled) Update(ctx context.Context, cmd *quota.UpdateQuotaCmd) error {
return quota.ErrDisabled
}
func (s *serviceDisabled) CheckQuotaReached(ctx context.Context, targetSrv quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error) {
return false, nil
}
func (s *serviceDisabled) DeleteQuotaForUser(ctx context.Context, userID int64) error {
return quota.ErrDisabled
}
func (s *serviceDisabled) RegisterQuotaReporter(e *quota.NewUsageReporter) error {
return nil
}
type service struct {
store store
Cfg *setting.Cfg
Logger log.Logger
mutex sync.RWMutex
reporters map[quota.TargetSrv]quota.UsageReporterFunc
defaultLimits *quota.Map
targetToSrv *quota.TargetToSrv
}
func ProvideService(db db.DB, cfg *setting.Cfg) quota.Service {
logger := log.New("quota_service")
s := service{
store: &sqlStore{db: db, logger: logger},
Cfg: cfg,
Logger: logger,
reporters: make(map[quota.TargetSrv]quota.UsageReporterFunc),
defaultLimits: &quota.Map{},
targetToSrv: quota.NewTargetToSrv(),
}
if s.IsDisabled() {
return &serviceDisabled{}
}
return &s
}
func (s *service) IsDisabled() bool {
return !s.Cfg.Quota.Enabled
}
// 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
}
func (s *service) QuotaReached(c *models.ReqContext, targetSrv quota.TargetSrv) (bool, error) {
// No request context means this is a background service, like LDAP Background Sync
if c == nil {
return false, nil
@ -46,91 +89,129 @@ func (s *Service) QuotaReached(c *models.ReqContext, target string) (bool, error
UserID: c.UserID,
}
}
return s.CheckQuotaReached(c.Req.Context(), target, params)
return s.CheckQuotaReached(c.Req.Context(), targetSrv, params)
}
func (s *service) GetQuotasByScope(ctx context.Context, scope quota.Scope, id int64) ([]quota.QuotaDTO, error) {
if err := scope.Validate(); err != nil {
return nil, err
}
q := make([]quota.QuotaDTO, 0)
scopeParams := quota.ScopeParameters{}
if scope == quota.OrgScope {
scopeParams.OrgID = id
} else if scope == quota.UserScope {
scopeParams.UserID = id
}
c, err := s.getContext(ctx)
if err != nil {
return nil, err
}
customLimits, err := s.store.Get(c, &scopeParams)
if err != nil {
return nil, err
}
u, err := s.getUsage(ctx, &scopeParams)
if err != nil {
return nil, err
}
for item := range s.defaultLimits.Iter() {
limit := item.Value
scp, err := item.Tag.GetScope()
if err != nil {
return nil, err
}
if scp != scope {
continue
}
if targetCustomLimit, ok := customLimits.Get(item.Tag); ok {
limit = targetCustomLimit
}
target, err := item.Tag.GetTarget()
if err != nil {
return nil, err
}
srv, err := item.Tag.GetSrv()
if err != nil {
return nil, err
}
used, _ := u.Get(item.Tag)
q = append(q, quota.QuotaDTO{
Target: string(target),
Limit: limit,
OrgId: scopeParams.OrgID,
UserId: scopeParams.UserID,
Used: used,
Service: string(srv),
Scope: string(scope),
})
}
return q, nil
}
func (s *service) Update(ctx context.Context, cmd *quota.UpdateQuotaCmd) error {
targetFound := false
knownTargets, err := s.defaultLimits.Targets()
if err != nil {
return err
}
for t := range knownTargets {
if t == quota.Target(cmd.Target) {
targetFound = true
}
}
if !targetFound {
return quota.ErrInvalidTarget.Errorf("unknown quota target: %s", cmd.Target)
}
c, err := s.getContext(ctx)
if err != nil {
return err
}
return s.store.Update(c, cmd)
}
// 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)
func (s *service) CheckQuotaReached(ctx context.Context, targetSrv quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error) {
targetSrvLimits, err := s.getOverridenLimits(ctx, targetSrv, scopeParams)
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
}
usageReporterFunc, ok := s.getReporter(targetSrv)
if !ok {
return false, quota.ErrInvalidTargetSrv
}
targetUsage, err := usageReporterFunc(ctx, scopeParams)
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
for t, limit := range targetSrvLimits {
switch {
case limit < 0:
continue
case limit == 0:
return true, nil
default:
u, ok := targetUsage.Get(t)
if !ok {
return false, fmt.Errorf("no usage for target:%s", t)
}
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 {
if u >= limit {
return true, nil
}
}
@ -138,68 +219,127 @@ func (s *Service) CheckQuotaReached(ctx context.Context, target string, scopePar
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
case "file":
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: s.Cfg.Quota.Global.File},
)
return scopes, nil
default:
return scopes, quota.ErrInvalidQuotaTarget
func (s *service) DeleteQuotaForUser(ctx context.Context, userID int64) error {
c, err := s.getContext(ctx)
if err != nil {
return err
}
return s.store.DeleteByUser(c, userID)
}
func (s *Service) DeleteByUser(ctx context.Context, userID int64) error {
return s.store.DeleteByUser(ctx, userID)
func (s *service) RegisterQuotaReporter(e *quota.NewUsageReporter) error {
s.mutex.Lock()
defer s.mutex.Unlock()
_, ok := s.reporters[e.TargetSrv]
if ok {
return quota.ErrTargetSrvConflict.Errorf("target service: %s already exists", e.TargetSrv)
}
s.reporters[e.TargetSrv] = e.Reporter
for item := range e.DefaultLimits.Iter() {
target, err := item.Tag.GetTarget()
if err != nil {
return err
}
srv, err := item.Tag.GetSrv()
if err != nil {
return err
}
s.targetToSrv.Set(target, srv)
s.defaultLimits.Set(item.Tag, item.Value)
}
return nil
}
func (s *service) getReporter(target quota.TargetSrv) (quota.UsageReporterFunc, bool) {
s.mutex.RLock()
defer s.mutex.RUnlock()
r, ok := s.reporters[target]
return r, ok
}
type reporter struct {
target quota.TargetSrv
reporterFunc quota.UsageReporterFunc
}
func (s *service) getReporters() <-chan reporter {
ch := make(chan reporter)
go func() {
s.mutex.RLock()
defer func() {
s.mutex.RUnlock()
close(ch)
}()
for t, r := range s.reporters {
ch <- reporter{target: t, reporterFunc: r}
}
}()
return ch
}
func (s *service) getOverridenLimits(ctx context.Context, targetSrv quota.TargetSrv, scopeParams *quota.ScopeParameters) (map[quota.Tag]int64, error) {
targetSrvLimits := make(map[quota.Tag]int64)
c, err := s.getContext(ctx)
if err != nil {
return nil, err
}
customLimits, err := s.store.Get(c, scopeParams)
if err != nil {
return targetSrvLimits, err
}
for item := range s.defaultLimits.Iter() {
srv, err := item.Tag.GetSrv()
if err != nil {
return nil, err
}
if srv != targetSrv {
continue
}
defaultLimit := item.Value
if customLimit, ok := customLimits.Get(item.Tag); ok {
targetSrvLimits[item.Tag] = customLimit
} else {
targetSrvLimits[item.Tag] = defaultLimit
}
}
return targetSrvLimits, nil
}
func (s *service) getUsage(ctx context.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
usage := &quota.Map{}
g, ctx := errgroup.WithContext(ctx)
for r := range s.getReporters() {
r := r
g.Go(func() error {
u, err := r.reporterFunc(ctx, scopeParams)
if err != nil {
return err
}
usage.Merge(u)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return usage, nil
}
func (s *service) getContext(ctx context.Context) (quota.Context, error) {
return quota.FromContext(ctx, s.targetToSrv), nil
}

View File

@ -3,26 +3,481 @@ package quotaimpl
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/annotations/annotationstest"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/dashboards"
dashboardStore "github.com/grafana/grafana/pkg/services/dashboards/database"
"github.com/grafana/grafana/pkg/services/datasources"
dsservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder/foldertest"
"github.com/grafana/grafana/pkg/services/ngalert"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
ngalertmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
ngalerttests "github.com/grafana/grafana/pkg/services/ngalert/tests"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/services/sqlstore"
storesrv "github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/tag/tagimpl"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/user/userimpl"
"github.com/grafana/grafana/pkg/setting"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
"github.com/xorcare/pointer"
)
func TestQuotaService(t *testing.T) {
quotaStore := &FakeQuotaStore{}
quotaService := Service{
quotaStore := &quotatest.FakeQuotaStore{}
quotaService := service{
store: quotaStore,
}
t.Run("delete quota", func(t *testing.T) {
err := quotaService.DeleteByUser(context.Background(), 1)
err := quotaService.DeleteQuotaForUser(context.Background(), 1)
require.NoError(t, err)
})
}
type FakeQuotaStore struct {
ExpectedError error
func TestIntegrationQuotaCommandsAndQueries(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := sqlstore.InitTestDB(t)
sqlStore.Cfg.Quota = setting.QuotaSettings{
Enabled: true,
Org: setting.OrgQuota{
User: 2,
Dashboard: 3,
DataSource: 4,
ApiKey: 5,
AlertRule: 6,
},
User: setting.UserQuota{
Org: 7,
},
Global: setting.GlobalQuota{
Org: 8,
User: 9,
Dashboard: 10,
DataSource: 11,
ApiKey: 12,
Session: 13,
AlertRule: 14,
File: 15,
},
}
b := bus.ProvideBus(tracing.InitializeTracerForTest())
quotaService := ProvideService(sqlStore, sqlStore.Cfg)
orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
require.NoError(t, err)
userService, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, quotaService)
require.NoError(t, err)
setupEnv(t, sqlStore, b, quotaService)
u, err := userService.Create(context.Background(), &user.CreateUserCommand{
Name: "TestUser",
SkipOrgSetup: true,
})
require.NoError(t, err)
o, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{
Name: "TestOrg",
UserID: u.ID,
})
require.NoError(t, err)
// fetch global default limit/usage
defaultGlobalLimits := make(map[quota.Tag]int64)
existingGlobalUsage := make(map[quota.Tag]int64)
scope := quota.GlobalScope
result, err := quotaService.GetQuotasByScope(context.Background(), scope, 0)
require.NoError(t, err)
for _, r := range result {
tag, err := r.Tag()
require.NoError(t, err)
defaultGlobalLimits[tag] = r.Limit
existingGlobalUsage[tag] = r.Used
}
tag, err := quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgQuotaTarget), scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.Org, defaultGlobalLimits[tag])
tag, err = quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.User, defaultGlobalLimits[tag])
tag, err = quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.Dashboard, defaultGlobalLimits[tag])
tag, err = quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.DataSource, defaultGlobalLimits[tag])
tag, err = quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.ApiKey, defaultGlobalLimits[tag])
tag, err = quota.NewTag(auth.QuotaTargetSrv, auth.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.Session, defaultGlobalLimits[tag])
tag, err = quota.NewTag(ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.AlertRule, defaultGlobalLimits[tag])
tag, err = quota.NewTag(storesrv.QuotaTargetSrv, storesrv.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Global.File, defaultGlobalLimits[tag])
// fetch default limit/usage for org
defaultOrgLimits := make(map[quota.Tag]int64)
existingOrgUsage := make(map[quota.Tag]int64)
scope = quota.OrgScope
result, err = quotaService.GetQuotasByScope(context.Background(), scope, o.ID)
require.NoError(t, err)
for _, r := range result {
tag, err := r.Tag()
require.NoError(t, err)
defaultOrgLimits[tag] = r.Limit
existingOrgUsage[tag] = r.Used
}
tag, err = quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Org.User, defaultOrgLimits[tag])
tag, err = quota.NewTag(dashboards.QuotaTargetSrv, dashboards.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Org.Dashboard, defaultOrgLimits[tag])
tag, err = quota.NewTag(datasources.QuotaTargetSrv, datasources.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Org.DataSource, defaultOrgLimits[tag])
tag, err = quota.NewTag(apikey.QuotaTargetSrv, apikey.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Org.ApiKey, defaultOrgLimits[tag])
tag, err = quota.NewTag(ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.Org.AlertRule, defaultOrgLimits[tag])
// fetch default limit/usage for user
defaultUserLimits := make(map[quota.Tag]int64)
existingUserUsage := make(map[quota.Tag]int64)
scope = quota.UserScope
result, err = quotaService.GetQuotasByScope(context.Background(), scope, u.ID)
require.NoError(t, err)
for _, r := range result {
tag, err := r.Tag()
require.NoError(t, err)
defaultUserLimits[tag] = r.Limit
existingUserUsage[tag] = r.Used
}
tag, err = quota.NewTag(quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), scope)
require.NoError(t, err)
require.Equal(t, sqlStore.Cfg.Quota.User.Org, defaultUserLimits[tag])
t.Run("Given saved org quota for users", func(t *testing.T) {
// update quota for the created org and limit users to 1
var customOrgUserLimit int64 = 1
orgCmd := quota.UpdateQuotaCmd{
OrgID: o.ID,
Target: org.OrgUserQuotaTarget,
Limit: customOrgUserLimit,
}
err := quotaService.Update(context.Background(), &orgCmd)
require.NoError(t, err)
t.Run("Should be able to get saved limit/usage for org users", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.OrgScope, &quota.ScopeParameters{OrgID: o.ID})
require.NoError(t, err)
require.Equal(t, customOrgUserLimit, q.Limit)
require.Equal(t, int64(1), q.Used)
})
t.Run("Should be able to get default org users limit/usage for unknown org", func(t *testing.T) {
unknownOrgID := -1
q, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.OrgScope, &quota.ScopeParameters{OrgID: int64(unknownOrgID)})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, defaultOrgLimits[tag], q.Limit)
require.Equal(t, int64(0), q.Used)
})
t.Run("Should be able to get zero used org alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) {
// disable Grafana Alerting
cfg := *sqlStore.Cfg
cfg.UnifiedAlerting = setting.UnifiedAlertingSettings{Enabled: pointer.Bool(false)}
quotaSrv := ProvideService(sqlStore, &cfg)
q, err := getQuotaBySrvTargetScope(t, quotaSrv, ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, quota.OrgScope, &quota.ScopeParameters{OrgID: o.ID})
require.NoError(t, err)
require.Equal(t, int64(0), q.Limit)
})
t.Run("Should be able to quota list for org", func(t *testing.T) {
result, err := quotaService.GetQuotasByScope(context.Background(), quota.OrgScope, o.ID)
require.NoError(t, err)
require.Len(t, result, 5)
require.NoError(t, err)
for _, res := range result {
tag, err := res.Tag()
require.NoError(t, err)
limit := defaultOrgLimits[tag]
used := existingOrgUsage[tag]
if res.Target == org.OrgUserQuotaTarget {
limit = customOrgUserLimit
used = 1 // one user in the created org
}
require.Equal(t, limit, res.Limit)
require.Equal(t, used, res.Used)
}
})
})
t.Run("Given saved org quota for dashboards", func(t *testing.T) {
// update quota for the created org and limit dashboards to 1
var customOrgDashboardLimit int64 = 1
orgCmd := quota.UpdateQuotaCmd{
OrgID: o.ID,
Target: string(dashboards.QuotaTarget),
Limit: customOrgDashboardLimit,
}
err := quotaService.Update(context.Background(), &orgCmd)
require.NoError(t, err)
t.Run("Should be able to get saved quota by org id and target", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.OrgScope, &quota.ScopeParameters{OrgID: o.ID})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, customOrgDashboardLimit, q.Limit)
require.Equal(t, existingOrgUsage[tag], q.Used)
})
})
t.Run("Given saved user quota for org", func(t *testing.T) {
// update quota for the created user and limit orgs to 1
var customUserOrgsLimit int64 = 1
userQuotaCmd := quota.UpdateQuotaCmd{
UserID: u.ID,
Target: org.OrgUserQuotaTarget,
Limit: customUserOrgsLimit,
}
err := quotaService.Update(context.Background(), &userQuotaCmd)
require.NoError(t, err)
t.Run("Should be able to get saved limit/usage for user orgs", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.UserScope, &quota.ScopeParameters{UserID: u.ID})
require.NoError(t, err)
require.Equal(t, customUserOrgsLimit, q.Limit)
require.Equal(t, int64(1), q.Used)
})
t.Run("Should be able to get default user orgs limit/usage for unknown user", func(t *testing.T) {
var unknownUserID int64 = -1
q, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.UserScope, &quota.ScopeParameters{UserID: unknownUserID})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, defaultUserLimits[tag], q.Limit)
require.Equal(t, int64(0), q.Used)
})
t.Run("Should be able to quota list for user", func(t *testing.T) {
result, err = quotaService.GetQuotasByScope(context.Background(), quota.UserScope, u.ID)
require.NoError(t, err)
require.Len(t, result, 1)
for _, res := range result {
tag, err := res.Tag()
require.NoError(t, err)
limit := defaultUserLimits[tag]
used := existingUserUsage[tag]
if res.Target == org.OrgUserQuotaTarget {
limit = customUserOrgsLimit // customized quota limit.
used = 1 // one user in the created org
}
require.Equal(t, limit, res.Limit)
require.Equal(t, used, res.Used)
}
})
})
t.Run("Should be able to global user quota", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope, &quota.ScopeParameters{})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, defaultGlobalLimits[tag], q.Limit)
require.Equal(t, int64(1), q.Used)
})
t.Run("Should be able to global org quota", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgQuotaTarget), quota.GlobalScope, &quota.ScopeParameters{})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, defaultGlobalLimits[tag], q.Limit)
require.Equal(t, int64(1), q.Used)
})
t.Run("Should be able to get zero used global alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, ngalertmodels.QuotaTargetSrv, ngalertmodels.QuotaTarget, quota.GlobalScope, &quota.ScopeParameters{})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, defaultGlobalLimits[tag], q.Limit)
require.Equal(t, int64(0), q.Used)
})
t.Run("Should be able to global dashboard quota", func(t *testing.T) {
q, err := getQuotaBySrvTargetScope(t, quotaService, dashboards.QuotaTargetSrv, dashboards.QuotaTarget, quota.GlobalScope, &quota.ScopeParameters{})
require.NoError(t, err)
tag, err := q.Tag()
require.NoError(t, err)
require.Equal(t, defaultGlobalLimits[tag], q.Limit)
require.Equal(t, int64(0), q.Used)
})
// related: https://github.com/grafana/grafana/issues/14342
t.Run("Should org quota updating is successful even if it called multiple time", func(t *testing.T) {
// update quota for the created org and limit users to 1
var customOrgUserLimit int64 = 1
orgCmd := quota.UpdateQuotaCmd{
OrgID: o.ID,
Target: org.OrgUserQuotaTarget,
Limit: customOrgUserLimit,
}
err := quotaService.Update(context.Background(), &orgCmd)
require.NoError(t, err)
query, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.OrgScope, &quota.ScopeParameters{OrgID: o.ID})
require.NoError(t, err)
require.Equal(t, customOrgUserLimit, query.Limit)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
customOrgUserLimit = 2
orgCmd = quota.UpdateQuotaCmd{
OrgID: o.ID,
Target: org.OrgUserQuotaTarget,
Limit: customOrgUserLimit,
}
err = quotaService.Update(context.Background(), &orgCmd)
require.NoError(t, err)
query, err = getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.OrgScope, &quota.ScopeParameters{OrgID: o.ID})
require.NoError(t, err)
require.Equal(t, customOrgUserLimit, query.Limit)
})
// related: https://github.com/grafana/grafana/issues/14342
t.Run("Should user quota updating is successful even if it called multiple time", func(t *testing.T) {
// update quota for the created org and limit users to 1
var customUserOrgLimit int64 = 1
userQuotaCmd := quota.UpdateQuotaCmd{
UserID: u.ID,
Target: org.OrgUserQuotaTarget,
Limit: customUserOrgLimit,
}
err := quotaService.Update(context.Background(), &userQuotaCmd)
require.NoError(t, err)
query, err := getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.UserScope, &quota.ScopeParameters{UserID: u.ID})
require.NoError(t, err)
require.Equal(t, customUserOrgLimit, query.Limit)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
customUserOrgLimit = 10
userQuotaCmd = quota.UpdateQuotaCmd{
UserID: u.ID,
Target: org.OrgUserQuotaTarget,
Limit: customUserOrgLimit,
}
err = quotaService.Update(context.Background(), &userQuotaCmd)
require.NoError(t, err)
query, err = getQuotaBySrvTargetScope(t, quotaService, quota.TargetSrv(org.QuotaTargetSrv), quota.Target(org.OrgUserQuotaTarget), quota.UserScope, &quota.ScopeParameters{UserID: u.ID})
require.NoError(t, err)
require.Equal(t, customUserOrgLimit, query.Limit)
})
// TODO data_source, file
}
func (f *FakeQuotaStore) DeleteByUser(ctx context.Context, userID int64) error {
return f.ExpectedError
func getQuotaBySrvTargetScope(t *testing.T, quotaService quota.Service, srv quota.TargetSrv, target quota.Target, scope quota.Scope, scopeParams *quota.ScopeParameters) (quota.QuotaDTO, error) {
t.Helper()
var id int64 = 0
switch {
case scope == quota.OrgScope:
id = scopeParams.OrgID
case scope == quota.UserScope:
id = scopeParams.UserID
}
result, err := quotaService.GetQuotasByScope(context.Background(), scope, id)
require.NoError(t, err)
for _, r := range result {
if r.Target != string(target) {
continue
}
if r.Service != string(srv) {
continue
}
if r.Scope != string(scope) {
continue
}
require.Equal(t, r.OrgId, scopeParams.OrgID)
require.Equal(t, r.UserId, scopeParams.UserID)
return r, nil
}
return quota.QuotaDTO{}, err
}
func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService quota.Service) {
_, err := apikeyimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
require.NoError(t, err)
_, err = auth.ProvideActiveAuthTokenService(sqlStore.Cfg, sqlStore, quotaService)
require.NoError(t, err)
_, err = dashboardStore.ProvideDashboardStore(sqlStore, sqlStore.Cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore, sqlStore.Cfg), quotaService)
require.NoError(t, err)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
_, err = dsservice.ProvideService(sqlStore, secretsService, secretsStore, sqlStore.Cfg, featuremgmt.WithFeatures(), acmock.New().WithDisabled(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
m := metrics.NewNGAlert(prometheus.NewRegistry())
_, err = ngalert.ProvideService(
sqlStore.Cfg, &ngalerttests.FakeFeatures{}, nil, nil, routing.NewRouteRegister(), sqlStore, nil, nil, nil, quotaService,
secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, annotationstest.NewFakeAnnotationsRepo(),
)
require.NoError(t, err)
_, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), sqlStore.Cfg, quotaService)
require.NoError(t, err)
}

View File

@ -1,23 +1,130 @@
package quotaimpl
import (
"context"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
type store interface {
DeleteByUser(context.Context, int64) error
Get(ctx quota.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error)
Update(ctx quota.Context, cmd *quota.UpdateQuotaCmd) error
DeleteByUser(quota.Context, int64) error
}
type sqlStore struct {
db db.DB
db db.DB
logger log.Logger
}
func (ss *sqlStore) DeleteByUser(ctx context.Context, userID int64) error {
func (ss *sqlStore) DeleteByUser(ctx quota.Context, userID int64) error {
return ss.db.WithDbSession(ctx, func(sess *db.Session) error {
var rawSQL = "DELETE FROM quota WHERE user_id = ?"
_, err := sess.Exec(rawSQL, userID)
return err
})
}
func (ss *sqlStore) Get(ctx quota.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
limits := quota.Map{}
if scopeParams.OrgID != 0 {
orgLimits, err := ss.getOrgScopeQuota(ctx, scopeParams.OrgID)
if err != nil {
return nil, err
}
limits.Merge(orgLimits)
}
if scopeParams.UserID != 0 {
userLimits, err := ss.getUserScopeQuota(ctx, scopeParams.UserID)
if err != nil {
return nil, err
}
limits.Merge(userLimits)
}
return &limits, nil
}
func (ss *sqlStore) Update(ctx quota.Context, cmd *quota.UpdateQuotaCmd) error {
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
// Check if quota is already defined in the DB
quota := quota.Quota{
Target: cmd.Target,
UserId: cmd.UserID,
OrgId: cmd.OrgID,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()
// No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
return err
}
} else {
// update existing quota entry in the DB.
_, err := sess.ID(quota.Id).Update(&quota)
if err != nil {
return err
}
}
return nil
})
}
func (ss *sqlStore) getUserScopeQuota(ctx quota.Context, userID int64) (*quota.Map, error) {
r := quota.Map{}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
quotas := make([]*quota.Quota, 0)
if err := sess.Table("quota").Where("user_id=? AND org_id=0", userID).Find(&quotas); err != nil {
return err
}
for _, q := range quotas {
srv, ok := ctx.TargetToSrv.Get(quota.Target(q.Target))
if !ok {
ss.logger.Info("failed to get service for target", "target", q.Target)
}
tag, err := quota.NewTag(srv, quota.Target(q.Target), quota.UserScope)
if err != nil {
return err
}
r.Set(tag, q.Limit)
}
return nil
})
return &r, err
}
func (ss *sqlStore) getOrgScopeQuota(ctx quota.Context, OrgID int64) (*quota.Map, error) {
r := quota.Map{}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
quotas := make([]*quota.Quota, 0)
if err := sess.Table("quota").Where("user_id=0 AND org_id=?", OrgID).Find(&quotas); err != nil {
return err
}
for _, q := range quotas {
srv, ok := ctx.TargetToSrv.Get(quota.Target(q.Target))
if !ok {
ss.logger.Info("failed to get service for target", "target", q.Target)
}
tag, err := quota.NewTag(srv, quota.Target(q.Target), quota.OrgScope)
if err != nil {
return err
}
r.Set(tag, q.Limit)
}
return nil
})
return &r, err
}

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/quota"
)
func TestIntegrationQuotaDataAccess(t *testing.T) {
@ -20,7 +21,8 @@ func TestIntegrationQuotaDataAccess(t *testing.T) {
}
t.Run("quota deleted", func(t *testing.T) {
err := quotaStore.DeleteByUser(context.Background(), 1)
ctx := quota.FromContext(context.Background(), &quota.TargetToSrv{})
err := quotaStore.DeleteByUser(ctx, 1)
require.NoError(t, err)
})
}

View File

@ -12,18 +12,46 @@ type FakeQuotaService struct {
err error
}
func NewQuotaServiceFake() *FakeQuotaService {
return &FakeQuotaService{}
func New(reached bool, err error) *FakeQuotaService {
return &FakeQuotaService{reached, err}
}
func (f *FakeQuotaService) QuotaReached(c *models.ReqContext, target string) (bool, error) {
func (f *FakeQuotaService) GetQuotasByScope(ctx context.Context, scope quota.Scope, id int64) ([]quota.QuotaDTO, error) {
return []quota.QuotaDTO{}, nil
}
func (f *FakeQuotaService) Update(ctx context.Context, cmd *quota.UpdateQuotaCmd) error {
return nil
}
func (f *FakeQuotaService) QuotaReached(c *models.ReqContext, target quota.TargetSrv) (bool, error) {
return f.reached, f.err
}
func (f *FakeQuotaService) CheckQuotaReached(c context.Context, target string, params *quota.ScopeParameters) (bool, error) {
func (f *FakeQuotaService) CheckQuotaReached(c context.Context, target quota.TargetSrv, params *quota.ScopeParameters) (bool, error) {
return f.reached, f.err
}
func (f *FakeQuotaService) DeleteByUser(c context.Context, userID int64) error {
func (f *FakeQuotaService) DeleteQuotaForUser(c context.Context, userID int64) error {
return f.err
}
func (f *FakeQuotaService) RegisterQuotaReporter(e *quota.NewUsageReporter) error {
return f.err
}
type FakeQuotaStore struct {
ExpectedError error
}
func (f *FakeQuotaStore) DeleteByUser(ctx quota.Context, userID int64) error {
return f.ExpectedError
}
func (f *FakeQuotaStore) Get(ctx quota.Context, scopeParams *quota.ScopeParameters) (*quota.Map, error) {
return nil, f.ExpectedError
}
func (f *FakeQuotaStore) Update(ctx quota.Context, cmd *quota.UpdateQuotaCmd) error {
return f.ExpectedError
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
@ -13,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
dsservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
@ -27,7 +29,9 @@ func SetupTestDataSourceSecretMigrationService(t *testing.T, sqlStore db.DB, kvS
features = featuremgmt.WithFeatures(featuremgmt.FlagDisableSecretsCompatibility, true)
}
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New().WithDisabled(), acmock.NewMockedPermissionsService())
quotaService := quotatest.New(false, nil)
dsService, err := dsservice.ProvideService(sqlStore, secretsService, secretsStore, cfg, features, acmock.New().WithDisabled(), acmock.NewMockedPermissionsService(), quotaService)
require.NoError(t, err)
migService := ProvideDataSourceMigrationService(dsService, kvStore, features)
return migService
}

View File

@ -27,6 +27,7 @@ import (
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
@ -44,9 +45,12 @@ var (
func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
store := db.InitTestDB(t)
apiKeyService := apikeyimpl.ProvideService(store, store.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService)
require.NoError(t, err)
kvStore := kvstore.ProvideService(store)
orgService := orgimpl.ProvideService(store, setting.NewCfg())
orgService, err := orgimpl.ProvideService(store, setting.NewCfg(), quotaService)
require.NoError(t, err)
saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, orgService)
svcmock := tests.ServiceAccountMock{}
@ -57,7 +61,7 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
}()
orgCmd := &models.CreateOrgCommand{Name: "Some Test Org"}
err := store.CreateOrg(context.Background(), orgCmd)
err = store.CreateOrg(context.Background(), orgCmd)
require.Nil(t, err)
type testCreateSATestCase struct {
@ -212,7 +216,9 @@ func TestServiceAccountsAPI_CreateServiceAccount(t *testing.T) {
func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
store := db.InitTestDB(t)
kvStore := kvstore.ProvideService(store)
apiKeyService := apikeyimpl.ProvideService(store, store.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService)
require.NoError(t, err)
saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil)
svcmock := tests.ServiceAccountMock{}
@ -284,7 +290,9 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
sqlStore db.DB, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) {
cfg := setting.NewCfg()
teamSvc := teamimpl.ProvideService(sqlStore, cfg)
userSvc := userimpl.ProvideService(sqlStore, nil, cfg, teamimpl.ProvideService(sqlStore, cfg), nil)
userSvc, err := userimpl.ProvideService(sqlStore, nil, cfg, teamimpl.ProvideService(sqlStore, cfg), nil, quotatest.New(false, nil))
require.NoError(t, err)
saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(
cfg, routing.NewRouteRegister(), sqlStore, acmock, &licensing.OSSLicensingService{}, saStore, acmock, teamSvc, userSvc)
require.NoError(t, err)
@ -316,7 +324,9 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock,
func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
store := db.InitTestDB(t)
apiKeyService := apikeyimpl.ProvideService(store, store.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService)
require.NoError(t, err)
kvStore := kvstore.ProvideService(store)
saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil)
svcmock := tests.ServiceAccountMock{}
@ -408,7 +418,9 @@ func newString(s string) *string {
func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) {
store := db.InitTestDB(t)
apiKeyService := apikeyimpl.ProvideService(store, store.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService)
require.NoError(t, err)
kvStore := kvstore.ProvideService(store)
saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil)
svcmock := tests.ServiceAccountMock{}

View File

@ -23,6 +23,7 @@ import (
accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
@ -54,7 +55,9 @@ func createTokenforSA(t *testing.T, store serviceaccounts.Store, keyName string,
func TestServiceAccountsAPI_CreateToken(t *testing.T) {
store := db.InitTestDB(t)
apiKeyService := apikeyimpl.ProvideService(store, store.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService)
require.NoError(t, err)
kvStore := kvstore.ProvideService(store)
saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil)
svcmock := tests.ServiceAccountMock{}
@ -171,7 +174,9 @@ func TestServiceAccountsAPI_CreateToken(t *testing.T) {
func TestServiceAccountsAPI_DeleteToken(t *testing.T) {
store := db.InitTestDB(t)
apiKeyService := apikeyimpl.ProvideService(store, store.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(store, store.Cfg, quotaService)
require.NoError(t, err)
kvStore := kvstore.ProvideService(store)
svcMock := &tests.ServiceAccountMock{}
saStore := database.ProvideServiceAccountsStore(store, apiKeyService, kvStore, nil)

View File

@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -112,9 +113,12 @@ func TestStore_DeleteServiceAccount(t *testing.T) {
func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreImpl) {
t.Helper()
db := db.InitTestDB(t)
apiKeyService := apikeyimpl.ProvideService(db, db.Cfg)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(db, db.Cfg, quotaService)
require.NoError(t, err)
kvStore := kvstore.ProvideService(db)
orgService := orgimpl.ProvideService(db, setting.NewCfg())
orgService, err := orgimpl.ProvideService(db, setting.NewCfg(), quotaService)
require.NoError(t, err)
return db, ProvideServiceAccountsStore(db, apiKeyService, kvStore, orgService)
}

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
@ -70,8 +71,10 @@ func SetupApiKey(t *testing.T, sqlStore *sqlstore.SQLStore, testKey TestApiKey)
addKeyCmd.Key = "secret"
}
apiKeyService := apikeyimpl.ProvideService(sqlStore, sqlStore.Cfg)
err := apiKeyService.AddAPIKey(context.Background(), addKeyCmd)
quotaService := quotatest.New(false, nil)
apiKeyService, err := apikeyimpl.ProvideService(sqlStore, sqlStore.Cfg, quotaService)
require.NoError(t, err)
err = apiKeyService.AddAPIKey(context.Background(), addKeyCmd)
require.NoError(t, err)
if testKey.IsExpired {

View File

@ -98,34 +98,6 @@ func (m *SQLStoreMock) WithNewDbSession(ctx context.Context, callback sqlstore.D
return m.ExpectedError
}
func (m *SQLStoreMock) GetOrgQuotaByTarget(ctx context.Context, query *models.GetOrgQuotaByTargetQuery) error {
return m.ExpectedError
}
func (m *SQLStoreMock) GetOrgQuotas(ctx context.Context, query *models.GetOrgQuotasQuery) error {
return m.ExpectedError
}
func (m *SQLStoreMock) UpdateOrgQuota(ctx context.Context, cmd *models.UpdateOrgQuotaCmd) error {
return m.ExpectedError
}
func (m *SQLStoreMock) GetUserQuotaByTarget(ctx context.Context, query *models.GetUserQuotaByTargetQuery) error {
return m.ExpectedError
}
func (m *SQLStoreMock) GetUserQuotas(ctx context.Context, query *models.GetUserQuotasQuery) error {
return m.ExpectedError
}
func (m *SQLStoreMock) UpdateUserQuota(ctx context.Context, cmd *models.UpdateUserQuotaCmd) error {
return m.ExpectedError
}
func (m *SQLStoreMock) GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error {
return m.ExpectedError
}
func (m *SQLStoreMock) WithTransactionalDbSession(ctx context.Context, callback sqlstore.DBTransactionFunc) error {
return m.ExpectedError
}

View File

@ -1,315 +0,0 @@
package sqlstore
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
)
const (
alertRuleTarget = "alert_rule"
dashboardTarget = "dashboard"
filesTarget = "file"
)
type targetCount struct {
Count int64
}
func (ss *SQLStore) GetOrgQuotaByTarget(ctx context.Context, query *models.GetOrgQuotaByTargetQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
quota := models.Quota{
Target: query.Target,
OrgId: query.OrgId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
} else if !has {
quota.Limit = query.Default
}
var used int64
if query.Target != alertRuleTarget || query.UnifiedAlertingEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s WHERE org_id=?",
dialect.Quote(query.Target))
if query.Target == dashboardTarget {
rawSQL += fmt.Sprintf(" AND is_folder=%s", dialect.BooleanStr(false))
}
// need to account for removing service accounts from the user table
if query.Target == "org_user" {
rawSQL = fmt.Sprintf("SELECT COUNT(*) as count from (select user_id from %s where org_id=? AND user_id IN (SELECT id as user_id FROM %s WHERE is_service_account=%s)) as subq",
dialect.Quote(query.Target),
dialect.Quote("user"),
dialect.BooleanStr(false),
)
}
resp := make([]*targetCount, 0)
if err := sess.SQL(rawSQL, query.OrgId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
query.Result = &models.OrgQuotaDTO{
Target: query.Target,
Limit: quota.Limit,
OrgId: query.OrgId,
Used: used,
}
return nil
})
}
func (ss *SQLStore) GetOrgQuotas(ctx context.Context, query *models.GetOrgQuotasQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
quotas := make([]*models.Quota, 0)
if err := sess.Table("quota").Where("org_id=? AND user_id=0", query.OrgId).Find(&quotas); err != nil {
return err
}
defaultQuotas := setting.Quota.Org.ToMap()
seenTargets := make(map[string]bool)
for _, q := range quotas {
seenTargets[q.Target] = true
}
for t, v := range defaultQuotas {
if _, ok := seenTargets[t]; !ok {
quotas = append(quotas, &models.Quota{
OrgId: query.OrgId,
Target: t,
Limit: v,
})
}
}
result := make([]*models.OrgQuotaDTO, len(quotas))
for i, q := range quotas {
var used int64
var rawSQL string
if q.Target != alertRuleTarget || query.UnifiedAlertingEnabled {
// get quota used.
rawSQL = fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target))
// need to account for removing service accounts from the user table
if q.Target == "org_user" {
rawSQL = fmt.Sprintf("SELECT COUNT(*) as count from (select user_id from %s where org_id=? AND user_id IN (SELECT id as user_id FROM %s WHERE is_service_account=%s)) as subq",
dialect.Quote(q.Target),
dialect.Quote("user"),
dialect.BooleanStr(false),
)
}
resp := make([]*targetCount, 0)
if err := sess.SQL(rawSQL, q.OrgId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
result[i] = &models.OrgQuotaDTO{
Target: q.Target,
Limit: q.Limit,
OrgId: q.OrgId,
Used: used,
}
}
query.Result = result
return nil
})
}
func (ss *SQLStore) UpdateOrgQuota(ctx context.Context, cmd *models.UpdateOrgQuotaCmd) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
// Check if quota is already defined in the DB
quota := models.Quota{
Target: cmd.Target,
OrgId: cmd.OrgId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()
// No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
return err
}
} else {
// update existing quota entry in the DB.
_, err := sess.ID(quota.Id).Update(&quota)
if err != nil {
return err
}
}
return nil
})
}
func (ss *SQLStore) GetUserQuotaByTarget(ctx context.Context, query *models.GetUserQuotaByTargetQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
quota := models.Quota{
Target: query.Target,
UserId: query.UserId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
} else if !has {
quota.Limit = query.Default
}
var used int64
if query.Target != alertRuleTarget || query.UnifiedAlertingEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
if err := sess.SQL(rawSQL, query.UserId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
query.Result = &models.UserQuotaDTO{
Target: query.Target,
Limit: quota.Limit,
UserId: query.UserId,
Used: used,
}
return nil
})
}
func (ss *SQLStore) GetUserQuotas(ctx context.Context, query *models.GetUserQuotasQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
quotas := make([]*models.Quota, 0)
if err := sess.Table("quota").Where("user_id=? AND org_id=0", query.UserId).Find(&quotas); err != nil {
return err
}
defaultQuotas := setting.Quota.User.ToMap()
seenTargets := make(map[string]bool)
for _, q := range quotas {
seenTargets[q.Target] = true
}
for t, v := range defaultQuotas {
if _, ok := seenTargets[t]; !ok {
quotas = append(quotas, &models.Quota{
UserId: query.UserId,
Target: t,
Limit: v,
})
}
}
result := make([]*models.UserQuotaDTO, len(quotas))
for i, q := range quotas {
var used int64
if q.Target != alertRuleTarget || query.UnifiedAlertingEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target))
resp := make([]*targetCount, 0)
if err := sess.SQL(rawSQL, q.UserId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
result[i] = &models.UserQuotaDTO{
Target: q.Target,
Limit: q.Limit,
UserId: q.UserId,
Used: used,
}
}
query.Result = result
return nil
})
}
func (ss *SQLStore) UpdateUserQuota(ctx context.Context, cmd *models.UpdateUserQuotaCmd) error {
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
// Check if quota is already defined in the DB
quota := models.Quota{
Target: cmd.Target,
UserId: cmd.UserId,
}
has, err := sess.Get(&quota)
if err != nil {
return err
}
quota.Updated = time.Now()
quota.Limit = cmd.Limit
if !has {
quota.Created = time.Now()
// No quota in the DB for this target, so create a new one.
if _, err := sess.Insert(&quota); err != nil {
return err
}
} else {
// update existing quota entry in the DB.
_, err := sess.ID(quota.Id).Update(&quota)
if err != nil {
return err
}
}
return nil
})
}
func (ss *SQLStore) GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error {
return ss.WithDbSession(ctx, func(sess *DBSession) error {
var used int64
if query.Target == filesTarget {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s",
dialect.Quote("file"))
notFolderCondition := fmt.Sprintf(" WHERE path NOT LIKE '%s'", "%/")
resp := make([]*targetCount, 0)
if err := sess.SQL(rawSQL + notFolderCondition).Find(&resp); err != nil {
return err
}
used = resp[0].Count
} else if query.Target != alertRuleTarget || query.UnifiedAlertingEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM %s",
dialect.Quote(query.Target))
if query.Target == dashboardTarget {
rawSQL += fmt.Sprintf(" WHERE is_folder=%s", dialect.BooleanStr(false))
}
// removing service accounts from count
if query.Target == dialect.Quote("user") {
rawSQL += fmt.Sprintf(" WHERE is_service_account=%s", dialect.BooleanStr(false))
}
resp := make([]*targetCount, 0)
if err := sess.SQL(rawSQL).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: used,
}
return nil
})
}

View File

@ -1,301 +0,0 @@
package sqlstore
import (
"context"
"testing"
"time"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
)
func TestIntegrationQuotaCommandsAndQueries(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
sqlStore := InitTestDB(t)
userId := int64(1)
orgId := int64(0)
setting.Quota = setting.QuotaSettings{
Enabled: true,
Org: &setting.OrgQuota{
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
AlertRule: 5,
},
User: &setting.UserQuota{
Org: 5,
},
Global: &setting.GlobalQuota{
Org: 5,
User: 5,
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
Session: 5,
AlertRule: 5,
},
}
createUserCmd := user.CreateUserCommand{
Name: "TestUser",
OrgID: orgId,
SkipOrgSetup: true,
}
user, err := sqlStore.CreateUser(context.Background(), createUserCmd)
require.NoError(t, err)
// create a new org and add user_id 1 as admin.
// we will then have an org with 1 user. and a user
// with 1 org.
userCmd := models.CreateOrgCommand{
Name: "TestOrg",
UserId: user.ID,
}
err = sqlStore.CreateOrg(context.Background(), &userCmd)
require.NoError(t, err)
orgId = userCmd.Result.Id
t.Run("Given saved org quota for users", func(t *testing.T) {
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 10,
}
err := sqlStore.UpdateOrgQuota(context.Background(), &orgCmd)
require.NoError(t, err)
t.Run("Should be able to get saved quota by org id and target", func(t *testing.T) {
query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(10), query.Result.Limit)
})
t.Run("Should be able to get default quota by org id and target", func(t *testing.T) {
query := models.GetOrgQuotaByTargetQuery{OrgId: 123, Target: "org_user", Default: 11}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(11), query.Result.Limit)
})
t.Run("Should be able to get used org quota when rows exist", func(t *testing.T) {
query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 11}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(1), query.Result.Used)
})
t.Run("Should be able to get used org quota when no rows exist", func(t *testing.T) {
query := models.GetOrgQuotaByTargetQuery{OrgId: 2, Target: "org_user", Default: 11}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(0), query.Result.Used)
})
t.Run("Should be able to get zero used org alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) {
query := models.GetOrgQuotaByTargetQuery{OrgId: 2, Target: "alert", Default: 11}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(0), query.Result.Used)
})
t.Run("Should be able to quota list for org", func(t *testing.T) {
query := models.GetOrgQuotasQuery{OrgId: orgId}
err = sqlStore.GetOrgQuotas(context.Background(), &query)
require.NoError(t, err)
require.Len(t, query.Result, 5)
for _, res := range query.Result {
limit := int64(5) // default quota limit
used := int64(0)
if res.Target == "org_user" {
limit = 10 // customized quota limit.
used = 1
}
require.Equal(t, limit, res.Limit)
require.Equal(t, used, res.Used)
}
})
})
t.Run("Given saved org quota for dashboards", func(t *testing.T) {
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: dashboardTarget,
Limit: 10,
}
err := sqlStore.UpdateOrgQuota(context.Background(), &orgCmd)
require.NoError(t, err)
t.Run("Should be able to get saved quota by org id and target", func(t *testing.T) {
query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: dashboardTarget, Default: 1}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(10), query.Result.Limit)
require.Equal(t, int64(0), query.Result.Used)
})
})
t.Run("Given saved user quota for org", func(t *testing.T) {
userQuotaCmd := models.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 10,
}
err := sqlStore.UpdateUserQuota(context.Background(), &userQuotaCmd)
require.NoError(t, err)
t.Run("Should be able to get saved quota by user id and target", func(t *testing.T) {
query := models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = sqlStore.GetUserQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(10), query.Result.Limit)
})
t.Run("Should be able to get default quota by user id and target", func(t *testing.T) {
query := models.GetUserQuotaByTargetQuery{UserId: 9, Target: "org_user", Default: 11}
err = sqlStore.GetUserQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(11), query.Result.Limit)
})
t.Run("Should be able to get used user quota when rows exist", func(t *testing.T) {
query := models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 11}
err = sqlStore.GetUserQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(1), query.Result.Used)
})
t.Run("Should be able to get used user quota when no rows exist", func(t *testing.T) {
query := models.GetUserQuotaByTargetQuery{UserId: 2, Target: "org_user", Default: 11}
err = sqlStore.GetUserQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(0), query.Result.Used)
})
t.Run("Should be able to quota list for user", func(t *testing.T) {
query := models.GetUserQuotasQuery{UserId: userId}
err = sqlStore.GetUserQuotas(context.Background(), &query)
require.NoError(t, err)
require.Len(t, query.Result, 1)
require.Equal(t, int64(10), query.Result[0].Limit)
require.Equal(t, int64(1), query.Result[0].Used)
})
})
t.Run("Should be able to global user quota", func(t *testing.T) {
query := models.GetGlobalQuotaByTargetQuery{Target: "user", Default: 5}
err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(5), query.Result.Limit)
require.Equal(t, int64(1), query.Result.Used)
})
t.Run("Should be able to global org quota", func(t *testing.T) {
query := models.GetGlobalQuotaByTargetQuery{Target: "org", Default: 5}
err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(5), query.Result.Limit)
require.Equal(t, int64(1), query.Result.Used)
})
t.Run("Should be able to get zero used global alert quota when table does not exist (ngalert is not enabled - default case)", func(t *testing.T) {
query := models.GetGlobalQuotaByTargetQuery{Target: "alert_rule", Default: 5}
err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(5), query.Result.Limit)
require.Equal(t, int64(0), query.Result.Used)
})
t.Run("Should be able to global dashboard quota", func(t *testing.T) {
query := models.GetGlobalQuotaByTargetQuery{Target: dashboardTarget, Default: 5}
err = sqlStore.GetGlobalQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(5), query.Result.Limit)
require.Equal(t, int64(0), query.Result.Used)
})
// related: https://github.com/grafana/grafana/issues/14342
t.Run("Should org quota updating is successful even if it called multiple time", func(t *testing.T) {
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 5,
}
err := sqlStore.UpdateOrgQuota(context.Background(), &orgCmd)
require.NoError(t, err)
query := models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(5), query.Result.Limit)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
orgCmd = models.UpdateOrgQuotaCmd{
OrgId: orgId,
Target: "org_user",
Limit: 10,
}
err = sqlStore.UpdateOrgQuota(context.Background(), &orgCmd)
require.NoError(t, err)
query = models.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1}
err = sqlStore.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(10), query.Result.Limit)
})
// related: https://github.com/grafana/grafana/issues/14342
t.Run("Should user quota updating is successful even if it called multiple time", func(t *testing.T) {
userQuotaCmd := models.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 5,
}
err := sqlStore.UpdateUserQuota(context.Background(), &userQuotaCmd)
require.NoError(t, err)
query := models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = sqlStore.GetUserQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(5), query.Result.Limit)
// XXX: resolution of `Updated` column is 1sec, so this makes delay
time.Sleep(1 * time.Second)
userQuotaCmd = models.UpdateUserQuotaCmd{
UserId: userId,
Target: "org_user",
Limit: 10,
}
err = sqlStore.UpdateUserQuota(context.Background(), &userQuotaCmd)
require.NoError(t, err)
query = models.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1}
err = sqlStore.GetUserQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, int64(10), query.Result.Limit)
})
}

View File

@ -23,13 +23,6 @@ type Store interface {
GetSignedInUser(ctx context.Context, query *models.GetSignedInUserQuery) error
WithDbSession(ctx context.Context, callback DBTransactionFunc) error
WithNewDbSession(ctx context.Context, callback DBTransactionFunc) error
GetOrgQuotaByTarget(ctx context.Context, query *models.GetOrgQuotaByTargetQuery) error
GetOrgQuotas(ctx context.Context, query *models.GetOrgQuotasQuery) error
UpdateOrgQuota(ctx context.Context, cmd *models.UpdateOrgQuotaCmd) error
GetUserQuotaByTarget(ctx context.Context, query *models.GetUserQuotaByTargetQuery) error
GetUserQuotas(ctx context.Context, query *models.GetUserQuotasQuery) error
UpdateUserQuota(ctx context.Context, cmd *models.UpdateUserQuotaCmd) error
GetGlobalQuotaByTarget(ctx context.Context, query *models.GetGlobalQuotaByTargetQuery) error
WithTransactionalDbSession(ctx context.Context, callback DBTransactionFunc) error
InTransaction(ctx context.Context, fn func(ctx context.Context) error) error
Migrate(bool) error

View File

@ -18,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@ -58,6 +59,11 @@ type CreateFolderCmd struct {
Path string `json:"path"`
}
const (
QuotaTargetSrv quota.TargetSrv = "store"
QuotaTarget quota.Target = "file"
)
type StorageService interface {
registry.BackgroundService
@ -97,7 +103,7 @@ func ProvideService(
features featuremgmt.FeatureToggles,
cfg *setting.Cfg,
quotaService quota.Service,
) StorageService {
) (StorageService, error) {
settings, err := LoadStorageConfig(cfg, features)
if err != nil {
grafanaStorageLogger.Warn("error loading storage config", "error", err)
@ -259,7 +265,37 @@ func ProvideService(
s := newStandardStorageService(sql, globalRoots, initializeOrgStorages, authService, cfg)
s.quotaService = quotaService
s.cfg = settings
return s
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return nil, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: QuotaTargetSrv,
DefaultLimits: defaultLimits,
Reporter: s.Usage,
}); err != nil {
return nil, err
}
return s, nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.File)
return limits, nil
}
func createSystemBrandingPathFilter() filestorage.PathFilter {
@ -329,6 +365,32 @@ func (s *standardStorageService) Read(ctx context.Context, user *user.SignedInUs
return s.tree.GetFile(ctx, getOrgId(user), path)
}
func (s *standardStorageService) Usage(ctx context.Context, ScopeParameters *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
type result struct {
Count int64
}
r := result{}
rawSQL := fmt.Sprintf("SELECT COUNT(*) AS count FROM file WHERE path NOT LIKE '%s'", "%/")
if _, err := sess.SQL(rawSQL).Get(&r); err != nil {
return err
}
tag, err := quota.NewTag(QuotaTargetSrv, QuotaTarget, quota.GlobalScope)
if err != nil {
return err
}
u.Set(tag, r.Count)
return nil
})
return u, err
}
type UploadRequest struct {
Contents []byte
Path string
@ -395,7 +457,7 @@ func (s *standardStorageService) Upload(ctx context.Context, user *user.SignedIn
func (s *standardStorageService) checkFileQuota(ctx context.Context, path string) error {
// assumes we are only uploading to the SQL database - TODO: refactor once we introduce object stores
quotaReached, err := s.quotaService.CheckQuotaReached(ctx, "file", nil)
quotaReached, err := s.quotaService.CheckQuotaReached(ctx, QuotaTargetSrv, nil)
if err != nil {
grafanaStorageLogger.Error("failed while checking upload quota", "path", path, "error", err)
return ErrUploadInternalError

View File

@ -118,7 +118,7 @@ func setupUploadStore(t *testing.T, authService storageAuthService) (StorageServ
store.cfg = &GlobalStorageConfig{
AllowUnsanitizedSvgUpload: true,
}
store.quotaService = quotatest.NewQuotaServiceFake()
store.quotaService = quotatest.New(false, nil)
return store, mockStorage, storageName
}
@ -297,7 +297,7 @@ func TestContentRootWithNestedStorage(t *testing.T) {
store.cfg = &GlobalStorageConfig{
AllowUnsanitizedSvgUpload: true,
}
store.quotaService = quotatest.NewQuotaServiceFake()
store.quotaService = quotatest.New(false, nil)
fileName := "file.jpg"
tests := []struct {

View File

@ -357,3 +357,8 @@ type SearchUserFilter interface {
}
type FilterHandler func(params []string) (Filter, error)
const (
QuotaTargetSrv string = "user"
QuotaTarget string = "user"
)

View File

@ -11,6 +11,7 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -37,6 +38,8 @@ type store interface {
BatchDisableUsers(context.Context, *user.BatchDisableUsersCommand) error
Disable(context.Context, *user.DisableUserCommand) error
Search(context.Context, *user.SearchUsersQuery) (*user.SearchUserQueryResult, error)
Count(ctx context.Context) (int64, error)
}
type sqlStore struct {
@ -461,6 +464,22 @@ func (ss *sqlStore) UpdatePermissions(ctx context.Context, userID int64, isAdmin
})
}
func (ss *sqlStore) Count(ctx context.Context) (int64, error) {
type result struct {
Count int64
}
r := result{}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s WHERE is_service_account=%s", ss.db.GetDialect().Quote("user"), ss.db.GetDialect().BooleanStr(false))
if _, err := sess.SQL(rawSQL).Get(&r); err != nil {
return err
}
return nil
})
return r.Count, err
}
// validateOneAdminLeft validate that there is an admin user left
func validateOneAdminLeft(ctx context.Context, sess *db.Session) error {
count, err := sess.Where("is_admin=?", true).Count(&user.User{})

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/models/roletype"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/team"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -32,15 +33,44 @@ func ProvideService(
cfg *setting.Cfg,
teamService team.Service,
cacheService *localcache.CacheService,
) user.Service {
quotaService quota.Service,
) (user.Service, error) {
store := ProvideStore(db, cfg)
return &Service{
s := &Service{
store: &store,
orgService: orgService,
cfg: cfg,
teamService: teamService,
cacheService: cacheService,
}
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
return s, err
}
if err := quotaService.RegisterQuotaReporter(&quota.NewUsageReporter{
TargetSrv: quota.TargetSrv(user.QuotaTargetSrv),
DefaultLimits: defaultLimits,
Reporter: s.Usage,
}); err != nil {
return s, err
}
return s, nil
}
func (s *Service) Usage(ctx context.Context, _ *quota.ScopeParameters) (*quota.Map, error) {
u := &quota.Map{}
if used, err := s.store.Count(ctx); err != nil {
return u, err
} else {
tag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope)
if err != nil {
return u, err
}
u.Set(tag, used)
}
return u, nil
}
func (s *Service) Create(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) {
@ -304,3 +334,19 @@ func (s *Service) GetProfile(ctx context.Context, query *user.GetUserProfileQuer
result, err := s.store.GetProfile(ctx, query)
return result, err
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}
if cfg == nil {
return limits, nil
}
globalQuotaTag, err := quota.NewTag(quota.TargetSrv(user.QuotaTargetSrv), quota.Target(user.QuotaTarget), quota.GlobalScope)
if err != nil {
return limits, err
}
limits.Set(globalQuotaTag, cfg.Quota.Global.User)
return limits, nil
}

View File

@ -252,3 +252,7 @@ func (f *FakeUserStore) Disable(ctx context.Context, cmd *user.DisableUserComman
func (f *FakeUserStore) Search(ctx context.Context, query *user.SearchUsersQuery) (*user.SearchUserQueryResult, error) {
return f.ExpectedSearchUserQueryResult, f.ExpectedError
}
func (f *FakeUserStore) Count(ctx context.Context) (int64, error) {
return 0, nil
}

View File

@ -153,9 +153,6 @@ var (
LDAPAllowSignup bool
LDAPActiveSyncEnabled bool
// Quota
Quota QuotaSettings
// Alerting
AlertingEnabled *bool
ExecuteAlerts bool
@ -422,12 +419,12 @@ type Cfg struct {
LDAPSkipOrgRoleSync bool
LDAPAllowSignup bool
Quota QuotaSettings
DefaultTheme string
DefaultLocale string
HomePage string
Quota QuotaSettings
AutoAssignOrg bool
AutoAssignOrgId int
AutoAssignOrgRole string
@ -1053,11 +1050,12 @@ func (cfg *Cfg) Load(args CommandLineArgs) error {
cfg.readAzureSettings()
cfg.readSessionConfig()
cfg.readSmtpSettings()
cfg.readQuotaSettings()
if err := cfg.readAnnotationSettings(); err != nil {
return err
}
cfg.readQuotaSettings()
cfg.readExpressionsSettings()
if err := cfg.readGrafanaEnvironmentMetrics(); err != nil {
return err

View File

@ -1,9 +1,5 @@
package setting
import (
"reflect"
)
type OrgQuota struct {
User int64 `target:"org_user"`
DataSource int64 `target:"data_source"`
@ -27,45 +23,17 @@ type GlobalQuota struct {
File int64 `target:"file"`
}
func (q *OrgQuota) ToMap() map[string]int64 {
return quotaToMap(*q)
}
func (q *UserQuota) ToMap() map[string]int64 {
return quotaToMap(*q)
}
func quotaToMap(q interface{}) map[string]int64 {
qMap := make(map[string]int64)
typ := reflect.TypeOf(q)
val := reflect.ValueOf(q)
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
name := field.Tag.Get("target")
if name == "" {
name = field.Name
}
if name == "-" {
continue
}
value := val.Field(i)
qMap[name] = value.Int()
}
return qMap
}
type QuotaSettings struct {
Enabled bool
Org *OrgQuota
User *UserQuota
Global *GlobalQuota
Org OrgQuota
User UserQuota
Global GlobalQuota
}
func (cfg *Cfg) readQuotaSettings() {
// set global defaults.
quota := cfg.Raw.Section("quota")
Quota.Enabled = quota.Key("enabled").MustBool(false)
cfg.Quota.Enabled = quota.Key("enabled").MustBool(false)
var alertOrgQuota int64
var alertGlobalQuota int64
@ -74,7 +42,7 @@ func (cfg *Cfg) readQuotaSettings() {
alertGlobalQuota = quota.Key("global_alert_rule").MustInt64(-1)
}
// per ORG Limits
Quota.Org = &OrgQuota{
cfg.Quota.Org = OrgQuota{
User: quota.Key("org_user").MustInt64(10),
DataSource: quota.Key("org_data_source").MustInt64(10),
Dashboard: quota.Key("org_dashboard").MustInt64(10),
@ -83,12 +51,12 @@ func (cfg *Cfg) readQuotaSettings() {
}
// per User limits
Quota.User = &UserQuota{
cfg.Quota.User = UserQuota{
Org: quota.Key("user_org").MustInt64(10),
}
// Global Limits
Quota.Global = &GlobalQuota{
cfg.Quota.Global = GlobalQuota{
User: quota.Key("global_user").MustInt64(-1),
Org: quota.Key("global_org").MustInt64(-1),
DataSource: quota.Key("global_data_source").MustInt64(-1),
@ -98,6 +66,4 @@ func (cfg *Cfg) readQuotaSettings() {
File: quota.Key("global_file").MustInt64(-1),
AlertRule: alertGlobalQuota,
}
cfg.Quota = Quota
}

View File

@ -16,7 +16,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
@ -1878,6 +1877,8 @@ func TestQuota(t *testing.T) {
// Create a user to make authenticated requests
createUser(t, store, user.CreateUserCommand{
// needs permission to update org quota
IsAdmin: true,
DefaultOrgRole: string(org.RoleEditor),
Password: "password",
Login: "grafana",
@ -1918,30 +1919,10 @@ func TestQuota(t *testing.T) {
// check quota limits
t.Run("when quota limit exceed creating new rule should fail", func(t *testing.T) {
// get existing org quota
query := models.GetOrgQuotaByTargetQuery{OrgId: 1, Target: "alert_rule"}
err = store.GetOrgQuotaByTarget(context.Background(), &query)
require.NoError(t, err)
used := query.Result.Used
limit := query.Result.Limit
// set org quota limit to equal used
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: 1,
Target: "alert_rule",
Limit: used,
}
err := store.UpdateOrgQuota(context.Background(), &orgCmd)
require.NoError(t, err)
limit, used := apiClient.GetOrgQuotaLimits(t, 1)
apiClient.UpdateAlertRuleOrgQuota(t, 1, used)
t.Cleanup(func() {
// reset org quota to original value
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: 1,
Target: "alert_rule",
Limit: limit,
}
err := store.UpdateOrgQuota(context.Background(), &orgCmd)
require.NoError(t, err)
apiClient.UpdateAlertRuleOrgQuota(t, 1, limit)
})
// try to create an alert rule

View File

@ -16,6 +16,7 @@ import (
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/util"
)
@ -202,6 +203,60 @@ func (a apiClient) CreateFolder(t *testing.T, uID string, title string) {
a.ReloadCachedPermissions(t)
}
func (a apiClient) GetOrgQuotaLimits(t *testing.T, orgID int64) (int64, int64) {
t.Helper()
u := fmt.Sprintf("%s/api/orgs/%d/quotas", a.url, orgID)
// nolint:gosec
resp, err := http.Get(u)
require.NoError(t, err)
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, http.StatusOK, resp.StatusCode)
results := []quota.QuotaDTO{}
require.NoError(t, json.Unmarshal(b, &results))
var limit int64 = 0
var used int64 = 0
for _, q := range results {
if q.Target != string(ngmodels.QuotaTargetSrv) {
continue
}
limit = q.Limit
used = q.Used
}
return limit, used
}
func (a apiClient) UpdateAlertRuleOrgQuota(t *testing.T, orgID int64, limit int64) {
t.Helper()
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err := enc.Encode(&quota.UpdateQuotaCmd{
Target: "alert_rule",
Limit: limit,
OrgID: orgID,
})
require.NoError(t, err)
u := fmt.Sprintf("%s/api/orgs/%d/quotas/alert_rule", a.url, orgID)
// nolint:gosec
client := &http.Client{}
req, err := http.NewRequest(http.MethodPut, u, &buf)
require.NoError(t, err)
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
require.NoError(t, err)
defer func() {
_ = resp.Body.Close()
}()
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func (a apiClient) PostRulesGroup(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig) (int, string) {
t.Helper()
buf := bytes.Buffer{}

View File

@ -16,16 +16,14 @@ import (
datasourceservice "github.com/grafana/grafana/pkg/services/datasources/service"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore"
secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
func TestHandleRequest(t *testing.T) {
cfg := &setting.Cfg{}
t.Run("Should invoke plugin manager QueryData when handling request for query", func(t *testing.T) {
origOAuthIsOAuthPassThruEnabledFunc := oAuthIsOAuthPassThruEnabledFunc
oAuthIsOAuthPassThruEnabledFunc = func(oAuthTokenService oauthtoken.OAuthTokenService, ds *datasources.DataSource) bool {
@ -46,7 +44,10 @@ func TestHandleRequest(t *testing.T) {
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
datasourcePermissions := acmock.NewMockedPermissionsService()
dsService := datasourceservice.ProvideService(nil, secretsService, secretsStore, cfg, featuremgmt.WithFeatures(), acmock.New(), datasourcePermissions)
quotaService := quotatest.New(false, nil)
dsService, err := datasourceservice.ProvideService(nil, secretsService, secretsStore, sqlStore.Cfg, featuremgmt.WithFeatures(), acmock.New(), datasourcePermissions, quotaService)
require.NoError(t, err)
s := ProvideService(client, nil, dsService)
ds := &datasources.DataSource{Id: 12, Type: "unregisteredType", JsonData: simplejson.New()}