diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 12deadcc9d5..f5f2ee9961b 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -7,14 +7,17 @@ import ( "github.com/grafana/grafana/pkg/api/routing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var ( @@ -46,9 +49,13 @@ func setUp(confs ...setUpConf) *HTTPServer { aclMockResp = c.aclMockResp } } - store.ExpectedDashboardAclInfoList = aclMockResp store.ExpectedTeamsByUser = []*models.TeamDTO{} - guardian.InitLegacyGuardian(store) + dashSvc := &dashboards.FakeDashboardService{} + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = aclMockResp + }).Return(nil) + guardian.InitLegacyGuardian(store, dashSvc) return hs } diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 23e51dc5291..b5cc9aa232b 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -1000,15 +1000,18 @@ func TestAPI_MassDeleteAnnotations_AccessControl(t *testing.T) { func setUpACL() { viewerRole := models.ROLE_VIEWER editorRole := models.ROLE_EDITOR - - aclMockResp := []*models.DashboardAclInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } store := mockstore.NewSQLStoreMock() - store.ExpectedDashboardAclInfoList = aclMockResp store.ExpectedTeamsByUser = []*models.TeamDTO{} - guardian.InitLegacyGuardian(store) + dashSvc := &dashboards.FakeDashboardService{} + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + }).Return(nil) + + guardian.InitLegacyGuardian(store, dashSvc) } func setUpRBACGuardian(t *testing.T) { diff --git a/pkg/api/api.go b/pkg/api/api.go index 9e755fcffe1..eb3fc87bf47 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -28,7 +28,7 @@ func (hs *HTTPServer) registerRoutes() { reqGrafanaAdmin := middleware.ReqGrafanaAdmin reqEditorRole := middleware.ReqEditorRole reqOrgAdmin := middleware.ReqOrgAdmin - reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin(hs.SQLStore) + reqOrgAdminFolderAdminOrTeamAdmin := middleware.OrgAdminFolderAdminOrTeamAdmin(hs.SQLStore, hs.dashboardService) reqCanAccessTeams := middleware.AdminOrEditorAndFeatureEnabled(hs.Cfg.EditorsCanAdmin) reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg) redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg) diff --git a/pkg/api/dashboard_public_config.go b/pkg/api/dashboard_public_config.go index 5aa785d3d2a..7dc903998d1 100644 --- a/pkg/api/dashboard_public_config.go +++ b/pkg/api/dashboard_public_config.go @@ -2,7 +2,6 @@ package api import ( "errors" - "fmt" "net/http" "github.com/grafana/grafana/pkg/api/response" @@ -42,8 +41,6 @@ func (hs *HTTPServer) SavePublicDashboard(c *models.ReqContext) response.Respons pdc, err := hs.dashboardService.SavePublicDashboardConfig(c.Req.Context(), &dto) - fmt.Println("err:", err) - if errors.Is(err, models.ErrDashboardNotFound) { return response.Error(http.StatusNotFound, "dashboard not found", err) } diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index d34d285c46d..538e49ed305 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -8,13 +8,16 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { @@ -29,10 +32,9 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { jsonModel, err := simplejson.NewJson([]byte(`{"id":100}`)) require.NoError(t, err) - viewerRole := models.ROLE_VIEWER - editorRole := models.ROLE_EDITOR sqlmock := mockstore.NewSQLStoreMock() - aclMockResp := []*models.DashboardAclInfoDTO{} + dashSvc := &dashboards.FakeDashboardService{} + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Return(nil) hs := &HTTPServer{DashboardsnapshotsService: &dashboardsnapshots.Service{SQLStore: sqlmock}} setUpSnapshotTest := func(t *testing.T) *models.DashboardSnapshot { @@ -47,7 +49,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { External: true, } sqlmock.ExpectedDashboardSnapshot = mockSnapshotResult - sqlmock.ExpectedDashboardAclInfoList = aclMockResp sqlmock.ExpectedTeamsByUser = []*models.TeamDTO{} return mockSnapshotResult @@ -63,7 +64,7 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { }) mockSnapshotResult.ExternalDeleteUrl = ts.URL sc.handlerFunc = hs.DeleteDashboardSnapshot - guardian.InitLegacyGuardian(sc.sqlStore) + guardian.InitLegacyGuardian(sc.sqlStore, dashSvc) sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec() assert.Equal(t, 403, sc.resp.Code) @@ -100,15 +101,19 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { }) t.Run("When user is editor and dashboard has default ACL", func(t *testing.T) { - aclMockResp = []*models.DashboardAclInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } + dashSvc := &dashboards.FakeDashboardService{} + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + }).Return(nil) loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { mockSnapshotResult := setUpSnapshotTest(t) - + guardian.InitLegacyGuardian(sc.sqlStore, dashSvc) var externalRequest *http.Request ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) { rw.WriteHeader(200) @@ -130,11 +135,9 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { }) t.Run("When user is editor and creator of the snapshot", func(t *testing.T) { - aclMockResp := []*models.DashboardAclInfoDTO{} loggedInUserScenarioWithRole(t, "Should be able to delete a snapshot when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { mockSnapshotResult := setUpSnapshotTest(t) - sqlmock.ExpectedDashboardAclInfoList = aclMockResp mockSnapshotResult.UserId = testUserID mockSnapshotResult.External = false @@ -151,7 +154,6 @@ func TestDashboardSnapshotAPIEndpoint_singleSnapshot(t *testing.T) { }) t.Run("When deleting an external snapshot", func(t *testing.T) { - aclMockResp = []*models.DashboardAclInfoDTO{} loggedInUserScenarioWithRole(t, "Should gracefully delete local snapshot when remote snapshot has already been removed when calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", models.ROLE_EDITOR, func(sc *scenarioContext) { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index b42602197b9..71944379016 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -133,7 +133,6 @@ func TestDashboardAPIEndpoint(t *testing.T) { q := args.Get(1).(*models.GetDashboardQuery) q.Result = fakeDash }).Return(nil) - mockSQLStore := mockstore.NewSQLStoreMock() hs := &HTTPServer{ @@ -150,13 +149,14 @@ func TestDashboardAPIEndpoint(t *testing.T) { setUp := func() { viewerRole := models.ROLE_VIEWER editorRole := models.ROLE_EDITOR - - aclMockResp := []*models.DashboardAclInfoDTO{ - {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, - {Role: &editorRole, Permission: models.PERMISSION_EDIT}, - } - mockSQLStore.ExpectedDashboardAclInfoList = aclMockResp - guardian.InitLegacyGuardian(mockSQLStore) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {Role: &viewerRole, Permission: models.PERMISSION_VIEW}, + {Role: &editorRole, Permission: models.PERMISSION_EDIT}, + } + }).Return(nil) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) } // This tests two scenarios: @@ -239,6 +239,16 @@ func TestDashboardAPIEndpoint(t *testing.T) { q := args.Get(1).(*models.GetDashboardQuery) q.Result = fakeDash }).Return(nil) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + { + DashboardId: 1, + Permission: models.PERMISSION_EDIT, + UserId: 200, + }, + } + }).Return(nil) mockSQLStore := mockstore.NewSQLStoreMock() cfg := setting.NewCfg() @@ -262,17 +272,7 @@ func TestDashboardAPIEndpoint(t *testing.T) { setting.ViewersCanEdit = origCanEdit }) setting.ViewersCanEdit = false - - aclMockResp := []*models.DashboardAclInfoDTO{ - { - DashboardId: 1, - Permission: models.PERMISSION_EDIT, - UserId: 200, - }, - } - - mockSQLStore.ExpectedDashboardAclInfoList = aclMockResp - guardian.InitLegacyGuardian(mockSQLStore) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) } // This tests six scenarios: @@ -362,13 +362,21 @@ func TestDashboardAPIEndpoint(t *testing.T) { t.Run("When user is an Org Viewer but has an edit permission", func(t *testing.T) { role := models.ROLE_VIEWER - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_EDIT}, - } - setUpInner := func() { - setUp() - mockSQLStore.ExpectedDashboardAclInfoList = mockResult + origCanEdit := setting.ViewersCanEdit + t.Cleanup(func() { + setting.ViewersCanEdit = origCanEdit + }) + setting.ViewersCanEdit = false + + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_EDIT}, + } + }).Return(nil) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) } loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", @@ -417,18 +425,20 @@ func TestDashboardAPIEndpoint(t *testing.T) { role := models.ROLE_VIEWER setUpInner := func() { - setUp() - - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, - } - mockSQLStore.ExpectedDashboardAclInfoList = mockResult - origCanEdit := setting.ViewersCanEdit t.Cleanup(func() { setting.ViewersCanEdit = origCanEdit }) setting.ViewersCanEdit = true + + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, + } + }).Return(nil) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) } loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { @@ -455,12 +465,20 @@ func TestDashboardAPIEndpoint(t *testing.T) { role := models.ROLE_VIEWER setUpInner := func() { - setUp() + origCanEdit := setting.ViewersCanEdit + t.Cleanup(func() { + setting.ViewersCanEdit = origCanEdit + }) + setting.ViewersCanEdit = true - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_ADMIN}, - } - mockSQLStore.ExpectedDashboardAclInfoList = mockResult + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_ADMIN}, + } + }).Return(nil) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) } loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { @@ -506,12 +524,14 @@ func TestDashboardAPIEndpoint(t *testing.T) { role := models.ROLE_EDITOR setUpInner := func() { - setUp() - - mockResult := []*models.DashboardAclInfoDTO{ - {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, - } - mockSQLStore.ExpectedDashboardAclInfoList = mockResult + dashboardService := dashboards.NewFakeDashboardService(t) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {OrgId: 1, DashboardId: 2, UserId: 1, Permission: models.PERMISSION_VIEW}, + } + }).Return(nil) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) } loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { @@ -735,8 +755,9 @@ func TestDashboardAPIEndpoint(t *testing.T) { } sqlmock := mockstore.SQLStoreMock{ExpectedDashboardVersions: dashboardvs} setUp := func() { - mockResult := []*models.DashboardAclInfoDTO{} - sqlmock.ExpectedDashboardAclInfoList = mockResult + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Return(nil) + guardian.InitLegacyGuardian(&sqlmock, dashSvc) } cmd := dtos.CalculateDiffOptions{ @@ -763,10 +784,8 @@ func TestDashboardAPIEndpoint(t *testing.T) { t.Run("when user does have permission", func(t *testing.T) { role := models.ROLE_ADMIN - postDiffScenario(t, "When calling POST on", "/api/dashboards/calculate-diff", "/api/dashboards/calculate-diff", cmd, role, func(sc *scenarioContext) { - setUp() - + // This test shouldn't hit GetDashboardAclInfoList, so no setup needed sc.dashboardVersionService = fakeDashboardVersionService callPostDashboard(sc) assert.Equal(t, 200, sc.resp.Code) @@ -857,40 +876,35 @@ func TestDashboardAPIEndpoint(t *testing.T) { t.Run("Given provisioned dashboard", func(t *testing.T) { mockSQLStore := mockstore.NewSQLStoreMock() - setUp := func() { - mockSQLStore.ExpectedDashboardAclInfoList = []*models.DashboardAclInfoDTO{ - {OrgId: testOrgID, DashboardId: 1, UserId: testUserID, Permission: models.PERMISSION_EDIT}, - } - } + dashboardStore := dashboards.NewFakeDashboardStore(t) + dashboardStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{ExternalId: "/dashboard1.json"}, nil).Once() + + dashboardService := dashboards.NewFakeDashboardService(t) dataValue, err := simplejson.NewJson([]byte(`{"id": 1, "editable": true, "style": "dark"}`)) require.NoError(t, err) - - dashboardService := dashboards.NewFakeDashboardService(t) dashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*models.GetDashboardQuery")).Run(func(args mock.Arguments) { q := args.Get(1).(*models.GetDashboardQuery) q.Result = &models.Dashboard{Id: 1, Data: dataValue} }).Return(nil) + dashboardService.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{{OrgId: testOrgID, DashboardId: 1, UserId: testUserID, Permission: models.PERMISSION_EDIT}} + }).Return(nil) + guardian.InitLegacyGuardian(mockSQLStore, dashboardService) loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { - setUp() fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" } - dashboardStore := &dashboards.FakeDashboardStore{} - defer dashboardStore.AssertExpectations(t) - - dashboardStore.On("GetProvisionedDataByDashboardID", mock.Anything).Return(&models.DashboardProvisioning{ExternalId: "/dashboard1.json"}, nil).Once() - dash := getDashboardShouldReturn200WithConfig(t, sc, fakeProvisioningService, dashboardStore, dashboardService) assert.Equal(t, "../../../dashboard1.json", dash.Meta.ProvisionedExternalId, mockSQLStore) }, mockSQLStore) loggedInUserScenarioWithRole(t, "When allowUiUpdates is true and calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", models.ROLE_EDITOR, func(sc *scenarioContext) { - setUp() fakeProvisioningService := provisioning.NewProvisioningServiceMock(context.Background()) fakeProvisioningService.GetDashboardProvisionerResolvedPathFunc = func(name string) string { return "/tmp/grafana/dashboards" diff --git a/pkg/api/index.go b/pkg/api/index.go index c279a12fc04..ea9c2536c43 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -670,7 +670,7 @@ func (hs *HTTPServer) buildAdminNavLinks(c *models.ReqContext) []*dtos.NavLink { func (hs *HTTPServer) editorInAnyFolder(c *models.ReqContext) bool { hasEditPermissionInFoldersQuery := models.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser} - if err := hs.SQLStore.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { + if err := hs.dashboardService.HasEditPermissionInFolders(c.Req.Context(), &hasEditPermissionInFoldersQuery); err != nil { return false } return hasEditPermissionInFoldersQuery.Result diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index a3b95627640..b95a2c46f74 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/middleware/cookies" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" @@ -191,14 +192,14 @@ func shouldForceLogin(c *models.ReqContext) bool { return forceLogin } -func OrgAdminFolderAdminOrTeamAdmin(ss sqlstore.Store) func(c *models.ReqContext) { +func OrgAdminFolderAdminOrTeamAdmin(ss sqlstore.Store, ds dashboards.DashboardService) func(c *models.ReqContext) { return func(c *models.ReqContext) { if c.OrgRole == models.ROLE_ADMIN { return } hasAdminPermissionInFoldersQuery := models.HasAdminPermissionInFoldersQuery{SignedInUser: c.SignedInUser} - if err := ss.HasAdminPermissionInFolders(c.Req.Context(), &hasAdminPermissionInFoldersQuery); err != nil { + if err := ds.HasAdminPermissionInFolders(c.Req.Context(), &hasAdminPermissionInFoldersQuery); err != nil { c.JsonApiErr(500, "Failed to check if user is a folder admin", err) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 58dda45f24a..af229e6ddaa 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -7,6 +7,7 @@ import ( "time" "github.com/gosimple/slug" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -451,14 +452,6 @@ type GetDashboardsQuery struct { Result []*Dashboard } -type GetDashboardPermissionsForUserQuery struct { - DashboardIds []int64 - OrgId int64 - UserId int64 - OrgRole RoleType - Result []*DashboardPermissionForUser -} - type GetDashboardsByPluginIdQuery struct { OrgId int64 PluginId string @@ -477,12 +470,6 @@ type GetDashboardsBySlugQuery struct { Result []*Dashboard } -type DashboardPermissionForUser struct { - DashboardId int64 `json:"dashboardId"` - Permission PermissionType `json:"permission"` - PermissionName string `json:"permissionName"` -} - type DashboardRef struct { Uid string Slug string diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index 3797a355237..4af65426ec3 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -13,9 +13,12 @@ type DashboardService interface { DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error + GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error GetPublicDashboardConfig(ctx context.Context, orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) + HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error + HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) MakeUserAdmin(ctx context.Context, orgID int64, userID, dashboardID int64, setViewAndEditPermissions bool) error SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*models.Dashboard, error) @@ -49,6 +52,7 @@ type Store interface { DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *models.DeleteOrphanedProvisionedDashboardsCommand) error FindDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *models.GetDashboardQuery) error + GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error GetDashboards(ctx context.Context, query *models.GetDashboardsQuery) error // GetDashboardsByPluginID retrieves dashboards identified by plugin. @@ -57,6 +61,8 @@ type Store interface { GetProvisionedDataByDashboardID(dashboardID int64) (*models.DashboardProvisioning, error) GetProvisionedDataByDashboardUID(orgID int64, dashboardUID string) (*models.DashboardProvisioning, error) GetPublicDashboardConfig(orgId int64, dashboardUid string) (*models.PublicDashboardConfig, error) + HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error + HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error // SaveAlerts saves dashboard alerts. SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index f06cee26d8b..dee1a1756da 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -90,6 +90,20 @@ func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *models. return r0 } +// GetDashboardAclInfoList provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardAclInfoListQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetDashboardUIDById provides a mock function with given fields: ctx, query func (_m *FakeDashboardService) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { ret := _m.Called(ctx, query) @@ -141,6 +155,34 @@ func (_m *FakeDashboardService) GetPublicDashboardConfig(ctx context.Context, or return r0, r1 } +// HasAdminPermissionInFolders provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.HasAdminPermissionInFoldersQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HasEditPermissionInFolders provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.HasEditPermissionInFoldersQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ImportDashboard provides a mock function with given fields: ctx, dto func (_m *FakeDashboardService) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*models.Dashboard, error) { ret := _m.Called(ctx, dto) diff --git a/pkg/services/dashboards/database/acl.go b/pkg/services/dashboards/database/acl.go new file mode 100644 index 00000000000..940a2ab7aad --- /dev/null +++ b/pkg/services/dashboards/database/acl.go @@ -0,0 +1,150 @@ +package database + +import ( + "context" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +// GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three +// different places. +// 1) Permissions for the dashboard +// 2) permissions for its parent folder +// 3) if no specific permissions have been set for the dashboard or its parent folder then get the default permissions +func (d *DashboardStore) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { + outerErr := d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + query.Result = make([]*models.DashboardAclInfoDTO, 0) + falseStr := d.dialect.BooleanStr(false) + + if query.DashboardID == 0 { + sql := `SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + '' as user_login, + '' as user_email, + '' as team, + '' as title, + '' as slug, + '' as uid,` + + falseStr + ` AS is_folder,` + + falseStr + ` AS inherited + FROM dashboard_acl as da + WHERE da.dashboard_id = -1` + return dbSession.SQL(sql).Find(&query.Result) + } + + rawSQL := ` + -- get permissions for the dashboard and its parent folder + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + u.login AS user_login, + u.email AS user_email, + ug.name AS team, + ug.email AS team_email, + d.title, + d.slug, + d.uid, + d.is_folder, + CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + d.dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + d.dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL + ORDER BY da.id ASC + ` + + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result) + }) + + if outerErr != nil { + return outerErr + } + + for _, p := range query.Result { + p.PermissionName = p.Permission.String() + } + + return nil +} + +// HasEditPermissionInFolders validates that an user have access to a certain folder +func (d *DashboardStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { + return d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + if query.SignedInUser.HasRole(models.ROLE_EDITOR) { + query.Result = true + return nil + } + + builder := &sqlstore.SQLBuilder{} + builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", + query.SignedInUser.OrgId, d.dialect.BooleanStr(true)) + builder.WriteDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_EDIT) + + type folderCount struct { + Count int64 + } + + resp := make([]*folderCount, 0) + + if err := dbSession.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&resp); err != nil { + return err + } + + query.Result = len(resp) > 0 && resp[0].Count > 0 + + return nil + }) +} + +func (d *DashboardStore) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error { + return d.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + if query.SignedInUser.HasRole(models.ROLE_ADMIN) { + query.Result = true + return nil + } + + builder := &sqlstore.SQLBuilder{} + builder.Write("SELECT COUNT(dashboard.id) AS count FROM dashboard WHERE dashboard.org_id = ? AND dashboard.is_folder = ?", query.SignedInUser.OrgId, d.dialect.BooleanStr(true)) + builder.WriteDashboardPermissionFilter(query.SignedInUser, models.PERMISSION_ADMIN) + + type folderCount struct { + Count int64 + } + + resp := make([]*folderCount, 0) + if err := dbSession.SQL(builder.GetSQLString(), builder.GetParams()...).Find(&resp); err != nil { + return err + } + + query.Result = len(resp) > 0 && resp[0].Count > 0 + + return nil + }) +} diff --git a/pkg/services/dashboards/database/permissions/database_acl_test.go b/pkg/services/dashboards/database/acl_test.go similarity index 81% rename from pkg/services/dashboards/database/permissions/database_acl_test.go rename to pkg/services/dashboards/database/acl_test.go index c4e36ef3541..255efaa694d 100644 --- a/pkg/services/dashboards/database/permissions/database_acl_test.go +++ b/pkg/services/dashboards/database/acl_test.go @@ -1,31 +1,28 @@ //go:build integration // +build integration -package permissions +package database import ( "context" - "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/services/dashboards/database" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" "testing" - "time" - - "github.com/grafana/grafana/pkg/models" "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" ) func TestIntegrationDashboardAclDataAccess(t *testing.T) { var sqlStore *sqlstore.SQLStore var currentUser models.User var savedFolder, childDash *models.Dashboard - var dashboardStore *database.DashboardStore + var dashboardStore *DashboardStore setup := func(t *testing.T) { sqlStore = sqlstore.InitTestDB(t) - dashboardStore = database.ProvideDashboardStore(sqlStore) + dashboardStore = ProvideDashboardStore(sqlStore) currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, true, "prod", "webapp") childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.Id, false, "prod", "webapp") @@ -45,7 +42,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { setup(t) query := models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err := dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 2, len(query.Result)) @@ -62,7 +59,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { setup(t) query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err := dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 2, len(query.Result)) @@ -81,7 +78,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { require.Nil(t, err) query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err = dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 0, len(query.Result)) @@ -102,7 +99,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) { query := models.GetDashboardAclInfoListQuery{DashboardID: childDash.Id, OrgID: 1} - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err := dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 1, len(query.Result)) @@ -121,7 +118,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) { query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id} - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err := dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 2, len(query.Result)) @@ -145,7 +142,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { query := models.GetDashboardAclInfoListQuery{OrgID: 1, DashboardID: childDash.Id} - err = sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err = dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) defaultPermissionsId := int64(-1) @@ -171,7 +168,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { require.Nil(t, err) q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q1) + err = dashboardStore.GetDashboardAclInfoList(context.Background(), q1) require.Nil(t, err) require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) @@ -185,7 +182,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { require.Nil(t, err) q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q3) + err = dashboardStore.GetDashboardAclInfoList(context.Background(), q3) require.Nil(t, err) require.Equal(t, 0, len(q3.Result)) }) @@ -204,7 +201,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { require.Nil(t, err) q1 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q1) + err = dashboardStore.GetDashboardAclInfoList(context.Background(), q1) require.Nil(t, err) require.Equal(t, savedFolder.Id, q1.Result[0].DashboardId) require.Equal(t, models.PERMISSION_EDIT, q1.Result[0].Permission) @@ -224,7 +221,7 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { require.Nil(t, err) q3 := &models.GetDashboardAclInfoListQuery{DashboardID: savedFolder.Id, OrgID: 1} - err = sqlStore.GetDashboardAclInfoList(context.Background(), q3) + err = dashboardStore.GetDashboardAclInfoList(context.Background(), q3) require.Nil(t, err) require.Equal(t, 1, len(q3.Result)) require.Equal(t, savedFolder.Id, q3.Result[0].DashboardId) @@ -236,11 +233,11 @@ func TestIntegrationDashboardAclDataAccess(t *testing.T) { t.Run("Default permissions for root folder dashboards", func(t *testing.T) { setup(t) var rootFolderId int64 = 0 - sqlStore := sqlstore.InitTestDB(t) + //sqlStore := sqlstore.InitTestDB(t) query := models.GetDashboardAclInfoListQuery{DashboardID: rootFolderId, OrgID: 1} - err := sqlStore.GetDashboardAclInfoList(context.Background(), &query) + err := dashboardStore.GetDashboardAclInfoList(context.Background(), &query) require.Nil(t, err) require.Equal(t, 2, len(query.Result)) @@ -268,39 +265,3 @@ func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role str require.Equal(t, models.RoleType(role), q1.Result[0].Role) return *currentUser } - -func insertTestDashboard(t *testing.T, dashboardStore *database.DashboardStore, title string, orgId int64, - folderId int64, isFolder bool, tags ...interface{}) *models.Dashboard { - t.Helper() - cmd := models.SaveDashboardCommand{ - OrgId: orgId, - FolderId: folderId, - IsFolder: isFolder, - Dashboard: simplejson.NewFromAny(map[string]interface{}{ - "id": nil, - "title": title, - "tags": tags, - }), - } - dash, err := dashboardStore.SaveDashboard(cmd) - require.NoError(t, err) - require.NotNil(t, dash) - dash.Data.Set("id", dash.Id) - dash.Data.Set("uid", dash.Uid) - return dash -} - -func updateDashboardAcl(t *testing.T, dashboardStore *database.DashboardStore, dashboardID int64, - items ...models.DashboardAcl) error { - t.Helper() - - var itemPtrs []*models.DashboardAcl - for _, it := range items { - item := it - item.Created = time.Now() - item.Updated = time.Now() - itemPtrs = append(itemPtrs, &item) - } - - return dashboardStore.UpdateDashboardACL(context.Background(), dashboardID, itemPtrs) -} diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index dc1e07bf34c..f163f132e98 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -310,24 +310,6 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.Equal(t, query.Result[1].ID, folder2.Id) }) - t.Run("should have write access to all folders and dashboards", func(t *testing.T) { - query := models.GetDashboardPermissionsForUserQuery{ - DashboardIds: []int64{folder1.Id, folder2.Id}, - OrgId: 1, - UserId: adminUser.Id, - OrgRole: models.ROLE_ADMIN, - } - - err := sqlStore.GetDashboardPermissionsForUser(context.Background(), &query) - require.NoError(t, err) - - require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].DashboardId, folder1.Id) - require.Equal(t, query.Result[0].Permission, models.PERMISSION_ADMIN) - require.Equal(t, query.Result[1].DashboardId, folder2.Id) - require.Equal(t, query.Result[1].Permission, models.PERMISSION_ADMIN) - }) - t.Run("should have edit permission in folders", func(t *testing.T) { query := &models.HasEditPermissionInFoldersQuery{ SignedInUser: &models.SignedInUser{UserId: adminUser.Id, OrgId: 1, OrgRole: models.ROLE_ADMIN}, @@ -363,24 +345,6 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.Equal(t, query.Result[1].ID, folder2.Id) }) - t.Run("should have edit access to folders with default ACL", func(t *testing.T) { - query := models.GetDashboardPermissionsForUserQuery{ - DashboardIds: []int64{folder1.Id, folder2.Id}, - OrgId: 1, - UserId: editorUser.Id, - OrgRole: models.ROLE_EDITOR, - } - - err := sqlStore.GetDashboardPermissionsForUser(context.Background(), &query) - require.NoError(t, err) - - require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].DashboardId, folder1.Id) - require.Equal(t, query.Result[0].Permission, models.PERMISSION_EDIT) - require.Equal(t, query.Result[1].DashboardId, folder2.Id) - require.Equal(t, query.Result[1].Permission, models.PERMISSION_EDIT) - }) - t.Run("Should have write access to one dashboard folder if default role changed to view for one folder", func(t *testing.T) { err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: editorUser.Id, Permission: models.PERMISSION_VIEW, @@ -427,26 +391,6 @@ func TestIntegrationDashboardFolderDataAccess(t *testing.T) { require.Equal(t, len(query.Result), 0) }) - t.Run("should have view access to folders with default ACL", func(t *testing.T) { - setup3() - - query := models.GetDashboardPermissionsForUserQuery{ - DashboardIds: []int64{folder1.Id, folder2.Id}, - OrgId: 1, - UserId: viewerUser.Id, - OrgRole: models.ROLE_VIEWER, - } - - err := sqlStore.GetDashboardPermissionsForUser(context.Background(), &query) - require.NoError(t, err) - - require.Equal(t, len(query.Result), 2) - require.Equal(t, query.Result[0].DashboardId, folder1.Id) - require.Equal(t, query.Result[0].Permission, models.PERMISSION_VIEW) - require.Equal(t, query.Result[1].DashboardId, folder2.Id) - require.Equal(t, query.Result[1].Permission, models.PERMISSION_VIEW) - }) - t.Run("Should be able to get one dashboard folder if default role changed to edit for one folder", func(t *testing.T) { err := updateDashboardAcl(t, dashboardStore, folder1.Id, models.DashboardAcl{ DashboardID: folder1.Id, OrgID: 1, UserID: viewerUser.Id, Permission: models.PERMISSION_EDIT, diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index a035374fde5..b5c2900d6ac 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -586,3 +586,15 @@ func makeQueryResult(query *models.FindPersistedDashboardsQuery, res []dashboard } } } + +func (dr *DashboardServiceImpl) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { + return dr.dashboardStore.GetDashboardAclInfoList(ctx, query) +} + +func (dr *DashboardServiceImpl) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error { + return dr.dashboardStore.HasAdminPermissionInFolders(ctx, query) +} + +func (dr *DashboardServiceImpl) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { + return dr.dashboardStore.HasEditPermissionInFolders(ctx, query) +} diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index b3c0caf1420..650703067f9 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -815,8 +815,14 @@ func permissionScenario(t *testing.T, desc string, canSave bool, fn permissionSc t.Run(desc, func(t *testing.T) { sqlStore := sqlstore.InitTestDB(t) - guardian.InitLegacyGuardian(sqlStore) dashboardStore := database.ProvideDashboardStore(sqlStore) + service := ProvideDashboardService( + &setting.Cfg{}, dashboardStore, &dummyDashAlertExtractor{}, + featuremgmt.WithFeatures(), + accesscontrolmock.NewMockedPermissionsService(), + accesscontrolmock.NewMockedPermissionsService(), + ) + guardian.InitLegacyGuardian(sqlStore, service) savedFolder := saveTestFolder(t, "Saved folder", testOrgID, sqlStore) savedDashInFolder := saveTestDashboard(t, "Saved dash in folder", testOrgID, savedFolder.Id, sqlStore) diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index bce5a3c9825..7853271dd74 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -81,6 +81,20 @@ func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *models.Ge return r0 } +// GetDashboardAclInfoList provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.GetDashboardAclInfoListQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // GetDashboardUIDById provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { ret := _m.Called(ctx, query) @@ -284,6 +298,34 @@ func (_m *FakeDashboardStore) GetPublicDashboardConfig(orgId int64, dashboardUid return r0, r1 } +// HasAdminPermissionInFolders provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.HasAdminPermissionInFoldersQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// HasEditPermissionInFolders provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { + ret := _m.Called(ctx, query) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *models.HasEditPermissionInFoldersQuery) error); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // SaveAlerts provides a mock function with given fields: ctx, dashID, alerts func (_m *FakeDashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*models.Alert) error { ret := _m.Called(ctx, dashID, alerts) @@ -367,20 +409,6 @@ func (_m *FakeDashboardStore) SavePublicDashboardConfig(cmd models.SavePublicDas return r0, r1 } -// SearchDashboards provides a mock function with given fields: ctx, query -func (_m *FakeDashboardStore) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { - ret := _m.Called(ctx, query) - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.FindPersistedDashboardsQuery) error); ok { - r0 = rf(ctx, query) - } else { - r0 = ret.Error(0) - } - - return r0 -} - // UnprovisionDashboard provides a mock function with given fields: ctx, id func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 5b624bf73e9..4464d55e1bb 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -36,14 +37,15 @@ type DashboardGuardian interface { } type dashboardGuardianImpl struct { - user *models.SignedInUser - dashId int64 - orgId int64 - acl []*models.DashboardAclInfoDTO - teams []*models.TeamDTO - log log.Logger - ctx context.Context - store sqlstore.Store + user *models.SignedInUser + dashId int64 + orgId int64 + acl []*models.DashboardAclInfoDTO + teams []*models.TeamDTO + log log.Logger + ctx context.Context + store sqlstore.Store + dashboardService dashboards.DashboardService } // New factory for creating a new dashboard guardian instance @@ -52,14 +54,15 @@ var New = func(ctx context.Context, dashId int64, orgId int64, user *models.Sign panic("no guardian factory implementation provided") } -func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser, store sqlstore.Store) *dashboardGuardianImpl { +func newDashboardGuardian(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser, store sqlstore.Store, dashSvc dashboards.DashboardService) *dashboardGuardianImpl { return &dashboardGuardianImpl{ - user: user, - dashId: dashId, - orgId: orgId, - log: log.New("dashboard.permissions"), - ctx: ctx, - store: store, + user: user, + dashId: dashId, + orgId: orgId, + log: log.New("dashboard.permissions"), + ctx: ctx, + store: store, + dashboardService: dashSvc, } } @@ -222,7 +225,7 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*models.DashboardAclInfoDTO, error) } query := models.GetDashboardAclInfoListQuery{DashboardID: g.dashId, OrgID: g.orgId} - if err := g.store.GetDashboardAclInfoList(g.ctx, &query); err != nil { + if err := g.dashboardService.GetDashboardAclInfoList(g.ctx, &query); err != nil { return nil, err } g.acl = query.Result diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go index cc557ad5d58..fd74cbcc95c 100644 --- a/pkg/services/guardian/guardian_test.go +++ b/pkg/services/guardian/guardian_test.go @@ -7,11 +7,13 @@ import ( "runtime" "testing" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" - - "github.com/stretchr/testify/require" ) const ( @@ -683,11 +685,15 @@ func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShou func TestGuardianGetHiddenACL(t *testing.T) { t.Run("Get hidden ACL tests", func(t *testing.T) { store := mockstore.NewSQLStoreMock() - store.ExpectedDashboardAclInfoList = []*models.DashboardAclInfoDTO{ - {Inherited: false, UserId: 1, UserLogin: "user1", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserId: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, - } + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {Inherited: false, UserId: 1, UserLogin: "user1", Permission: models.PERMISSION_EDIT}, + {Inherited: false, UserId: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, + {Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, + } + }).Return(nil) cfg := setting.NewCfg() cfg.HiddenUsers = map[string]struct{}{"user2": {}} @@ -698,7 +704,7 @@ func TestGuardianGetHiddenACL(t *testing.T) { UserId: 1, Login: "user1", } - g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store) + g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc) hiddenACL, err := g.GetHiddenACL(cfg) require.NoError(t, err) @@ -714,7 +720,7 @@ func TestGuardianGetHiddenACL(t *testing.T) { Login: "user1", IsGrafanaAdmin: true, } - g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store) + g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, &dashboards.FakeDashboardService{}) hiddenACL, err := g.GetHiddenACL(cfg) require.NoError(t, err) @@ -727,17 +733,20 @@ func TestGuardianGetHiddenACL(t *testing.T) { func TestGuardianGetAclWithoutDuplicates(t *testing.T) { t.Run("Get hidden ACL tests", func(t *testing.T) { store := mockstore.NewSQLStoreMock() - - store.ExpectedDashboardAclInfoList = []*models.DashboardAclInfoDTO{ - {Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_EDIT}, - {Inherited: false, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserId: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, - {Inherited: true, UserId: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserId: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, - {Inherited: false, UserId: 5, UserLogin: "user5", Permission: models.PERMISSION_EDIT}, - {Inherited: true, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_VIEW}, - {Inherited: false, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, - } + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = []*models.DashboardAclInfoDTO{ + {Inherited: true, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_EDIT}, + {Inherited: false, UserId: 3, UserLogin: "user3", Permission: models.PERMISSION_VIEW}, + {Inherited: false, UserId: 2, UserLogin: "user2", Permission: models.PERMISSION_ADMIN}, + {Inherited: true, UserId: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, + {Inherited: false, UserId: 4, UserLogin: "user4", Permission: models.PERMISSION_ADMIN}, + {Inherited: false, UserId: 5, UserLogin: "user5", Permission: models.PERMISSION_EDIT}, + {Inherited: true, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_VIEW}, + {Inherited: false, UserId: 6, UserLogin: "user6", Permission: models.PERMISSION_EDIT}, + } + }).Return(nil) t.Run("Should get acl without duplicates", func(t *testing.T) { user := &models.SignedInUser{ @@ -745,7 +754,7 @@ func TestGuardianGetAclWithoutDuplicates(t *testing.T) { UserId: 1, Login: "user1", } - g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store) + g := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, dashSvc) acl, err := g.GetACLWithoutDuplicates() require.NoError(t, err) diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index de8717ac134..d0f3d6b427d 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -8,8 +8,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" ) @@ -38,7 +40,7 @@ func orgRoleScenario(desc string, t *testing.T, role models.RoleType, fn scenari OrgRole: role, } store := mockstore.NewSQLStoreMock() - guard := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store) + guard := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, &dashboards.FakeDashboardService{}) sc := &scenarioContext{ t: t, @@ -60,7 +62,7 @@ func apiKeyScenario(desc string, t *testing.T, role models.RoleType, fn scenario ApiKeyId: 10, } store := mockstore.NewSQLStoreMock() - guard := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store) + guard := newDashboardGuardian(context.Background(), dashboardID, orgID, user, store, &dashboards.FakeDashboardService{}) sc := &scenarioContext{ t: t, orgRoleScenario: desc, @@ -77,7 +79,6 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, permissions []*models.DashboardAclInfoDTO, fn scenarioFunc) { sc.t.Run(desc, func(t *testing.T) { store := mockstore.NewSQLStoreMock() - store.ExpectedDashboardAclInfoList = permissions teams := []*models.TeamDTO{} for _, p := range permissions { @@ -87,8 +88,14 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, } store.ExpectedTeamsByUser = teams + dashSvc := dashboards.NewFakeDashboardService(t) + dashSvc.On("GetDashboardAclInfoList", mock.Anything, mock.AnythingOfType("*models.GetDashboardAclInfoListQuery")).Run(func(args mock.Arguments) { + q := args.Get(1).(*models.GetDashboardAclInfoListQuery) + q.Result = permissions + }).Return(nil) + sc.permissionScenario = desc - sc.g = newDashboardGuardian(context.Background(), dashboardID, sc.givenUser.OrgId, sc.givenUser, store) + sc.g = newDashboardGuardian(context.Background(), dashboardID, sc.givenUser.OrgId, sc.givenUser, store, dashSvc) sc.givenDashboardID = dashboardID sc.givenPermissions = permissions sc.givenTeams = teams diff --git a/pkg/services/guardian/provider.go b/pkg/services/guardian/provider.go index 935a8362821..a26d1f1078e 100644 --- a/pkg/services/guardian/provider.go +++ b/pkg/services/guardian/provider.go @@ -20,14 +20,14 @@ func ProvideService( // TODO: Fix this hack, see https://github.com/grafana/grafana-enterprise/issues/2935 InitAccessControlGuardian(store, ac, folderPermissionsService, dashboardPermissionsService, dashboardService) } else { - InitLegacyGuardian(store) + InitLegacyGuardian(store, dashboardService) } return &Provider{} } -func InitLegacyGuardian(store sqlstore.Store) { +func InitLegacyGuardian(store sqlstore.Store, dashSvc dashboards.DashboardService) { New = func(ctx context.Context, dashId int64, orgId int64, user *models.SignedInUser) DashboardGuardian { - return newDashboardGuardian(ctx, dashId, orgId, user, store) + return newDashboardGuardian(ctx, dashId, orgId, user, store, dashSvc) } } diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index 741767997be..469f50aeeab 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -296,7 +296,7 @@ func validateAndUnMarshalArrayResponse(t *testing.T, resp response.Response) lib func scenarioWithPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { t.Helper() store := mockstore.NewSQLStoreMock() - guardian.InitLegacyGuardian(store) + guardian.InitLegacyGuardian(store, &dashboards.FakeDashboardService{}) testScenario(t, desc, func(t *testing.T, sc scenarioContext) { command := getCreatePanelCommand(sc.folder.Id, "Text - Library Panel") @@ -322,18 +322,17 @@ func testScenario(t *testing.T, desc string, fn func(t *testing.T, sc scenarioCo orgID := int64(1) role := models.ROLE_ADMIN sqlStore := sqlstore.InitTestDB(t) - guardian.InitLegacyGuardian(sqlStore) dashboardStore := database.ProvideDashboardStore(sqlStore) features := featuremgmt.WithFeatures() cfg := setting.NewCfg() cfg.IsFeatureToggleEnabled = features.IsEnabled folderPermissions := acmock.NewMockedPermissionsService() dashboardPermissions := acmock.NewMockedPermissionsService() - dashboardService := dashboardservice.ProvideDashboardService( cfg, dashboardStore, nil, features, folderPermissions, dashboardPermissions, ) + guardian.InitLegacyGuardian(sqlStore, dashboardService) ac := acmock.New() service := LibraryElementService{ Cfg: cfg, diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index d2a66ce69ac..6a590e09850 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -1429,7 +1429,7 @@ func updateFolderACL(t *testing.T, dashboardStore *database.DashboardStore, fold func scenarioWithLibraryPanel(t *testing.T, desc string, fn func(t *testing.T, sc scenarioContext)) { store := mockstore.NewSQLStoreMock() - guardian.InitLegacyGuardian(store) + guardian.InitLegacyGuardian(store, &dashboards.FakeDashboardService{}) t.Helper() testScenario(t, desc, func(t *testing.T, sc scenarioContext) { diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index 5d500baa12a..54f99c5a0c8 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -2,7 +2,6 @@ package sqlstore import ( "context" - "strings" "github.com/prometheus/client_golang/prometheus" @@ -42,78 +41,6 @@ func (ss *SQLStore) GetDashboardTags(ctx context.Context, query *models.GetDashb }) } -// GetDashboardPermissionsForUser returns the maximum permission the specified user has for a dashboard(s) -// The function takes in a list of dashboard ids and the user id and role -func (ss *SQLStore) GetDashboardPermissionsForUser(ctx context.Context, query *models.GetDashboardPermissionsForUserQuery) error { - return ss.WithDbSession(ctx, func(dbSession *DBSession) error { - if len(query.DashboardIds) == 0 { - return models.ErrCommandValidationFailed - } - - if query.OrgRole == models.ROLE_ADMIN { - var permissions = make([]*models.DashboardPermissionForUser, 0) - for _, d := range query.DashboardIds { - permissions = append(permissions, &models.DashboardPermissionForUser{ - DashboardId: d, - Permission: models.PERMISSION_ADMIN, - PermissionName: models.PERMISSION_ADMIN.String(), - }) - } - query.Result = permissions - - return nil - } - - params := make([]interface{}, 0) - - // check dashboards that have ACLs via user id, team id or role - sql := `SELECT d.id AS dashboard_id, MAX(COALESCE(da.permission, pt.permission)) AS permission - FROM dashboard AS d - LEFT JOIN dashboard_acl as da on d.folder_id = da.dashboard_id or d.id = da.dashboard_id - LEFT JOIN team_member as ugm on ugm.team_id = da.team_id - LEFT JOIN org_user ou ON ou.role = da.role AND ou.user_id = ? - ` - params = append(params, query.UserId) - - // check the user's role for dashboards that do not have hasAcl set - sql += `LEFT JOIN org_user ouRole ON ouRole.user_id = ? AND ouRole.org_id = ?` - params = append(params, query.UserId) - params = append(params, query.OrgId) - - sql += ` - LEFT JOIN (SELECT 1 AS permission, 'Viewer' AS role - UNION SELECT 2 AS permission, 'Editor' AS role - UNION SELECT 4 AS permission, 'Admin' AS role) pt ON ouRole.role = pt.role - WHERE - d.Id IN (?` + strings.Repeat(",?", len(query.DashboardIds)-1) + `) ` - for _, id := range query.DashboardIds { - params = append(params, id) - } - - sql += ` AND - d.org_id = ? AND - ( - (d.has_acl = ? AND (da.user_id = ? OR ugm.user_id = ? OR ou.id IS NOT NULL)) - OR (d.has_acl = ? AND ouRole.id IS NOT NULL) - ) - group by d.id - order by d.id asc` - params = append(params, query.OrgId) - params = append(params, dialect.BooleanStr(true)) - params = append(params, query.UserId) - params = append(params, query.UserId) - params = append(params, dialect.BooleanStr(false)) - - err := dbSession.SQL(sql, params...).Find(&query.Result) - - for _, p := range query.Result { - p.PermissionName = p.Permission.String() - } - - return err - }) -} - // HasEditPermissionInFolders validates that an user have access to a certain folder func (ss *SQLStore) HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error { return ss.WithDbSession(ctx, func(dbSession *DBSession) error { diff --git a/pkg/services/sqlstore/dashboard_acl.go b/pkg/services/sqlstore/dashboard_acl.go deleted file mode 100644 index df4a4fcf129..00000000000 --- a/pkg/services/sqlstore/dashboard_acl.go +++ /dev/null @@ -1,94 +0,0 @@ -package sqlstore - -import ( - "context" - - "github.com/grafana/grafana/pkg/models" -) - -// GetDashboardAclInfoList returns a list of permissions for a dashboard. They can be fetched from three -// different places. -// 1) Permissions for the dashboard -// 2) permissions for its parent folder -// 3) if no specific permissions have been set for the dashboard or its parent folder then get the default permissions -func (ss *SQLStore) GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error { - outerErr := ss.WithDbSession(ctx, func(dbSession *DBSession) error { - query.Result = make([]*models.DashboardAclInfoDTO, 0) - falseStr := dialect.BooleanStr(false) - - if query.DashboardID == 0 { - sql := `SELECT - da.id, - da.org_id, - da.dashboard_id, - da.user_id, - da.team_id, - da.permission, - da.role, - da.created, - da.updated, - '' as user_login, - '' as user_email, - '' as team, - '' as title, - '' as slug, - '' as uid,` + - falseStr + ` AS is_folder,` + - falseStr + ` AS inherited - FROM dashboard_acl as da - WHERE da.dashboard_id = -1` - return dbSession.SQL(sql).Find(&query.Result) - } - - rawSQL := ` - -- get permissions for the dashboard and its parent folder - SELECT - da.id, - da.org_id, - da.dashboard_id, - da.user_id, - da.team_id, - da.permission, - da.role, - da.created, - da.updated, - u.login AS user_login, - u.email AS user_email, - ug.name AS team, - ug.email AS team_email, - d.title, - d.slug, - d.uid, - d.is_folder, - CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited - FROM dashboard as d - LEFT JOIN dashboard folder on folder.id = d.folder_id - LEFT JOIN dashboard_acl AS da ON - da.dashboard_id = d.id OR - da.dashboard_id = d.folder_id OR - ( - -- include default permissions --> - da.org_id = -1 AND ( - (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR - (folder.id IS NULL AND d.has_acl = ` + falseStr + `) - ) - ) - LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id - LEFT JOIN team ug on ug.id = da.team_id - WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL - ORDER BY da.id ASC - ` - - return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result) - }) - - if outerErr != nil { - return outerErr - } - - for _, p := range query.Result { - p.PermissionName = p.Permission.String() - } - - return nil -} diff --git a/pkg/services/sqlstore/mockstore/mockstore.go b/pkg/services/sqlstore/mockstore/mockstore.go index 6285a75285c..120790a0095 100644 --- a/pkg/services/sqlstore/mockstore/mockstore.go +++ b/pkg/services/sqlstore/mockstore/mockstore.go @@ -429,15 +429,6 @@ func (m *SQLStoreMock) RemoveOrgUser(ctx context.Context, cmd *models.RemoveOrgU return testData.Response } -func (m *SQLStoreMock) SaveDashboard(cmd models.SaveDashboardCommand) (*models.Dashboard, error) { - return nil, m.ExpectedError -} - -func (m SQLStoreMock) SearchDashboards(ctx context.Context, query *models.FindPersistedDashboardsQuery) error { - query.Result = m.ExpectedPersistedDashboards - return m.ExpectedError -} - func (m *SQLStoreMock) GetDashboardTags(ctx context.Context, query *models.GetDashboardTagsQuery) error { return nil // TODO: Implement } @@ -447,10 +438,6 @@ func (m *SQLStoreMock) GetDashboards(ctx context.Context, query *models.GetDashb return m.ExpectedError } -func (m *SQLStoreMock) GetDashboardUIDById(ctx context.Context, query *models.GetDashboardRefByIdQuery) error { - return m.ExpectedError -} - func (m SQLStoreMock) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error { query.Result = m.ExpectedDatasource return m.ExpectedError @@ -614,10 +601,6 @@ func (m *SQLStoreMock) HasAdminPermissionInFolders(ctx context.Context, query *m return m.ExpectedError } -func (m *SQLStoreMock) GetDashboardPermissionsForUser(ctx context.Context, query *models.GetDashboardPermissionsForUserQuery) error { - return m.ExpectedError -} - func (m *SQLStoreMock) IsAdminOfTeams(ctx context.Context, query *models.IsAdminOfTeamsQuery) error { return m.ExpectedError } diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index ca9e6b14754..0acecf03971 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -9,12 +9,13 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" - "github.com/stretchr/testify/require" ) func TestIntegrationAccountDataAccess(t *testing.T) { @@ -301,7 +302,6 @@ func TestIntegrationAccountDataAccess(t *testing.T) { require.Equal(t, query.Result.Name, "ac2 name") require.Equal(t, query.Result.Login, "ac2") require.Equal(t, query.Result.OrgName, "ac1@test.com") - // require.Equal(t, query.Result.OrgRole, "Viewer") }) t.Run("Should set last org as current when removing user from current", func(t *testing.T) { @@ -390,7 +390,7 @@ func TestIntegrationAccountDataAccess(t *testing.T) { t.Run("Should remove dependent permissions for deleted org user", func(t *testing.T) { permQuery := &models.GetDashboardAclInfoListQuery{DashboardID: dash1.Id, OrgID: ac1.OrgId} - err = sqlStore.GetDashboardAclInfoList(context.Background(), permQuery) + err = getDashboardAclInfoList(sqlStore, permQuery) require.NoError(t, err) require.Equal(t, len(permQuery.Result), 0) @@ -399,7 +399,7 @@ func TestIntegrationAccountDataAccess(t *testing.T) { t.Run("Should not remove dashboard permissions for same user in another org", func(t *testing.T) { permQuery := &models.GetDashboardAclInfoListQuery{DashboardID: dash2.Id, OrgID: ac3.OrgId} - err = sqlStore.GetDashboardAclInfoList(context.Background(), permQuery) + err = getDashboardAclInfoList(sqlStore, permQuery) require.NoError(t, err) require.Equal(t, len(permQuery.Result), 1) @@ -455,6 +455,7 @@ func insertTestDashboard(t *testing.T, sqlStore *SQLStore, title string, orgId i Message: cmd.Message, Data: dash.Data, } + require.NoError(t, err) if affectedRows, err := sess.Insert(dashVersion); err != nil { return err @@ -502,3 +503,88 @@ func updateDashboardAcl(t *testing.T, sqlStore *SQLStore, dashboardID int64, ite }) return err } + +// This function was copied from pkg/services/dashboards/database to circumvent +// import cycles. When this org-related code is refactored into a service the +// tests can the real GetDashboardAclInfoList functions +func getDashboardAclInfoList(s *SQLStore, query *models.GetDashboardAclInfoListQuery) error { + outerErr := s.WithDbSession(context.Background(), func(dbSession *DBSession) error { + query.Result = make([]*models.DashboardAclInfoDTO, 0) + falseStr := dialect.BooleanStr(false) + + if query.DashboardID == 0 { + sql := `SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + '' as user_login, + '' as user_email, + '' as team, + '' as title, + '' as slug, + '' as uid,` + + falseStr + ` AS is_folder,` + + falseStr + ` AS inherited + FROM dashboard_acl as da + WHERE da.dashboard_id = -1` + return dbSession.SQL(sql).Find(&query.Result) + } + + rawSQL := ` + -- get permissions for the dashboard and its parent folder + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + u.login AS user_login, + u.email AS user_email, + ug.name AS team, + ug.email AS team_email, + d.title, + d.slug, + d.uid, + d.is_folder, + CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL + ORDER BY da.id ASC + ` + + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&query.Result) + }) + + if outerErr != nil { + return outerErr + } + + for _, p := range query.Result { + p.PermissionName = p.Permission.String() + } + + return nil +} diff --git a/pkg/services/sqlstore/store.go b/pkg/services/sqlstore/store.go index ba54cdbad08..5bcb7663f48 100644 --- a/pkg/services/sqlstore/store.go +++ b/pkg/services/sqlstore/store.go @@ -16,7 +16,6 @@ type Store interface { CreateDashboardSnapshot(ctx context.Context, cmd *models.CreateDashboardSnapshotCommand) error DeleteDashboardSnapshot(ctx context.Context, cmd *models.DeleteDashboardSnapshotCommand) error GetDashboardSnapshot(ctx context.Context, query *models.GetDashboardSnapshotQuery) error - HasEditPermissionInFolders(ctx context.Context, query *models.HasEditPermissionInFoldersQuery) error SearchDashboardSnapshots(ctx context.Context, query *models.GetDashboardSnapshotsQuery) error GetOrgByName(name string) (*models.Org, error) CreateOrg(ctx context.Context, cmd *models.CreateOrgCommand) error @@ -75,7 +74,6 @@ type Store interface { WithTransactionalDbSession(ctx context.Context, callback DBTransactionFunc) error InTransaction(ctx context.Context, fn func(ctx context.Context) error) error GetDashboardVersions(ctx context.Context, query *models.GetDashboardVersionsQuery) error - GetDashboardAclInfoList(ctx context.Context, query *models.GetDashboardAclInfoListQuery) error CreatePlaylist(ctx context.Context, cmd *models.CreatePlaylistCommand) error UpdatePlaylist(ctx context.Context, cmd *models.UpdatePlaylistCommand) error GetPlaylist(ctx context.Context, query *models.GetPlaylistByIdQuery) error @@ -134,7 +132,5 @@ type Store interface { ExpireOldUserInvites(ctx context.Context, cmd *models.ExpireTempUsersCommand) error GetDBHealthQuery(ctx context.Context, query *models.GetDBHealthQuery) error SearchOrgs(ctx context.Context, query *models.SearchOrgsQuery) error - HasAdminPermissionInFolders(ctx context.Context, query *models.HasAdminPermissionInFoldersQuery) error - GetDashboardPermissionsForUser(ctx context.Context, query *models.GetDashboardPermissionsForUserQuery) error IsAdminOfTeams(ctx context.Context, query *models.IsAdminOfTeamsQuery) error } diff --git a/pkg/services/sqlstore/team_test.go b/pkg/services/sqlstore/team_test.go index 0284b45c84e..f20e3840bf4 100644 --- a/pkg/services/sqlstore/team_test.go +++ b/pkg/services/sqlstore/team_test.go @@ -296,7 +296,7 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { require.Equal(t, err, models.ErrTeamNotFound) permQuery := &models.GetDashboardAclInfoListQuery{DashboardID: 1, OrgID: testOrgID} - err = sqlStore.GetDashboardAclInfoList(context.Background(), permQuery) + err = getDashboardAclInfoList(sqlStore, permQuery) require.NoError(t, err) require.Equal(t, len(permQuery.Result), 0) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 62f02da4ccc..31a3d1b9893 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -261,7 +261,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Len(t, query1.Result, 1) permQuery := &models.GetDashboardAclInfoListQuery{DashboardID: 1, OrgID: users[0].OrgId} - err = ss.GetDashboardAclInfoList(context.Background(), permQuery) + err = getDashboardAclInfoList(ss, permQuery) require.Nil(t, err) require.Len(t, permQuery.Result, 0) @@ -335,7 +335,7 @@ func TestIntegrationUserDataAccess(t *testing.T) { require.Len(t, query2.Result, 1) permQuery = &models.GetDashboardAclInfoListQuery{DashboardID: 1, OrgID: users[0].OrgId} - err = ss.GetDashboardAclInfoList(context.Background(), permQuery) + err = getDashboardAclInfoList(ss, permQuery) require.Nil(t, err) require.Len(t, permQuery.Result, 0)