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
pull/102025/head
Ieva 9 months ago committed by GitHub
parent ad71270ee0
commit 163546d40f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 25
      pkg/api/api.go
  2. 96
      pkg/api/dashboard.go
  3. 137
      pkg/api/dashboard_test.go
  4. 10
      pkg/services/annotations/accesscontrol/accesscontrol_test.go
  5. 11
      pkg/services/annotations/annotationsimpl/annotations_test.go
  6. 106
      pkg/services/dashboards/service/dashboard_service.go
  7. 1266
      pkg/services/dashboards/service/dashboard_service_integration_test.go
  8. 22
      pkg/services/dashboards/service/dashboard_service_test.go
  9. 9
      pkg/services/publicdashboards/service/service_test.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))

@ -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 {

@ -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()
expectedDashVersions := []*dashver.DashboardVersionDTO{
{Data: simplejson.NewFromAny(map[string]any{"title": "Dash"})},
{Data: simplejson.NewFromAny(map[string]any{"title": "Dash updated"})},
}
hs.dashboardVersionService = &dashvertest.FakeDashboardVersionService{
ExpectedListDashboarVersions: []*dashver.DashboardVersionDTO{},
ExpectedDashboardVersions: expectedDashVersions,
ExpectedDashboardVersion: &dashver.DashboardVersionDTO{},
}
guardian.InitAccessControlGuardian(hs.Cfg, hs.AccessControl, hs.DashboardService, hs.folderService, log.NewNopLogger())
})
}
@ -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,

@ -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())

@ -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(

@ -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,7 @@ 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
}
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
}
@ -428,33 +420,16 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
}
}
if validateProvisionedDashboard {
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID)
if err != nil {
return nil, err
}
if provisionedData != nil {
return nil, dashboards.ErrDashboardCannotSaveProvisionedDashboard
}
}
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 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 := guard.CanSave(); err != nil || !canSave {
if canSave, err := dr.canSaveDashboard(ctx, dto.User, dash); err != nil || !canSave {
if err != nil {
return nil, err
}
@ -462,6 +437,17 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d
}
}
if validateProvisionedDashboard {
provisionedData, err := dr.GetProvisionedDashboardDataByDashboardID(ctx, dash.ID)
if err != nil {
return nil, err
}
if provisionedData != nil {
return nil, dashboards.ErrDashboardCannotSaveProvisionedDashboard
}
}
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

@ -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},

@ -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

Loading…
Cancel
Save