From 163546d40f40b84fb2521a53804c2596be0af553 Mon Sep 17 00:00:00 2001 From: Ieva Date: Thu, 20 Mar 2025 17:38:09 +0000 Subject: [PATCH] RBAC: Remove dashboard guardians pt 1 (#102314) * replace the usage of dashboard guardians with calling AC evaluators or checking access in middleware * linting fixes * fix test * more test fixes * remove a todo comment --- pkg/api/api.go | 25 +- pkg/api/dashboard.go | 96 +- pkg/api/dashboard_test.go | 141 +- .../accesscontrol/accesscontrol_test.go | 10 +- .../annotationsimpl/annotations_test.go | 11 +- .../dashboards/service/dashboard_service.go | 110 +- .../dashboard_service_integration_test.go | 1444 ++++++++--------- .../service/dashboard_service_test.go | 22 +- .../publicdashboards/service/service_test.go | 9 +- 9 files changed, 772 insertions(+), 1096 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 77efde07038..3aefe35ad22 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -462,22 +462,24 @@ func (hs *HTTPServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { - dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead)), routing.Wrap(hs.GetDashboard)) + dashUIDScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(ac.Parameter(":uid")) + + dashboardRoute.Get("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsRead, dashUIDScope)), routing.Wrap(hs.GetDashboard)) if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { - dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.SoftDeleteDashboard)) + dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.SoftDeleteDashboard)) } else { - dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete)), routing.Wrap(hs.DeleteDashboardByUID)) + dashboardRoute.Delete("/uid/:uid", authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.DeleteDashboardByUID)) } dashboardRoute.Group("/uid/:uid", func(dashUidRoute routing.RouteRegister) { - dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions)) - dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion)) - dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) + dashUidRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersions)) + dashUidRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDashboardVersion)) + dashUidRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.GetDashboardVersion)) if hs.Features.IsEnabledGlobally(featuremgmt.FlagDashboardRestore) { - dashUidRoute.Patch("/trash", reqOrgAdmin, routing.Wrap(hs.RestoreDeletedDashboard)) - dashUidRoute.Delete("/trash", reqOrgAdmin, routing.Wrap(hs.HardDeleteDashboardByUID)) + dashUidRoute.Patch("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashUIDScope)), routing.Wrap(hs.RestoreDeletedDashboard)) + dashUidRoute.Delete("/trash", reqOrgAdmin, authorize(ac.EvalPermission(dashboards.ActionDashboardsDelete, dashUIDScope)), routing.Wrap(hs.HardDeleteDashboardByUID)) } dashUidRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { @@ -497,9 +499,10 @@ func (hs *HTTPServer) registerRoutes() { // Deprecated: use /uid/:uid API instead. dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) { - dashIdRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersions)) - dashIdRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.GetDashboardVersion)) - dashIdRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite)), routing.Wrap(hs.RestoreDashboardVersion)) + dashIDScope := dashboards.ScopeDashboardsProvider.GetResourceScope(ac.Parameter(":dashboardId")) + dashIdRoute.Get("/versions", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.GetDashboardVersions)) + dashIdRoute.Get("/versions/:id", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.GetDashboardVersion)) + dashIdRoute.Post("/restore", authorize(ac.EvalPermission(dashboards.ActionDashboardsWrite, dashIDScope)), routing.Wrap(hs.RestoreDashboardVersion)) dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) { dashboardPermissionRoute.Get("/", authorize(ac.EvalPermission(dashboards.ActionDashboardsPermissionsRead)), routing.Wrap(hs.GetDashboardPermissionList)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index c1c61753676..704d736f371 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -27,7 +27,6 @@ import ( "github.com/grafana/grafana/pkg/services/dashboardversion/dashverimpl" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" pref "github.com/grafana/grafana/pkg/services/preference" publicdashboardModels "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -141,18 +140,21 @@ func (hs *HTTPServer) GetDashboard(c *contextmodel.ReqContext) response.Response dash.Data.Set("id", dash.ID) } } - guardian, err := guardian.NewByDashboard(ctx, dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - if canView, err := guardian.CanView(); err != nil || !canView { - return dashboardGuardianResponse(err) + dashScope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID) + writeEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashScope) + canSave, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, writeEvaluator) + canEdit := canSave + //nolint:staticcheck // ViewersCanEdit is deprecated but still used for backward compatibility + if hs.Cfg.ViewersCanEdit { + canEdit = true } - canEdit, _ := guardian.CanEdit() - canSave, _ := guardian.CanSave() - canAdmin, _ := guardian.CanAdmin() - canDelete, _ := guardian.CanDelete() + deleteEvaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsDelete, dashScope) + canDelete, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, deleteEvaluator) + adminEvaluator := accesscontrol.EvalAll( + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsRead, dashScope), + accesscontrol.EvalPermission(dashboards.ActionDashboardsPermissionsWrite, dashScope)) + canAdmin, _ := hs.AccessControl.Evaluate(ctx, c.SignedInUser, adminEvaluator) isStarred, err := hs.isDashboardStarredByUser(c, dash.ID) if err != nil { @@ -369,15 +371,6 @@ func (hs *HTTPServer) RestoreDeletedDashboard(c *contextmodel.ReqContext) respon return response.Error(http.StatusNotFound, "Dashboard not found", err) } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canRestore, err := guardian.CanSave(); err != nil || !canRestore { - return dashboardGuardianResponse(err) - } - err = hs.DashboardService.RestoreDashboard(c.Req.Context(), dash, c.SignedInUser, cmd.FolderUID) if err != nil { var dashboardErr dashboards.DashboardErr @@ -417,16 +410,7 @@ func (hs *HTTPServer) SoftDeleteDashboard(c *contextmodel.ReqContext) response.R return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canDelete, err := guardian.CanDelete(); err != nil || !canDelete { - return dashboardGuardianResponse(err) - } - - err = hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid) + err := hs.DashboardService.SoftDeleteDashboard(c.Req.Context(), c.SignedInUser.GetOrgID(), uid) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { @@ -498,21 +482,12 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo } } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canDelete, err := guardian.CanDelete(); err != nil || !canDelete { - return dashboardGuardianResponse(err) - } - if dash.IsFolder { return response.Error(http.StatusBadRequest, "Use folders endpoint for deleting folders.", nil) } // disconnect all library elements for this dashboard - err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) + err := hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) if err != nil { hs.log.Error( "Failed to disconnect library elements", @@ -840,14 +815,6 @@ func (hs *HTTPServer) GetDashboardVersions(c *contextmodel.ReqContext) response. return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - query := dashver.ListDashboardVersionsQuery{ OrgID: c.SignedInUser.GetOrgID(), DashboardID: dash.ID, @@ -959,15 +926,6 @@ func (hs *HTTPServer) GetDashboardVersion(c *contextmodel.ReqContext) response.R return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - version, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64) if err != nil { return response.Err(err) @@ -1027,22 +985,15 @@ func (hs *HTTPServer) CalculateDashboardDiff(c *contextmodel.ReqContext) respons if err := web.Bind(c.Req, &apiOptions); err != nil { return response.Error(http.StatusBadRequest, "bad request data", err) } - guardianBase, err := guardian.New(c.Req.Context(), apiOptions.Base.DashboardId, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - if canSave, err := guardianBase.CanSave(); err != nil || !canSave { + evaluator := accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.Base.DashboardId, 10))) + if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite { return dashboardGuardianResponse(err) } if apiOptions.Base.DashboardId != apiOptions.New.DashboardId { - guardianNew, err := guardian.New(c.Req.Context(), apiOptions.New.DashboardId, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canSave, err := guardianNew.CanSave(); err != nil || !canSave { + evaluator = accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScope(strconv.FormatInt(apiOptions.New.DashboardId, 10))) + if canWrite, err := hs.AccessControl.Evaluate(c.Req.Context(), c.SignedInUser, evaluator); err != nil || !canWrite { return dashboardGuardianResponse(err) } } @@ -1159,15 +1110,6 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon return rsp } - guardian, err := guardian.NewByDashboard(c.Req.Context(), dash, c.SignedInUser.GetOrgID(), c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canSave, err := guardian.CanSave(); err != nil || !canSave { - return dashboardGuardianResponse(err) - } - versionQuery := dashver.GetDashboardVersionQuery{DashboardID: dashID, DashboardUID: dash.UID, Version: apiCmd.Version, OrgID: c.SignedInUser.GetOrgID()} version, err := hs.dashboardVersionService.Get(c.Req.Context(), &versionQuery) if err != nil { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 3ed18a073e8..b715e17481f 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "context" "encoding/json" "fmt" @@ -327,12 +328,16 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { hs.AccessControl = acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) hs.starService = startest.NewStarServiceFake() - hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{ - ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{}, - ExpectedDashboardVersion: &dashver.DashboardVersionDTO{}, + expectedDashVersions := []*dashver.DashboardVersionDTO{ + {Data: simplejson.NewFromAny(map[string]any{"title": "Dash"})}, + {Data: simplejson.NewFromAny(map[string]any{"title": "Dash updated"})}, } - guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger()) + hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{ + ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{}, + ExpectedDashboardVersions: expectedDashVersions, + ExpectedDashboardVersion: &dashver.DashboardVersionDTO{}, + } }) } @@ -344,6 +349,17 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { return server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/versions"), userWithPermissions(1, permissions))) } + calculateDiff := func(server *webtest.Server, permissions []accesscontrol.Permission) (*http.Response, error) { + cmd := &dtos.CalculateDiffOptions{ + Base: dtos.CalculateDiffTarget{DashboardId: 1, Version: 1}, + New: dtos.CalculateDiffTarget{DashboardId: 1, Version: 2}, + DiffType: "json", + } + jsonBytes, err := json.Marshal(cmd) + require.NoError(t, err) + return server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/calculate-diff", bytes.NewReader(jsonBytes)), userWithPermissions(1, permissions))) + } + t.Run("Should not be able to list dashboard versions without correct permission", func(t *testing.T) { server := setup() @@ -363,7 +379,6 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { server := setup() permissions := []accesscontrol.Permission{ - {Action: dashboards.ActionDashboardsRead, Scope: "dashboards:uid:1"}, {Action: dashboards.ActionDashboardsWrite, Scope: "dashboards:uid:1"}, } @@ -378,6 +393,28 @@ func TestHTTPServer_GetDashboardVersions_AccessControl(t *testing.T) { require.NoError(t, res.Body.Close()) }) + + t.Run("Should be able to diff dashboards with correct permissions", func(t *testing.T) { + server := setup() + + permissions := []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll}, + } + + res, err := calculateDiff(server, permissions) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("Should not be able to diff dashboards without permissions", func(t *testing.T) { + server := setup() + + res, err := calculateDiff(server, []accesscontrol.Permission{}) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) } func TestDashboardAPIEndpoint(t *testing.T) { @@ -527,39 +564,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { }), }, } - sqlmock := dbtest.NewFakeDB() - cmd := dtos.CalculateDiffOptions{ - Base: dtos.CalculateDiffTarget{ - DashboardId: 1, - Version: 1, - }, - New: dtos.CalculateDiffTarget{ - DashboardId: 2, - Version: 2, - }, - DiffType: "basic", - } - - t.Run("when user does not have permission", func(t *testing.T) { - role := org.RoleViewer - postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: false}) - - callPostDashboard(sc) - assert.Equal(t, http.StatusForbidden, sc.resp.Code) - }, sqlmock, fakeDashboardVersionService) - }) - - t.Run("when user does have permission", func(t *testing.T) { - role := org.RoleAdmin - postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - // This test shouldn't hit GetDashboardACLInfoList, so no setup needed - sc.dashboardVersionService = fakeDashboardVersionService - callPostDashboard(sc) - assert.Equal(t, http.StatusOK, sc.resp.Code) - }, sqlmock, fakeDashboardVersionService) - }) }) t.Run("Given dashboard in folder being restored should restore to folder", func(t *testing.T) { @@ -588,11 +592,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { }, } mockSQLStore := dbtest.NewFakeDB() - origNewGuardian := guardian.New - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - t.Cleanup(func() { - guardian.New = origNewGuardian - }) restoreDashboardVersionScenario(t, "When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", dashboardService, fakeDashboardVersionService, cmd, func(sc *scenarioContext) { @@ -648,7 +647,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { require.NoError(t, err) qResult := &dashboards.Dashboard{ID: 1, Data: dataValue} dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true}) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) @@ -678,7 +676,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryElementService: &libraryelementsfake.LibraryElementService{}, dashboardProvisioningService: mockDashboardProvisioningService{}, SQLStore: mockSQLStore, - AccessControl: accesscontrolmock.New(), + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), @@ -710,7 +708,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { Data: dataValue, } dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true}) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", org.RoleEditor, func(sc *scenarioContext) { hs := &HTTPServer{ @@ -718,7 +715,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { LibraryPanelService: &mockLibraryPanelService{}, LibraryElementService: &libraryelementsfake.LibraryElementService{}, SQLStore: mockSQLStore, - AccessControl: accesscontrolmock.New(), + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, DashboardService: dashboardService, Features: featuremgmt.WithFeatures(), starService: startest.NewStarServiceFake(), @@ -753,7 +750,7 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { Cfg: cfg, pluginStore: &pluginstore.FakePluginStore{}, SQLStore: mockSQLStore, - AccessControl: accesscontrolmock.New(), + AccessControl: actest.FakeAccessControl{ExpectedEvaluate: true}, Features: featuremgmt.WithFeatures(), DashboardService: dashboardService, dashboardVersionService: fakeDashboardVersionService, @@ -765,13 +762,8 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { } } - setUp := func() { - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - } - loggedInUserScenarioWithRole(t, "When user exists and calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) { - setUp() fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{ { Version: 1, @@ -797,7 +789,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When user does not exist and calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) { - setUp() fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{ { Version: 1, @@ -823,7 +814,6 @@ func TestDashboardVersionsAPIEndpoint(t *testing.T) { loggedInUserScenarioWithRole(t, "When failing to get user and calling GET on", "GET", "/api/dashboards/id/2/versions", "/api/dashboards/id/:dashboardId/versions", org.RoleEditor, func(sc *scenarioContext) { - setUp() fakeDashboardVersionService.ExpectedListDashboarVersions = []*dashver.DashboardVersionDTO{ { Version: 1, @@ -978,47 +968,6 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s }) } -func postDiffScenario(t *testing.T, desc string, url string, routePattern string, cmd dtos.CalculateDiffOptions, - role org.RoleType, fn scenarioFunc, sqlmock db.DB, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService, -) { - t.Run(fmt.Sprintf("%s %s", desc, url), func(t *testing.T) { - cfg := setting.NewCfg() - - dashSvc := dashboards.NewFakeDashboardService(t) - hs := HTTPServer{ - Cfg: cfg, - ProvisioningService: provisioning.NewProvisioningServiceMock(context.Background()), - Live: newTestLive(t, db.InitTestDB(t)), - QuotaService: quotatest.New(false, nil), - LibraryPanelService: &mockLibraryPanelService{}, - LibraryElementService: &libraryelementsfake.LibraryElementService{}, - SQLStore: sqlmock, - dashboardVersionService: fakeDashboardVersionService, - Features: featuremgmt.WithFeatures(), - DashboardService: dashSvc, - tracer: tracing.InitializeTracerForTest(), - } - - sc := setupScenarioContext(t, url) - sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { - c.Req.Body = mockRequestBody(cmd) - c.Req.Header.Add("Content-Type", "application/json") - sc.context = c - sc.context.SignedInUser = &user.SignedInUser{ - OrgID: testOrgID, - UserID: testUserID, - } - sc.context.OrgRole = role - - return hs.CalculateDashboardDiff(c) - }) - - sc.m.Post(routePattern, sc.defaultHandler) - - fn(sc) - }) -} - func restoreDashboardVersionScenario(t *testing.T, desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, fakeDashboardVersionService *dashvertest.FakeDashboardVersionService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc, sqlStore db.DB, diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 3dc79fe876f..1bff50c2dd3 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" @@ -22,7 +22,6 @@ import ( dashboardsservice "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder/folderimpl" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/search/sort" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" @@ -42,16 +41,13 @@ func TestIntegrationAuthorize(t *testing.T) { } sql, cfg := db.InitTestDBWithCfg(t) - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) folderStore := folderimpl.ProvideDashboardFolderStore(sql) fStore := folderimpl.ProvideStore(sql) dashStore, err := database.ProvideDashboardStore(sql, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql)) require.NoError(t, err) - ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) + ac := actest.FakeAccessControl{ExpectedEvaluate: true} folderSvc := folderimpl.ProvideService( - fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, + fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 0615023b430..145c94c0748 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -15,7 +15,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations/testutil" @@ -54,16 +54,13 @@ func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { features := featuremgmt.WithFeatures() tagService := tagimpl.ProvideService(sql) ruleStore := alertingStore.SetupStoreForTesting(t, sql) - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{}) folderStore := folderimpl.ProvideDashboardFolderStore(sql) fStore := folderimpl.ProvideStore(sql) dashStore, err := database.ProvideDashboardStore(sql, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sql)) require.NoError(t, err) - ac := acimpl.ProvideAccessControl(featuremgmt.WithFeatures()) + ac := actest.FakeAccessControl{ExpectedEvaluate: true} folderSvc := folderimpl.ProvideService( - fStore, accesscontrolmock.New(), bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, + fStore, ac, bus.ProvideBus(tracing.InitializeTracerForTest()), dashStore, folderStore, nil, sql, featuremgmt.WithFeatures(), supportbundlestest.NewFakeBundleService(), nil, cfg, nil, tracing.InitializeTracerForTest(), nil, dualwrite.ProvideTestService(), sort.ProvideService()) dashSvc, err := dashboardsservice.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), accesscontrolmock.NewMockedPermissionsService(), ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) @@ -242,7 +239,7 @@ func TestIntegrationAnnotationListingWithInheritedRBAC(t *testing.T) { guardian.New = origNewGuardian }) - ac := acimpl.ProvideAccessControl(features) + ac := actest.FakeAccessControl{ExpectedEvaluate: true} fStore := folderimpl.ProvideStore(sql) folderStore := folderimpl.ProvideDashboardFolderStore(sql) folderSvc := folderimpl.ProvideService( diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 53164cda375..a8516b0d594 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -43,7 +43,6 @@ import ( dashboardsearch "github.com/grafana/grafana/pkg/services/dashboards/service/search" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/quota" @@ -413,14 +412,24 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } if isParentFolderChanged { - // Check that the user is allowed to add a dashboard to the folder - guardian, err := guardian.NewByDashboard(ctx, dash, dto.OrgID, dto.User) - if err != nil { - return nil, err + if canCreate, err := dr.canCreateDashboard(ctx, dto.User, dash); err != nil || !canCreate { + if err != nil { + return nil, err + } + return nil, dashboards.ErrDashboardUpdateAccessDenied } + } + + if dash.ID == 0 { metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - if canSave, err := guardian.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canSave { + if canCreate, err := dr.canCreateDashboard(ctx, dto.User, dash); err != nil || !canCreate { + if err != nil { + return nil, err + } + return nil, dashboards.ErrDashboardUpdateAccessDenied + } + } else { + if canSave, err := dr.canSaveDashboard(ctx, dto.User, dash); err != nil || !canSave { if err != nil { return nil, err } @@ -439,29 +448,6 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d } } - guard, err := getGuardianForSavePermissionCheck(ctx, dash, dto.User) - if err != nil { - return nil, err - } - - if dash.ID == 0 { - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - // nolint:staticcheck - if canCreate, err := guard.CanCreate(dash.FolderID, dash.IsFolder); err != nil || !canCreate { - if err != nil { - return nil, err - } - return nil, dashboards.ErrDashboardUpdateAccessDenied - } - } else { - if canSave, err := guard.CanSave(); err != nil || !canSave { - if err != nil { - return nil, err - } - return nil, dashboards.ErrDashboardUpdateAccessDenied - } - } - var userID int64 if id, err := identity.UserIdentifier(dto.User.GetID()); err == nil { userID = id @@ -561,6 +547,30 @@ func (dr *DashboardServiceImpl) ValidateDashboardBeforeSave(ctx context.Context, return isParentFolderChanged, nil } +func (dr *DashboardServiceImpl) canSaveDashboard(ctx context.Context, user identity.Requester, dash *dashboards.Dashboard) (bool, error) { + action := dashboards.ActionDashboardsWrite + if dash.IsFolder { + action = dashboards.ActionFoldersWrite + } + scope := dashboards.ScopeDashboardsProvider.GetResourceScopeUID(dash.UID) + if dash.IsFolder { + scope = dashboards.ScopeFoldersProvider.GetResourceScopeUID(dash.UID) + } + return dr.ac.Evaluate(ctx, user, accesscontrol.EvalPermission(action, scope)) +} + +func (dr *DashboardServiceImpl) canCreateDashboard(ctx context.Context, user identity.Requester, dash *dashboards.Dashboard) (bool, error) { + action := dashboards.ActionDashboardsCreate + if dash.IsFolder { + action = dashboards.ActionFoldersCreate + } + scope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(dash.FolderUID) + if dash.FolderUID == "" { + scope = dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID) + } + return dr.ac.Evaluate(ctx, user, accesscontrol.EvalPermission(action, scope)) +} + // waitForSearchQuery waits for the search query to return the expected number of hits. // Since US doesn't offer search-after-write guarantees, we can use this to wait after writes until the indexer is up to date. func (dr *DashboardServiceImpl) waitForSearchQuery(ctx context.Context, query *dashboards.FindPersistedDashboardsQuery, maxRetries int, expectedHits int64) error { @@ -620,46 +630,6 @@ func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context. return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) } -// getGuardianForSavePermissionCheck returns the guardian to be used for checking permission of dashboard -// It replaces deleted Dashboard.GetDashboardIdForSavePermissionCheck() -func getGuardianForSavePermissionCheck(ctx context.Context, d *dashboards.Dashboard, user identity.Requester) (guardian.DashboardGuardian, error) { - ctx, span := tracer.Start(ctx, "dashboards.service.getGuardianForSavePermissionCheck") - defer span.End() - - newDashboard := d.ID == 0 - - if newDashboard { - // if it's a new dashboard/folder check the parent folder permissions - metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Dashboard).Inc() - guard, err := guardian.NewByFolder(ctx, &folder.Folder{ - ID: d.FolderID, // nolint:staticcheck - OrgID: d.OrgID, - }, d.OrgID, user) - if err != nil { - return nil, err - } - return guard, nil - } - - if d.IsFolder { - guard, err := guardian.NewByFolder(ctx, &folder.Folder{ - ID: d.ID, // nolint:staticcheck - UID: d.UID, - OrgID: d.OrgID, - }, d.OrgID, user) - if err != nil { - return nil, err - } - return guard, nil - } - - guard, err := guardian.NewByDashboard(ctx, d, d.OrgID, user) - if err != nil { - return nil, err - } - return guard, nil -} - func validateDashboardRefreshInterval(minRefreshInterval string, dash *dashboards.Dashboard) error { if minRefreshInterval == "" { return nil diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 221ade24f72..e22a5ae0cd4 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -8,13 +8,12 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/client" @@ -23,7 +22,6 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "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/publicdashboards" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -49,809 +47,710 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { // Basic validation tests - permissionScenario(t, "When saving a dashboard with non-existing id", true, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": float64(123412321), - "title": "Expect error", - }), - } + permissionScenario(t, "When saving a dashboard with non-existing id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": float64(123412321), + "title": "Expect error", + }), + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardNotFound, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardNotFound, err) + }) // Given other organization t.Run("Given organization B", func(t *testing.T) { const otherOrgId int64 = 2 - permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", - true, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: otherOrgId, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Expect error", - }), - Overwrite: false, - } + permissionScenario(t, "When creating a dashboard with same id as dashboard in organization A", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "title": "Expect error", + }), + Overwrite: false, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardNotFound, err) - }) - - permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", - true, func(t *testing.T, sc *permissionScenarioContext) { - const otherOrgId int64 = 2 - cmd := dashboards.SaveDashboardCommand{ - OrgID: otherOrgId, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Dash with existing uid in other org", - }), - Overwrite: false, - } - - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - OrgID: otherOrgId, - UID: sc.savedDashInFolder.UID, - }) - require.NoError(t, err) - }) - }) - - t.Run("Given user has no permission to save", func(t *testing.T) { - const canSave = false - - permissionScenario(t, "When creating a new dashboard in the General folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - sqlStore := db.InitTestDB(t) - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash", - }), - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sqlStore) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, "", sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When creating a new dashboard in other folder, it should create dashboard guardian for other folder with correct arguments and rsult in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.otherSavedFolder.ID, sc.dashboardGuardianMock.DashID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When creating a new dashboard by existing title in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() - - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When creating a new dashboard by existing UID in folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "New dash", - }), - FolderUID: sc.savedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When updating a dashboard by existing id in the General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Dash", - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When updating a dashboard by existing id in other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Dash", - }), - FolderUID: sc.savedDashInFolder.FolderUID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing ID to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing id to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "Dash", - }), - FolderUID: "", - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInGeneralFolder.UID, - "title": "Dash", - }), - FolderUID: sc.otherSavedFolder.UID, - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) - }) - - permissionScenario(t, "When moving a dashboard by existing UID to the General folder from other folder, it should create dashboard guardian for dashboard with correct arguments and result in access denied error", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Dash", - }), - FolderUID: "", - UserID: 10000, - Overwrite: true, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetID()) - require.NoError(t, err) - - assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) - assert.Equal(t, cmd.OrgID, sc.dashboardGuardianMock.OrgID) - assert.Equal(t, cmd.UserID, userID) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardNotFound, err) + }) + + permissionScenario(t, "When creating a dashboard with same uid as dashboard in organization A, it should create a new dashboard in org B", func(t *testing.T, sc *permissionScenarioContext) { + const otherOrgId int64 = 2 + cmd := dashboards.SaveDashboardCommand{ + OrgID: otherOrgId, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Dash with existing uid in other org", + }), + Overwrite: false, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + OrgID: otherOrgId, + UID: sc.savedDashInFolder.UID, }) + require.NoError(t, err) + }) }) t.Run("Given user has permission to save", func(t *testing.T) { - const canSave = true - t.Run("and overwrite flag is set to false", func(t *testing.T) { const shouldOverwrite = false - permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When creating a dashboard in General folder with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) - permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } + require.NoError(t, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + permissionScenario(t, "When creating a dashboard in other folder with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } - assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When creating a folder with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) + assert.True(t, res.IsFolder) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Dash without id and uid", + }), + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + assert.Greater(t, res.ID, int64(0)) + assert.NotEmpty(t, res.UID) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When saving a dashboard when dashboard id is zero ", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": 0, + "title": "Dash with zero id", + }), + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, + }) + require.NoError(t, err) + }) + + permissionScenario(t, "When saving a dashboard in non-existing folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Expect error", + }), + FolderUID: "123412321", + Overwrite: shouldOverwrite, + } + + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrFolderNotFound, err) + }) + + permissionScenario(t, "When updating an existing dashboard by id without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInGeneralFolder.ID, + "title": "test dash 23", + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } + + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) + }) + + permissionScenario(t, "When updating an existing dashboard by id with current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInGeneralFolder.ID, + "title": "Updated title", + "version": sc.savedDashInGeneralFolder.Version, + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) - permissionScenario(t, "When creating a folder with same name as dashboard in other folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } + require.NoError(t, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + permissionScenario(t, "When updating an existing dashboard by uid without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "test dash 23", + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } - assert.NotEqual(t, sc.savedDashInGeneralFolder.ID, res.ID) - assert.True(t, res.IsFolder) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) + }) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + permissionScenario(t, "When updating an existing dashboard by uid with current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Updated title", + "version": sc.savedDashInFolder.Version, + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When saving a dashboard without id and uid and unique title in folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Dash without id and uid", - }), - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderUID: sc.savedDashInFolder.FolderUID, + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) - assert.Greater(t, res.ID, int64(0)) - assert.NotEmpty(t, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) + permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderUID: sc.savedDashInGeneralFolder.FolderUID, + Overwrite: shouldOverwrite, + } - permissionScenario(t, "When saving a dashboard when dashboard id is zero ", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": 0, - "title": "Dash with zero id", - }), - Overwrite: shouldOverwrite, - } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + permissionScenario(t, "When creating a folder with same name as existing folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When saving a dashboard in non-existing folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": "Expect error", - }), - FolderUID: "123412321", - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrFolderNotFound, err) - }) - - permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "test dash 23", - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) - }) - - permissionScenario(t, "When updating an existing dashboard by id with current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Updated title", - "version": sc.savedDashInGeneralFolder.Version, - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } - - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInGeneralFolder.ID, - OrgID: cmd.OrgID, - }) - - require.NoError(t, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "test dash 23", - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardVersionMismatch, err) - }) - - permissionScenario(t, "When updating an existing dashboard by uid with current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Updated title", - "version": sc.savedDashInFolder.Version, - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } - - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedDashInFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", - canSave, func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) - - permissionScenario(t, "When creating a folder with same name as existing folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) }) t.Run("and overwrite flag is set to true", func(t *testing.T) { const shouldOverwrite = true - permissionScenario(t, "When updating an existing dashboard by id without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInGeneralFolder.ID, - "title": "Updated title", - }), - FolderUID: sc.savedFolder.UID, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating an existing dashboard by id without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInGeneralFolder.ID, + "title": "Updated title", + }), + FolderUID: sc.savedFolder.UID, + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInGeneralFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInGeneralFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When updating an existing dashboard by uid without current version", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "Updated title", - }), - FolderUID: "", - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating an existing dashboard by uid without current version", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Updated title", + }), + FolderUID: "", + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When updating uid for existing dashboard using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "uid": "new-uid", - "title": sc.savedDashInFolder.Title, - }), - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating uid for existing dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "uid": "new-uid", + "title": sc.savedDashInFolder.Title, + }), + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.ID, res.ID) - assert.Equal(t, "new-uid", res.UID) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, "new-uid", res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: sc.savedDashInFolder.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: sc.savedDashInFolder.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "uid": sc.savedDashInGeneralFolder.UID, - "title": sc.savedDashInFolder.Title, - }), - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating uid to an existing uid for existing dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "uid": sc.savedDashInGeneralFolder.UID, + "title": sc.savedDashInFolder.Title, + }), + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardWithSameUIDExists, err) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardWithSameUIDExists, err) + }) + + permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", func(t *testing.T, sc *permissionScenarioContext) { + t.Skip() + + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInFolder.Title, + }), + FolderUID: sc.savedDashInFolder.FolderUID, + Overwrite: shouldOverwrite, + } + + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInFolder.UID, res.UID) + + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When creating a dashboard with same name as dashboard in other folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() + permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", func(t *testing.T, sc *permissionScenarioContext) { + t.Skip() - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInFolder.Title, - }), - FolderUID: sc.savedDashInFolder.FolderUID, - Overwrite: shouldOverwrite, - } + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": nil, + "title": sc.savedDashInGeneralFolder.Title, + }), + FolderUID: sc.savedDashInGeneralFolder.FolderUID, + Overwrite: shouldOverwrite, + } - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInFolder.ID, res.ID) - assert.Equal(t, sc.savedDashInFolder.UID, res.UID) + res, _ := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NotNil(t, res) + assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) + assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) + _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ + ID: res.ID, + OrgID: cmd.OrgID, }) + require.NoError(t, err) + }) - permissionScenario(t, "When creating a dashboard with same name as dashboard in General folder", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - t.Skip() + permissionScenario(t, "When updating existing folder to a dashboard using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedFolder.ID, + "title": "new title", + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } - cmd := dashboards.SaveDashboardCommand{ - OrgID: testOrgID, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": nil, - "title": sc.savedDashInGeneralFolder.Title, - }), - FolderUID: sc.savedDashInGeneralFolder.FolderUID, - Overwrite: shouldOverwrite, - } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - res := callSaveWithResult(t, cmd, sc.sqlStore) - require.NotNil(t, res) - assert.Equal(t, sc.savedDashInGeneralFolder.ID, res.ID) - assert.Equal(t, sc.savedDashInGeneralFolder.UID, res.UID) + permissionScenario(t, "When updating existing dashboard to a folder using id", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "id": sc.savedDashInFolder.ID, + "title": "new folder title", + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - _, err := sc.dashboardStore.GetDashboard(context.Background(), &dashboards.GetDashboardQuery{ - ID: res.ID, - OrgID: cmd.OrgID, - }) - require.NoError(t, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - permissionScenario(t, "When updating existing folder to a dashboard using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedFolder.ID, - "title": "new title", - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing folder to a dashboard using uid", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedFolder.UID, + "title": "new title", + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - permissionScenario(t, "When updating existing dashboard to a folder using id", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "id": sc.savedDashInFolder.ID, - "title": "new folder title", - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing dashboard to a folder using uid", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "new folder title", + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) + }) - permissionScenario(t, "When updating existing folder to a dashboard using uid", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedFolder.UID, - "title": "new title", - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing folder to a dashboard using title", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": sc.savedFolder.Title, + }), + IsFolder: false, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) - permissionScenario(t, "When updating existing dashboard to a folder using uid", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "uid": sc.savedDashInFolder.UID, - "title": "new folder title", - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } + permissionScenario(t, "When updating existing dashboard to a folder using title", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: 1, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": sc.savedDashInGeneralFolder.Title, + }), + IsFolder: true, + Overwrite: shouldOverwrite, + } - err := callSaveWithError(t, cmd, sc.sqlStore) - assert.Equal(t, dashboards.ErrDashboardTypeMismatch, err) - }) - - permissionScenario(t, "When updating existing folder to a dashboard using title", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedFolder.Title, - }), - IsFolder: false, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) - - permissionScenario(t, "When updating existing dashboard to a folder using title", canSave, - func(t *testing.T, sc *permissionScenarioContext) { - cmd := dashboards.SaveDashboardCommand{ - OrgID: 1, - Dashboard: simplejson.NewFromAny(map[string]any{ - "title": sc.savedDashInGeneralFolder.Title, - }), - IsFolder: true, - Overwrite: shouldOverwrite, - } - - err := callSaveWithError(t, cmd, sc.sqlStore) - require.NoError(t, err) - }) + _, err := callSaveWithResult(t, cmd, sc.sqlStore, nil) + require.NoError(t, err) + }) }) }) }) } +func TestIntegrationDashboardServicePermissions(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + t.Run("Given saved folders and dashboards in organization A", func(t *testing.T) { + permissionScenario(t, "When creating a new dashboard in the General folder, requires create permissions scoped to the general folder", + func(t *testing.T, sc *permissionScenarioContext) { + sqlStore := db.InitTestDB(t) + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Dash", + }), + UserID: 10000, + Overwrite: true, + } + + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsAll}, + }, + } + _, err := callSaveWithResult(t, cmd, sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When creating a new dashboard in other folder, requires create permissions scoped to the other folder", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "Dash", + }), + FolderUID: sc.otherSavedFolder.UID, + UserID: 10000, + Overwrite: true, + } + + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID("different_folder_uid")}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When creating a new dashboard by existing UID in folder, requires write permissions on the existing dashboard", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "New dash", + }), + FolderUID: sc.savedFolder.UID, + UserID: 10000, + Overwrite: true, + } + + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID("different_dash_uid")}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When moving a dashboard by existing uid to other folder from General folder, requires dashboard creation permissions on the destination folder and write access to the dashboard", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInGeneralFolder.UID, + "title": "Dash", + }), + FolderUID: sc.otherSavedFolder.UID, + UserID: 10000, + Overwrite: true, + } + + // Perms to write dashboard but not create dashboards in the destination folder + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInGeneralFolder.UID)}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to create dashboards in the destination folder but not write the dashboard + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to write dashboard and create dashboards in the destination folder + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInGeneralFolder.UID)}, + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(sc.otherSavedFolder.UID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Nil(t, err) + }) + + permissionScenario(t, "When moving a dashboard by existing uid to the General folder from other folder, requires dashboard creation permissions on the general folder and write access to the dashboard", func(t *testing.T, sc *permissionScenarioContext) { + cmd := dashboards.SaveDashboardCommand{ + OrgID: testOrgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "uid": sc.savedDashInFolder.UID, + "title": "Dash", + }), + FolderUID: "", + UserID: 10000, + Overwrite: true, + } + + // Perms to write dashboard but not create dashboards in the destination folder + permissions := map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, + }, + } + _, err := callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to create dashboards in the destination folder but not write the dashboard + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) + + // Perms to write dashboard and create dashboards in the destination folder + permissions = map[int64]map[string][]string{ + testOrgID: { + dashboards.ActionDashboardsWrite: {dashboards.ScopeDashboardsProvider.GetResourceScopeUID(sc.savedDashInFolder.UID)}, + dashboards.ActionDashboardsCreate: {dashboards.ScopeFoldersProvider.GetResourceScopeUID(accesscontrol.GeneralFolderUID)}, + }, + } + _, err = callSaveWithResult(t, cmd, sc.sqlStore, permissions) + assert.NoError(t, err) + }) + }) +} + type permissionScenarioContext struct { - dashboardGuardianMock *guardian.FakeDashboardGuardian sqlStore db.DB dashboardStore dashboards.Store savedFolder *dashboards.Dashboard @@ -862,14 +761,9 @@ type permissionScenarioContext struct { type permissionScenarioFunc func(t *testing.T, sc *permissionScenarioContext) -func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionScenarioFunc) { +func permissionScenario(t *testing.T, desc string, fn permissionScenarioFunc) { t.Helper() - guardianMock := &guardian.FakeDashboardGuardian{ - CanSaveValue: canSave, - CanViewValue: true, - } - t.Run(desc, func(t *testing.T) { features := featuremgmt.WithFeatures() cfg := setting.NewCfg() @@ -922,7 +816,6 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc ) dashboardService.RegisterDashboardPermissions(dashboardPermissions) require.NoError(t, err) - guardian.InitAccessControlGuardian(cfg, ac, dashboardService, folderService, log.NewNopLogger()) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.UID, sqlStore) @@ -942,14 +835,7 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc require.False(t, savedDashInFolder.IsFolder) require.NotEmpty(t, savedDashInFolder.UID) - origNewDashboardGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewDashboardGuardian - }) - guardian.MockDashboardGuardian(guardianMock) - sc := &permissionScenarioContext{ - dashboardGuardianMock: guardianMock, sqlStore: sqlStore, savedDashInFolder: savedDashInFolder, otherSavedFolder: otherSavedFolder, @@ -962,11 +848,17 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc }) } -func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) *dashboards.Dashboard { +func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB, permissions map[int64]map[string][]string) (*dashboards.Dashboard, error) { t.Helper() features := featuremgmt.WithFeatures() dto := toSaveDashboardDto(cmd) + var ac accesscontrol.AccessControl + ac = actest.FakeAccessControl{ExpectedEvaluate: true} + if permissions != nil { + dto.User = &user.SignedInUser{UserID: cmd.UserID, OrgID: testOrgID, Permissions: permissions} + ac = acimpl.ProvideAccessControl(features) + } cfg := setting.NewCfg() quotaService := quotatest.New(false, nil) dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) @@ -1002,7 +894,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, - actest.FakeAccessControl{}, + ac, folderService, folder.NewFakeStore(), nil, @@ -1017,61 +909,7 @@ func callSaveWithResult(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlSt ) require.NoError(t, err) service.RegisterDashboardPermissions(dashboardPermissions) - res, err := service.SaveDashboard(context.Background(), &dto, false) - require.NoError(t, err) - - return res -} - -func callSaveWithError(t *testing.T, cmd dashboards.SaveDashboardCommand, sqlStore db.DB) error { - features := featuremgmt.WithFeatures() - dto := toSaveDashboardDto(cmd) - cfg := setting.NewCfg() - quotaService := quotatest.New(false, nil) - dashboardStore, err := database.ProvideDashboardStore(sqlStore, cfg, features, tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - folderStore := folderimpl.ProvideDashboardFolderStore(sqlStore) - tracer := tracing.InitializeTracerForTest() - publicDashboardFakeService := publicdashboards.NewFakePublicDashboardServiceWrapper(t) - folderStore2 := folderimpl.ProvideStore(sqlStore) - folderService := folderimpl.ProvideService(folderStore2, - actest.FakeAccessControl{ExpectedEvaluate: true}, - bus.ProvideBus(tracer), - dashboardStore, - folderStore, - nil, - sqlStore, - features, - supportbundlestest.NewFakeBundleService(), - publicDashboardFakeService, - cfg, - nil, - tracer, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - ) - service, err := ProvideDashboardServiceImpl( - cfg, dashboardStore, folderStore, - featuremgmt.WithFeatures(), - accesscontrolmock.NewMockedPermissionsService(), - actest.FakeAccessControl{}, - folderService, - folder.NewFakeStore(), - nil, - client.MockTestRestConfig{}, - nil, - quotaService, - nil, - nil, - nil, - dualwrite.ProvideTestService(), - sort.ProvideService(), - ) - require.NoError(t, err) - service.RegisterDashboardPermissions(accesscontrolmock.NewMockedPermissionsService()) - _, err = service.SaveDashboard(context.Background(), &dto, false) - return err + return service.SaveDashboard(context.Background(), &dto, false) } func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string, sqlStore db.DB) *dashboards.Dashboard { @@ -1127,7 +965,7 @@ func saveTestDashboard(t *testing.T, title string, orgID int64, folderUID string cfg, dashboardStore, folderStore, features, accesscontrolmock.NewMockedPermissionsService(), - actest.FakeAccessControl{}, + actest.FakeAccessControl{ExpectedEvaluate: true}, folderService, folder.NewFakeStore(), nil, @@ -1206,7 +1044,7 @@ func saveTestFolder(t *testing.T, title string, orgID int64, sqlStore db.DB) *da cfg, dashboardStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, - actest.FakeAccessControl{}, + actest.FakeAccessControl{ExpectedEvaluate: true}, folderService, folder.NewFakeStore(), nil, diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index 2223eabb9d7..0f68c97d2ec 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -20,13 +20,13 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/apiserver/client" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/folder/foldertest" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/publicdashboards" @@ -50,6 +50,7 @@ func TestDashboardService(t *testing.T) { log: log.New("test.logger"), dashboardStore: &fakeStore, folderService: folderSvc, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, features: featuremgmt.WithFeatures(), publicDashboardService: fakePublicDashboardService, } @@ -57,10 +58,6 @@ func TestDashboardService(t *testing.T) { folderStore.On("GetFolderByUID", mock.Anything, mock.AnythingOfType("int64"), mock.AnythingOfType("string")).Return(nil, dashboards.ErrFolderNotFound).Once() service.folderStore = &folderStore - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - t.Run("Save dashboard validation", func(t *testing.T) { dto := &dashboards.SaveDashboardDTO{} @@ -1292,13 +1289,10 @@ func TestSetDefaultPermissionsWhenSavingFolderForProvisionedDashboards(t *testin UID: "general", }, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, log: log.NewNopLogger(), } - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - cmd := &folder.CreateFolderCommand{ Title: "foo", OrgID: 1, @@ -1326,13 +1320,10 @@ func TestSaveProvisionedDashboard(t *testing.T) { UID: "general", }, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, log: log.NewNopLogger(), } - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - query := &dashboards.SaveDashboardDTO{ OrgID: 1, User: &user.SignedInUser{UserID: 1}, @@ -1392,12 +1383,9 @@ func TestSaveDashboard(t *testing.T) { folderService: &foldertest.FakeService{ ExpectedFolder: &folder.Folder{}, }, + ac: actest.FakeAccessControl{ExpectedEvaluate: true}, } - origNewDashboardGuardian := guardian.New - defer func() { guardian.New = origNewDashboardGuardian }() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true}) - query := &dashboards.SaveDashboardDTO{ OrgID: 1, User: &user.SignedInUser{UserID: 1}, diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 1ae4797a45f..67feb554cf2 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -29,7 +29,6 @@ import ( dashsvc "github.com/grafana/grafana/pkg/services/dashboards/service" "github.com/grafana/grafana/pkg/services/featuremgmt" "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/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" @@ -1392,7 +1391,7 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { testDB, cfg := db.InitTestDBWithCfg(t) dashStore, err := dashboardsDB.ProvideDashboardStore(testDB, cfg, features, tagimpl.ProvideService(testDB)) require.NoError(t, err) - ac := acmock.New() + ac := actest.FakeAccessControl{ExpectedEvaluate: true} fStore := folderimpl.ProvideStore(testDB) folderPermissions := acmock.NewMockedPermissionsService() @@ -1404,12 +1403,6 @@ func TestPublicDashboardServiceImpl_ListPublicDashboards(t *testing.T) { dashboardService, err := dashsvc.ProvideDashboardServiceImpl(cfg, dashStore, folderStore, featuremgmt.WithFeatures(), folderPermissions, ac, folderSvc, fStore, nil, client.MockTestRestConfig{}, nil, quotatest.New(false, nil), nil, nil, nil, dualwrite.ProvideTestService(), sort.ProvideService()) require.NoError(t, err) dashboardService.RegisterDashboardPermissions(&actest.FakePermissionsService{}) - fakeGuardian := &guardian.FakeDashboardGuardian{ - CanSaveValue: true, - CanEditUIDs: []string{}, - CanViewUIDs: []string{}, - } - guardian.MockDashboardGuardian(fakeGuardian) // insert in test data so we can check that permissions are working properly through the dashboard service // this will create 4 dashboards and 3 users