diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 9cdb9169d83..4cbdbd9641c 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -11,13 +11,11 @@ import ( "path/filepath" "testing" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/models/usertoken" @@ -29,10 +27,8 @@ import ( "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/contexthandler" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" "github.com/grafana/grafana/pkg/services/featuremgmt" - "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/licensing" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/logintest" @@ -41,7 +37,6 @@ import ( "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/search/model" "github.com/grafana/grafana/pkg/services/searchusers" - "github.com/grafana/grafana/pkg/services/team/teamtest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" @@ -296,31 +291,10 @@ var ( editorRole = org.RoleEditor ) -type setUpConf struct { - aclMockResp []*dashboards.DashboardACLInfoDTO -} - type mockSearchService struct{ ExpectedResult model.HitList } func (mss *mockSearchService) SearchHandler(_ context.Context, q *search.Query) (model.HitList, error) { return mss.ExpectedResult, nil } + func (mss *mockSearchService) SortOptions() []model.SortOption { return nil } - -func setUp(confs ...setUpConf) *HTTPServer { - store := dbtest.NewFakeDB() - hs := &HTTPServer{SQLStore: store, SearchService: &mockSearchService{}} - - aclMockResp := []*dashboards.DashboardACLInfoDTO{} - for _, c := range confs { - if c.aclMockResp != nil { - aclMockResp = c.aclMockResp - } - } - teamSvc := &teamtest.FakeService{} - dashSvc := &dashboards.FakeDashboardService{} - qResult := aclMockResp - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) - guardian.InitLegacyGuardian(setting.NewCfg(), store, dashSvc, teamSvc) - return hs -} diff --git a/pkg/api/dashboard_permission.go b/pkg/api/dashboard_permission.go index 5f12cd7ef2e..ea2e6a00c5c 100644 --- a/pkg/api/dashboard_permission.go +++ b/pkg/api/dashboard_permission.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "net/http" "strconv" "time" @@ -10,9 +9,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/auth/identity" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/web" ) @@ -57,16 +57,7 @@ func (hs *HTTPServer) GetDashboardPermissionList(c *contextmodel.ReqContext) res return rsp } - g, err := guardian.NewByDashboard(c.Req.Context(), dash, c.OrgID, c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { - return dashboardGuardianResponse(err) - } - - acl, err := g.GetACLWithoutDuplicates() + acl, err := hs.getDashboardACL(c.Req.Context(), c.SignedInUser, dash) if err != nil { return response.Error(500, "Failed to get dashboard permissions", err) } @@ -147,15 +138,6 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) res return rsp } - g, err := guardian.NewByDashboard(c.Req.Context(), dash, c.OrgID, c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { - return dashboardGuardianResponse(err) - } - items := make([]*dashboards.DashboardACL, 0, len(apiCmd.Items)) for _, item := range apiCmd.Items { items = append(items, &dashboards.DashboardACL{ @@ -170,34 +152,102 @@ func (hs *HTTPServer) UpdateDashboardPermissions(c *contextmodel.ReqContext) res }) } - hiddenACL, err := g.GetHiddenACL(hs.Cfg) - if err != nil { - return response.Error(http.StatusInternalServerError, "Error while retrieving hidden permissions", err) - } - items = append(items, hiddenACL...) - - if okToUpdate, err := g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, items); err != nil || !okToUpdate { - if err != nil { - if errors.Is(err, guardian.ErrGuardianPermissionExists) || errors.Is(err, guardian.ErrGuardianOverride) { - return response.Error(http.StatusBadRequest, err.Error(), err) - } - - return response.Error(http.StatusInternalServerError, "Error while checking dashboard permissions", err) - } - - return response.Error(http.StatusForbidden, "Cannot remove own admin permission for a folder", nil) - } - - old, err := g.GetACL() + acl, err := hs.getDashboardACL(c.Req.Context(), c.SignedInUser, dash) if err != nil { return response.Error(http.StatusInternalServerError, "Error while checking dashboard permissions", err) } - if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgID, dash.UID, false, items, old); err != nil { + + items = append(items, hs.filterHiddenACL(c.SignedInUser, acl)...) + + if err := hs.updateDashboardAccessControl(c.Req.Context(), dash.OrgID, dash.UID, false, items, acl); err != nil { return response.Error(http.StatusInternalServerError, "Failed to update permissions", err) } + return response.Success("Dashboard permissions updated") } +var dashboardPermissionMap = map[string]dashboards.PermissionType{ + "View": dashboards.PERMISSION_VIEW, + "Edit": dashboards.PERMISSION_EDIT, + "Admin": dashboards.PERMISSION_ADMIN, +} + +func (hs *HTTPServer) getDashboardACL(ctx context.Context, user identity.Requester, dashboard *dashboards.Dashboard) ([]*dashboards.DashboardACLInfoDTO, error) { + permissions, err := hs.dashboardPermissionsService.GetPermissions(ctx, user, dashboard.UID) + if err != nil { + return nil, err + } + + acl := make([]*dashboards.DashboardACLInfoDTO, 0, len(permissions)) + for _, p := range permissions { + if !p.IsManaged { + continue + } + + var role *org.RoleType + if p.BuiltInRole != "" { + tmp := org.RoleType(p.BuiltInRole) + role = &tmp + } + + permission := dashboardPermissionMap[hs.dashboardPermissionsService.MapActions(p)] + + acl = append(acl, &dashboards.DashboardACLInfoDTO{ + OrgID: dashboard.OrgID, + DashboardID: dashboard.ID, + FolderID: dashboard.FolderID, + Created: p.Created, + Updated: p.Updated, + UserID: p.UserId, + UserLogin: p.UserLogin, + UserEmail: p.UserEmail, + TeamID: p.TeamId, + TeamEmail: p.TeamEmail, + Team: p.Team, + Role: role, + Permission: permission, + PermissionName: permission.String(), + UID: dashboard.UID, + Title: dashboard.Title, + Slug: dashboard.Slug, + IsFolder: dashboard.IsFolder, + URL: dashboard.GetURL(), + Inherited: false, + }) + } + + return acl, nil +} + +func (hs *HTTPServer) filterHiddenACL(user identity.Requester, acl []*dashboards.DashboardACLInfoDTO) []*dashboards.DashboardACL { + var hiddenACL []*dashboards.DashboardACL + + if user.GetIsGrafanaAdmin() { + return hiddenACL + } + + for _, item := range acl { + if item.Inherited || item.UserLogin == user.GetLogin() { + continue + } + + if _, hidden := hs.Cfg.HiddenUsers[item.UserLogin]; hidden { + hiddenACL = append(hiddenACL, &dashboards.DashboardACL{ + OrgID: item.OrgID, + DashboardID: item.DashboardID, + UserID: item.UserID, + TeamID: item.TeamID, + Role: item.Role, + Permission: item.Permission, + Created: item.Created, + Updated: item.Updated, + }) + } + } + + return hiddenACL +} + // updateDashboardAccessControl is used for api backward compatibility func (hs *HTTPServer) updateDashboardAccessControl(ctx context.Context, orgID int64, uid string, isFolder bool, items []*dashboards.DashboardACL, old []*dashboards.DashboardACLInfoDTO) error { commands := []accesscontrol.SetResourcePermissionCommand{} diff --git a/pkg/api/dashboard_permission_test.go b/pkg/api/dashboard_permission_test.go index 6e7100aed3e..3a89486f8ca 100644 --- a/pkg/api/dashboard_permission_test.go +++ b/pkg/api/dashboard_permission_test.go @@ -2,370 +2,158 @@ package api import ( "encoding/json" - "fmt" + "net/http" + "strings" "testing" "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/api/routing" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" - accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" - dashboardservice "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/folder/foldertest" - "github.com/grafana/grafana/pkg/services/guardian" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) -func TestDashboardPermissionAPIEndpoint(t *testing.T) { - t.Run("Dashboard permissions test", func(t *testing.T) { - settings := setting.NewCfg() - dashboardStore := &dashboards.FakeDashboardStore{} - qResult := &dashboards.Dashboard{} - dashboardStore.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Return(qResult, nil) - defer dashboardStore.AssertExpectations(t) +func TestHTTPServer_GetDashboardPermissionList(t *testing.T) { + t.Run("should not be able to list acl when user does not have permission to do so", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) {}) - features := featuremgmt.WithFeatures() - mockSQLStore := dbtest.NewFakeDB() - ac := accesscontrolmock.New() - folderPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - - folderSvc := folderimpl.ProvideService(ac, bus.ProvideBus(tracing.InitializeTracerForTest()), settings, dashboardStore, foldertest.NewFakeFolderStore(t), mockSQLStore, featuremgmt.WithFeatures()) - dashboardService, err := dashboardservice.ProvideDashboardServiceImpl( - settings, dashboardStore, foldertest.NewFakeFolderStore(t), nil, features, folderPermissions, dashboardPermissions, ac, - folderSvc, - ) + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/permissions"), userWithPermissions(1, nil))) require.NoError(t, err) - hs := &HTTPServer{ - Cfg: settings, - SQLStore: mockSQLStore, - Features: features, - DashboardService: dashboardService, - AccessControl: accesscontrolmock.New(), - dashboardPermissionsService: dashboardPermissions, - } + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) - t.Run("Given user has no admin permissions", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", - "/api/dashboards/id/:dashboardId/permissions", org.RoleEditor, func(sc *scenarioContext) { - callGetDashboardPermissions(sc, hs) - assert.Equal(t, 403, sc.resp.Code) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 403, sc.resp.Code) - }, - }, hs) - }) - - t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: true, - GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, Permission: dashboards.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, Permission: dashboards.PERMISSION_ADMIN}, - {OrgID: 1, DashboardID: 1, TeamID: 1, Permission: dashboards.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, TeamID: 2, Permission: dashboards.PERMISSION_ADMIN}, - }, - }) - - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", - "/api/dashboards/id/:dashboardId/permissions", org.RoleAdmin, func(sc *scenarioContext) { - callGetDashboardPermissions(sc, hs) - assert.Equal(t, 200, sc.resp.Code) - - var resp []*dashboards.DashboardACLInfoDTO - err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) - require.NoError(t, err) - - assert.Len(t, resp, 5) - assert.Equal(t, int64(2), resp[0].UserID) - assert.Equal(t, dashboards.PERMISSION_VIEW, resp[0].Permission) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil).Once() - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 200, sc.resp.Code) - }, - }, hs) - }) - - t.Run("When trying to add permissions with both a team and user", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: true, - }) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, TeamID: 1, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - respJSON, err := jsonMap(sc.resp.Body.Bytes()) - require.NoError(t, err) - assert.Equal(t, dashboards.ErrPermissionsWithUserAndTeamNotAllowed.Error(), respJSON["error"]) - }, - }, hs) - }) - - t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, - }) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - }, - }, hs) - }) - - t.Run("When trying to update team or user permissions with a role", func(t *testing.T) { - role := org.RoleEditor - cmds := []dtos.UpdateDashboardACLCommand{ - { - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, - }, - }, - { - Items: []dtos.DashboardACLUpdateItem{ - {TeamID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, - }, - }, - } - - for _, cmd := range cmds { - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - respJSON, err := jsonMap(sc.resp.Body.Bytes()) - require.NoError(t, err) - assert.Equal(t, dashboards.ErrPermissionsWithRoleNotAllowed.Error(), respJSON["error"]) - }, - }, hs) + t.Run("should be able to list acl with correct permission", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + svc := dashboards.NewFakeDashboardService(t) + svc.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1, UID: "1"}, nil) + hs.DashboardService = svc + hs.dashboardPermissionsService = &actest.FakePermissionsService{ + ExpectedPermissions: []accesscontrol.ResourcePermission{}, } }) - t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/permissions"), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsPermissionsRead, Scope: "dashboards:uid:1"}, + }))) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, - ) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, + t.Run("should filter out hidden users from acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + cfg := setting.NewCfg() + cfg.HiddenUsers = map[string]struct{}{"hidden": {}} + hs.Cfg = cfg + + svc := dashboards.NewFakeDashboardService(t) + svc.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1, UID: "1"}, nil) + + hs.DashboardService = svc + hs.dashboardPermissionsService = &actest.FakePermissionsService{ + ExpectedPermissions: []accesscontrol.ResourcePermission{ + {UserId: 1, UserLogin: "regular", IsManaged: true}, + {UserId: 2, UserLogin: "hidden", IsManaged: true}, }, } - - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateDashboardPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - }, - }, hs) }) - t.Run("Getting and updating dashboard permissions with hidden users", func(t *testing.T) { - origNewGuardian := guardian.New - settings.HiddenUsers = map[string]struct{}{ - "hiddenUser": {}, - testUserLogin: {}, - } - t.Cleanup(func() { - guardian.New = origNewGuardian - settings.HiddenUsers = make(map[string]struct{}) - }) + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/dashboards/uid/1/permissions"), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsPermissionsRead, Scope: "dashboards:uid:1"}, + }))) - mockSQLStore := dbtest.NewFakeDB() - var resp []*dashboards.DashboardACLInfoDTO - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/dashboards/id/1/permissions", - "/api/dashboards/id/:dashboardId/permissions", org.RoleAdmin, func(sc *scenarioContext) { - setUp() - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: true, - GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, UserLogin: "hiddenUser", Permission: dashboards.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, UserLogin: testUserLogin, Permission: dashboards.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, UserLogin: "user_1", Permission: dashboards.PERMISSION_ADMIN}, - }, - GetHiddenACLValue: []*dashboards.DashboardACL{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, - }, - }) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) - callGetDashboardPermissions(sc, hs) - assert.Equal(t, 200, sc.resp.Code) + var result []dashboards.DashboardACLInfoDTO + require.NoError(t, json.NewDecoder(res.Body).Decode(&result)) - err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) - require.NoError(t, err) - - assert.Len(t, resp, 2) - assert.Equal(t, int64(3), resp[0].UserID) - assert.Equal(t, dashboards.PERMISSION_EDIT, resp[0].Permission) - assert.Equal(t, int64(4), resp[1].UserID) - assert.Equal(t, dashboards.PERMISSION_ADMIN, resp[1].Permission) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - for _, acl := range resp { - cmd.Items = append(cmd.Items, dtos.DashboardACLUpdateItem{ - UserID: acl.UserID, - Permission: acl.Permission, - }) - } - assert.Len(t, cmd.Items, 3) - - dashboardPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil).Once() - updateDashboardPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/dashboards/id/1/permissions", - routePattern: "/api/dashboards/id/:dashboardId/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - assert.Equal(t, 200, sc.resp.Code) - }, - }, hs) - }) + assert.Len(t, result, 1) + assert.Equal(t, result[0].UserLogin, "regular") + require.NoError(t, res.Body.Close()) }) } -func callGetDashboardPermissions(sc *scenarioContext, hs *HTTPServer) { - sc.handlerFunc = hs.GetDashboardPermissionList - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() -} +func TestHTTPServer_UpdateDashboardPermissions(t *testing.T) { + t.Run("should not be able to update acl when user does not have permission to do so", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) {}) -func callUpdateDashboardPermissions(t *testing.T, sc *scenarioContext) { - t.Helper() - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() -} + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/uid/1/permissions", nil), userWithPermissions(1, nil))) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) -type updatePermissionContext struct { - desc string - url string - routePattern string - cmd dtos.UpdateDashboardACLCommand - fn scenarioFunc -} + t.Run("should be able to update acl with correct permissions", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + svc := dashboards.NewFakeDashboardService(t) + svc.On("GetDashboard", mock.Anything, mock.Anything).Return(&dashboards.Dashboard{ID: 1, UID: "1"}, nil) -func updateDashboardPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) { - t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { - sc := setupScenarioContext(t, ctx.url) - - sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { - c.Req.Body = mockRequestBody(ctx.cmd) - c.Req.Header.Add("Content-Type", "application/json") - sc.context = c - sc.context.OrgID = testOrgID - sc.context.UserID = testUserID - - return hs.UpdateDashboardPermissions(c) + hs.DashboardService = svc + hs.dashboardPermissionsService = &actest.FakePermissionsService{} }) - sc.m.Post(ctx.routePattern, sc.defaultHandler) + body := `{"items": []}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/uid/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:uid:1"}, + }))) - ctx.fn(sc) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("should not be able to specify team and user in same acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.DashboardService = dashboards.NewFakeDashboardService(t) + hs.dashboardPermissionsService = &actest.FakePermissionsService{} + }) + + body := `{"items": [{ userId:1, teamId: 2 }]}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/uid/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:uid:1"}, + }))) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("should not be able to specify team and role in same acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.DashboardService = dashboards.NewFakeDashboardService(t) + hs.dashboardPermissionsService = &actest.FakePermissionsService{} + }) + + body := `{"items": [{ teamId:1, role: "Admin" }]}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/uid/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:uid:1"}, + }))) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("should not be able to specify user and role in same acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.DashboardService = dashboards.NewFakeDashboardService(t) + hs.dashboardPermissionsService = &actest.FakePermissionsService{} + }) + + body := `{"items": [{ userId:1, role: "Admin" }]}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/dashboards/uid/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsPermissionsWrite, Scope: "dashboards:uid:1"}, + }))) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 419a53161d2..be0171d16b0 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -1,17 +1,18 @@ package api import ( - "errors" + "context" "net/http" "time" "github.com/grafana/grafana/pkg/api/apierrors" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/services/auth/identity" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/dashboards" "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/web" ) @@ -33,16 +34,7 @@ func (hs *HTTPServer) GetFolderPermissionList(c *contextmodel.ReqContext) respon return apierrors.ToFolderErrorResponse(err) } - g, err := guardian.New(c.Req.Context(), folder.ID, c.OrgID, c.SignedInUser) - if err != nil { - return response.Err(err) - } - - if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { - return apierrors.ToFolderErrorResponse(dashboards.ErrFolderAccessDenied) - } - - acl, err := g.GetACL() + acl, err := hs.getFolderACL(c.Req.Context(), c.SignedInUser, folder) if err != nil { return response.Error(500, "Failed to get folder permissions", err) } @@ -97,20 +89,6 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *contextmodel.ReqContext) respon return apierrors.ToFolderErrorResponse(err) } - g, err := guardian.New(c.Req.Context(), folder.ID, c.OrgID, c.SignedInUser) - if err != nil { - return response.Err(err) - } - - canAdmin, err := g.CanAdmin() - if err != nil { - return apierrors.ToFolderErrorResponse(err) - } - - if !canAdmin { - return apierrors.ToFolderErrorResponse(dashboards.ErrFolderAccessDenied) - } - items := make([]*dashboards.DashboardACL, 0, len(apiCmd.Items)) for _, item := range apiCmd.Items { items = append(items, &dashboards.DashboardACL{ @@ -125,35 +103,72 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *contextmodel.ReqContext) respon }) } - hiddenACL, err := g.GetHiddenACL(hs.Cfg) - if err != nil { - return response.Error(http.StatusInternalServerError, "Error while retrieving hidden permissions", err) - } - items = append(items, hiddenACL...) - - if okToUpdate, err := g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, items); err != nil || !okToUpdate { - if err != nil { - if errors.Is(err, guardian.ErrGuardianPermissionExists) || - errors.Is(err, guardian.ErrGuardianOverride) { - return response.Error(http.StatusBadRequest, err.Error(), err) - } - - return response.Error(http.StatusInternalServerError, "Error while checking folder permissions", err) - } - - return response.Error(http.StatusForbidden, "Cannot remove own admin permission for a folder", nil) - } - - old, err := g.GetACL() + acl, err := hs.getFolderACL(c.Req.Context(), c.SignedInUser, folder) if err != nil { return response.Error(http.StatusInternalServerError, "Error while checking folder permissions", err) } - if err := hs.updateDashboardAccessControl(c.Req.Context(), c.OrgID, folder.UID, true, items, old); err != nil { + + items = append(items, hs.filterHiddenACL(c.SignedInUser, acl)...) + + if err := hs.updateDashboardAccessControl(c.Req.Context(), c.OrgID, folder.UID, true, items, acl); err != nil { return response.Error(http.StatusInternalServerError, "Failed to create permission", err) } + return response.Success("Folder permissions updated") } +var folderPermissionMap = map[string]dashboards.PermissionType{ + "View": dashboards.PERMISSION_VIEW, + "Edit": dashboards.PERMISSION_EDIT, + "Admin": dashboards.PERMISSION_ADMIN, +} + +func (hs *HTTPServer) getFolderACL(ctx context.Context, user identity.Requester, folder *folder.Folder) ([]*dashboards.DashboardACLInfoDTO, error) { + permissions, err := hs.folderPermissionsService.GetPermissions(ctx, user, folder.UID) + if err != nil { + return nil, err + } + + acl := make([]*dashboards.DashboardACLInfoDTO, 0, len(permissions)) + for _, p := range permissions { + if !p.IsManaged { + continue + } + + var role *org.RoleType + if p.BuiltInRole != "" { + tmp := org.RoleType(p.BuiltInRole) + role = &tmp + } + + permission := folderPermissionMap[hs.folderPermissionsService.MapActions(p)] + + acl = append(acl, &dashboards.DashboardACLInfoDTO{ + OrgID: folder.OrgID, + DashboardID: folder.ID, + FolderUID: folder.ParentUID, + Created: p.Created, + Updated: p.Updated, + UserID: p.UserId, + UserLogin: p.UserLogin, + UserEmail: p.UserEmail, + TeamID: p.TeamId, + TeamEmail: p.TeamEmail, + Team: p.Team, + Role: role, + Permission: permission, + PermissionName: permission.String(), + UID: folder.UID, + Title: folder.Title, + URL: folder.WithURL().URL, + IsFolder: true, + Inherited: false, + }) + } + + return acl, nil +} + // swagger:parameters getFolderPermissionList type GetFolderPermissionListParams struct { // in:path diff --git a/pkg/api/folder_permission_test.go b/pkg/api/folder_permission_test.go index 374ab32e593..8eb8368fb44 100644 --- a/pkg/api/folder_permission_test.go +++ b/pkg/api/folder_permission_test.go @@ -2,373 +2,151 @@ package api import ( "encoding/json" - "fmt" + "net/http" + "strings" "testing" "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/api/routing" - "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/services/accesscontrol" - accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/dashboards" - service "github.com/grafana/grafana/pkg/services/dashboards/service" - "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/setting" + "github.com/grafana/grafana/pkg/web/webtest" ) -func TestFolderPermissionAPIEndpoint(t *testing.T) { - settings := setting.NewCfg() +func TestHTTPServer_GetFolderPermissionList(t *testing.T) { + t.Run("should not be able to list acl when user does not have permission to do so", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) {}) - folderService := &foldertest.FakeService{} - - dashboardStore := &dashboards.FakeDashboardStore{} - defer dashboardStore.AssertExpectations(t) - - features := featuremgmt.WithFeatures() - ac := accesscontrolmock.New() - folderPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardPermissions := accesscontrolmock.NewMockedPermissionsService() - dashboardService, err := service.ProvideDashboardServiceImpl( - settings, dashboardStore, foldertest.NewFakeFolderStore(t), nil, features, folderPermissions, dashboardPermissions, ac, - folderService, - ) - require.NoError(t, err) - - hs := &HTTPServer{ - Cfg: settings, - Features: features, - folderService: folderService, - folderPermissionsService: folderPermissions, - dashboardPermissionsService: dashboardPermissions, - DashboardService: dashboardService, - AccessControl: ac, - } - - t.Run("Given folder not exists", func(t *testing.T) { - t.Cleanup(func() { - folderService.ExpectedError = nil - }) - folderService.ExpectedError = dashboards.ErrFolderNotFound - mockSQLStore := dbtest.NewFakeDB() - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", org.RoleEditor, func(sc *scenarioContext) { - callGetFolderPermissions(sc, hs) - assert.Equal(t, 404, sc.resp.Code) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(t, sc) - assert.Equal(t, 404, sc.resp.Code) - }, - }, hs) + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/folders/1/permissions"), userWithPermissions(1, nil))) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) }) - t.Run("Given user has no admin permissions", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - folderService.ExpectedError = nil + t.Run("should be able to list acl with correct permission", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.folderService = &foldertest.FakeService{ExpectedFolder: &folder.Folder{ID: 1, UID: "1"}} + hs.folderPermissionsService = &actest.FakePermissionsService{ + ExpectedPermissions: []accesscontrol.ResourcePermission{}, + } }) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanAdminValue: false}) - folderService.ExpectedError = dashboards.ErrFolderAccessDenied - mockSQLStore := dbtest.NewFakeDB() + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/folders/1/permissions"), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersPermissionsRead, Scope: "folders:uid:1"}, + }))) - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", org.RoleEditor, func(sc *scenarioContext) { - callGetFolderPermissions(sc, hs) - assert.Equal(t, 403, sc.resp.Code) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(t, sc) - assert.Equal(t, 403, sc.resp.Code) - }, - }, hs) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) }) - t.Run("Given user has admin permissions and permissions to update", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) + t.Run("should filter out hidden users from acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + cfg := setting.NewCfg() + cfg.HiddenUsers = map[string]struct{}{"hidden": {}} + hs.Cfg = cfg + hs.folderService = &foldertest.FakeService{ExpectedFolder: &folder.Folder{ID: 1, UID: "1"}} - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: true, - GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, Permission: dashboards.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, Permission: dashboards.PERMISSION_ADMIN}, - {OrgID: 1, DashboardID: 1, TeamID: 1, Permission: dashboards.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, TeamID: 2, Permission: dashboards.PERMISSION_ADMIN}, - }, - }) - - folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"} - mockSQLStore := dbtest.NewFakeDB() - - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", org.RoleAdmin, func(sc *scenarioContext) { - callGetFolderPermissions(sc, hs) - assert.Equal(t, 200, sc.resp.Code) - - var resp []*dashboards.DashboardACLInfoDTO - err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) - require.NoError(t, err) - - assert.Len(t, resp, 5) - assert.Equal(t, int64(2), resp[0].UserID) - assert.Equal(t, dashboards.PERMISSION_VIEW, resp[0].Permission) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil).Once() - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(t, sc) - assert.Equal(t, 200, sc.resp.Code) - - var resp struct { - Message string - } - err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) - require.NoError(t, err) - - assert.Equal(t, "Folder permissions updated", resp.Message) - }, - }, hs) - }) - - t.Run("When trying to update permissions with duplicate permissions", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian - }) - - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianPermissionExists, - }) - - folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"} - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - }, - }, hs) - }) - - t.Run("When trying to update team or user permissions with a role", func(t *testing.T) { - role := org.RoleAdmin - cmds := []dtos.UpdateDashboardACLCommand{ - { - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, + hs.folderPermissionsService = &actest.FakePermissionsService{ + ExpectedPermissions: []accesscontrol.ResourcePermission{ + {UserId: 1, UserLogin: "regular", IsManaged: true}, + {UserId: 2, UserLogin: "hidden", IsManaged: true}, }, - }, - { - Items: []dtos.DashboardACLUpdateItem{ - {TeamID: 1000, Permission: dashboards.PERMISSION_ADMIN, Role: &role}, - }, - }, - } - - for _, cmd := range cmds { - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - respJSON, err := jsonMap(sc.resp.Body.Bytes()) - require.NoError(t, err) - assert.Equal(t, dashboards.ErrPermissionsWithRoleNotAllowed.Error(), respJSON["error"]) - }, - }, hs) - } - }) - - t.Run("When trying to override inherited permissions with lower precedence", func(t *testing.T) { - origNewGuardian := guardian.New - t.Cleanup(func() { - guardian.New = origNewGuardian + } }) - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: false, - CheckPermissionBeforeUpdateError: guardian.ErrGuardianOverride}, - ) + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewGetRequest("/api/folders/1/permissions"), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersPermissionsRead, Scope: "folders:uid:1"}, + }))) - folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"} + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } + var result []dashboards.DashboardACLInfoDTO + require.NoError(t, json.NewDecoder(res.Body).Decode(&result)) - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - callUpdateFolderPermissions(t, sc) - assert.Equal(t, 400, sc.resp.Code) - }, - }, hs) - }) - - t.Run("Getting and updating folder permissions with hidden users", func(t *testing.T) { - origNewGuardian := guardian.New - settings.HiddenUsers = map[string]struct{}{ - "hiddenUser": {}, - testUserLogin: {}, - } - t.Cleanup(func() { - guardian.New = origNewGuardian - settings.HiddenUsers = make(map[string]struct{}) - }) - - guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{ - CanAdminValue: true, - CheckPermissionBeforeUpdateValue: true, - GetACLValue: []*dashboards.DashboardACLInfoDTO{ - {OrgID: 1, DashboardID: 1, UserID: 2, UserLogin: "hiddenUser", Permission: dashboards.PERMISSION_VIEW}, - {OrgID: 1, DashboardID: 1, UserID: 3, UserLogin: testUserLogin, Permission: dashboards.PERMISSION_EDIT}, - {OrgID: 1, DashboardID: 1, UserID: 4, UserLogin: "user_1", Permission: dashboards.PERMISSION_ADMIN}, - }, - GetHiddenACLValue: []*dashboards.DashboardACL{ - {OrgID: 1, DashboardID: 1, UserID: 2, Permission: dashboards.PERMISSION_VIEW}, - }, - }) - - folderService.ExpectedFolder = &folder.Folder{ID: 1, UID: "uid", Title: "Folder"} - - var resp []*dashboards.DashboardACLInfoDTO - mockSQLStore := dbtest.NewFakeDB() - loggedInUserScenarioWithRole(t, "When calling GET on", "GET", "/api/folders/uid/permissions", "/api/folders/:uid/permissions", org.RoleAdmin, func(sc *scenarioContext) { - callGetFolderPermissions(sc, hs) - assert.Equal(t, 200, sc.resp.Code) - - err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) - require.NoError(t, err) - - assert.Len(t, resp, 2) - assert.Equal(t, int64(3), resp[0].UserID) - assert.Equal(t, dashboards.PERMISSION_EDIT, resp[0].Permission) - assert.Equal(t, int64(4), resp[1].UserID) - assert.Equal(t, dashboards.PERMISSION_ADMIN, resp[1].Permission) - }, mockSQLStore) - - cmd := dtos.UpdateDashboardACLCommand{ - Items: []dtos.DashboardACLUpdateItem{ - {UserID: 1000, Permission: dashboards.PERMISSION_ADMIN}, - }, - } - for _, acl := range resp { - cmd.Items = append(cmd.Items, dtos.DashboardACLUpdateItem{ - UserID: acl.UserID, - Permission: acl.Permission, - }) - } - assert.Len(t, cmd.Items, 3) - - folderPermissions.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil).Once() - updateFolderPermissionScenario(t, updatePermissionContext{ - desc: "When calling POST on", - url: "/api/folders/uid/permissions", - routePattern: "/api/folders/:uid/permissions", - cmd: cmd, - fn: func(sc *scenarioContext) { - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() - assert.Equal(t, 200, sc.resp.Code) - }, - }, hs) + assert.Len(t, result, 1) + assert.Equal(t, result[0].UserLogin, "regular") + require.NoError(t, res.Body.Close()) }) } -func callGetFolderPermissions(sc *scenarioContext, hs *HTTPServer) { - sc.handlerFunc = hs.GetFolderPermissionList - sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() -} +func TestHTTPServer_UpdateFolderPermissions(t *testing.T) { + t.Run("should not be able to update acl when user does not have permission to do so", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) {}) -func callUpdateFolderPermissions(t *testing.T, sc *scenarioContext) { - t.Helper() - sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() -} + res, err := server.Send(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/folders/1/permissions", nil), userWithPermissions(1, nil))) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) -func updateFolderPermissionScenario(t *testing.T, ctx updatePermissionContext, hs *HTTPServer) { - t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { - sc := setupScenarioContext(t, ctx.url) - - sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { - c.Req.Body = mockRequestBody(ctx.cmd) - c.Req.Header.Add("Content-Type", "application/json") - sc.context = c - sc.context.OrgID = testOrgID - sc.context.UserID = testUserID - - return hs.UpdateFolderPermissions(c) + t.Run("should be able to update acl with correct permissions", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.folderService = &foldertest.FakeService{ExpectedFolder: &folder.Folder{ID: 1, UID: "1"}} + hs.folderPermissionsService = &actest.FakePermissionsService{} }) - sc.m.Post(ctx.routePattern, sc.defaultHandler) + body := `{"items": []}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/folders/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersPermissionsWrite, Scope: "folders:uid:1"}, + }))) - ctx.fn(sc) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("should not be able to specify team and user in same acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.folderService = &foldertest.FakeService{ExpectedFolder: &folder.Folder{ID: 1, UID: "1"}} + hs.folderPermissionsService = &actest.FakePermissionsService{} + }) + + body := `{"items": [{ userId:1, teamId: 2 }]}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/folders/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersPermissionsWrite, Scope: "folders:uid:1"}, + }))) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("should not be able to specify team and role in same acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.folderService = &foldertest.FakeService{ExpectedFolder: &folder.Folder{ID: 1, UID: "1"}} + hs.folderPermissionsService = &actest.FakePermissionsService{} + }) + + body := `{"items": [{ teamId:1, role: "Admin" }]}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/folders/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersPermissionsWrite, Scope: "folders:uid:1"}, + }))) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + require.NoError(t, res.Body.Close()) + }) + + t.Run("should not be able to specify user and role in same acl", func(t *testing.T) { + server := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.folderService = &foldertest.FakeService{ExpectedFolder: &folder.Folder{ID: 1, UID: "1"}} + hs.folderPermissionsService = &actest.FakePermissionsService{} + }) + + body := `{"items": [{ userId:1, role: "Admin" }]}` + res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewPostRequest("/api/folders/1/permissions", strings.NewReader(body)), userWithPermissions(1, []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersPermissionsWrite, Scope: "folders:uid:1"}, + }))) + + require.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) + require.NoError(t, res.Body.Close()) }) } diff --git a/pkg/api/utils.go b/pkg/api/utils.go index ffad0766f06..08a1c525393 100644 --- a/pkg/api/utils.go +++ b/pkg/api/utils.go @@ -1,7 +1,6 @@ package api import ( - "encoding/json" "net/mail" "github.com/grafana/grafana/pkg/middleware/cookies" @@ -21,12 +20,6 @@ func (hs *HTTPServer) GetRedirectURL(c *contextmodel.ReqContext) string { return redirectURL } -func jsonMap(data []byte) (map[string]string, error) { - jsonMap := make(map[string]string) - err := json.Unmarshal(data, &jsonMap) - return jsonMap, err -} - func ValidateAndNormalizeEmail(email string) (string, error) { if email == "" { return "", nil diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index 612ea82289c..c87e117016d 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -9,17 +9,10 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/folder" - "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) -var permissionMap = map[string]dashboards.PermissionType{ - "View": dashboards.PERMISSION_VIEW, - "Edit": dashboards.PERMISSION_EDIT, - "Admin": dashboards.PERMISSION_ADMIN, -} - var _ DashboardGuardian = new(accessControlDashboardGuardian) // NewAccessControlDashboardGuardianByDashboard creates a dashboard guardian by the provided dashboardId. @@ -390,189 +383,6 @@ func (a *accessControlFolderGuardian) evaluate(evaluator accesscontrol.Evaluator return ok, err } -func (a *accessControlBaseGuardian) CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { - // always true for access control - return true, nil -} - -// GetACL translate access control permissions to dashboard acl info -func (a *accessControlDashboardGuardian) GetACL() ([]*dashboards.DashboardACLInfoDTO, error) { - if a.dashboard == nil { - return nil, ErrGuardianGetDashboardFailure.Errorf("failed to translate access control permissions to dashboard acl info") - } - - svc := a.dashboardPermissionsService - - permissions, err := svc.GetPermissions(a.ctx, a.user, a.dashboard.UID) - if err != nil { - return nil, err - } - - acl := make([]*dashboards.DashboardACLInfoDTO, 0, len(permissions)) - for _, p := range permissions { - if !p.IsManaged { - continue - } - - var role *org.RoleType - if p.BuiltInRole != "" { - tmp := org.RoleType(p.BuiltInRole) - role = &tmp - } - - acl = append(acl, &dashboards.DashboardACLInfoDTO{ - OrgID: a.dashboard.OrgID, - DashboardID: a.dashboard.ID, - FolderID: a.dashboard.FolderID, - Created: p.Created, - Updated: p.Updated, - UserID: p.UserId, - UserLogin: p.UserLogin, - UserEmail: p.UserEmail, - TeamID: p.TeamId, - TeamEmail: p.TeamEmail, - Team: p.Team, - Role: role, - Permission: permissionMap[svc.MapActions(p)], - PermissionName: permissionMap[svc.MapActions(p)].String(), - UID: a.dashboard.UID, - Title: a.dashboard.Title, - Slug: a.dashboard.Slug, - IsFolder: a.dashboard.IsFolder, - URL: a.dashboard.GetURL(), - Inherited: false, - }) - } - - return acl, nil -} - -// GetACL translate access control permissions to dashboard acl info -func (a *accessControlFolderGuardian) GetACL() ([]*dashboards.DashboardACLInfoDTO, error) { - if a.folder == nil { - return nil, ErrGuardianGetFolderFailure.Errorf("failed to translate access control permissions to dashboard acl info") - } - - svc := a.folderPermissionsService - - permissions, err := svc.GetPermissions(a.ctx, a.user, a.folder.UID) - if err != nil { - return nil, err - } - - acl := make([]*dashboards.DashboardACLInfoDTO, 0, len(permissions)) - for _, p := range permissions { - if !p.IsManaged { - continue - } - - var role *org.RoleType - if p.BuiltInRole != "" { - tmp := org.RoleType(p.BuiltInRole) - role = &tmp - } - - acl = append(acl, &dashboards.DashboardACLInfoDTO{ - OrgID: a.folder.OrgID, - DashboardID: a.folder.ID, - FolderUID: a.folder.ParentUID, - Created: p.Created, - Updated: p.Updated, - UserID: p.UserId, - UserLogin: p.UserLogin, - UserEmail: p.UserEmail, - TeamID: p.TeamId, - TeamEmail: p.TeamEmail, - Team: p.Team, - Role: role, - Permission: permissionMap[svc.MapActions(p)], - PermissionName: permissionMap[svc.MapActions(p)].String(), - UID: a.folder.UID, - Title: a.folder.Title, - //Slug: a.folder.Slug, - IsFolder: true, - URL: a.folder.WithURL().URL, - Inherited: false, - }) - } - - return acl, nil -} - -func (a *accessControlDashboardGuardian) GetACLWithoutDuplicates() ([]*dashboards.DashboardACLInfoDTO, error) { - return a.GetACL() -} - -func (a *accessControlFolderGuardian) GetACLWithoutDuplicates() ([]*dashboards.DashboardACLInfoDTO, error) { - return a.GetACL() -} - -func (a *accessControlDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*dashboards.DashboardACL, error) { - var hiddenACL []*dashboards.DashboardACL - if a.user.IsGrafanaAdmin { - return hiddenACL, nil - } - - existingPermissions, err := a.GetACL() - if err != nil { - return hiddenACL, err - } - - for _, item := range existingPermissions { - if item.Inherited || item.UserLogin == a.user.Login { - continue - } - - if _, hidden := cfg.HiddenUsers[item.UserLogin]; hidden { - hiddenACL = append(hiddenACL, &dashboards.DashboardACL{ - OrgID: item.OrgID, - DashboardID: item.DashboardID, - UserID: item.UserID, - TeamID: item.TeamID, - Role: item.Role, - Permission: item.Permission, - Created: item.Created, - Updated: item.Updated, - }) - } - } - - return hiddenACL, nil -} - -func (a *accessControlFolderGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*dashboards.DashboardACL, error) { - var hiddenACL []*dashboards.DashboardACL - if a.user.IsGrafanaAdmin { - return hiddenACL, nil - } - - existingPermissions, err := a.GetACL() - if err != nil { - return hiddenACL, err - } - - for _, item := range existingPermissions { - if item.Inherited || item.UserLogin == a.user.Login { - continue - } - - if _, hidden := cfg.HiddenUsers[item.UserLogin]; hidden { - hiddenACL = append(hiddenACL, &dashboards.DashboardACL{ - OrgID: item.OrgID, - DashboardID: item.DashboardID, - UserID: item.UserID, - TeamID: item.TeamID, - Role: item.Role, - Permission: item.Permission, - Created: item.Created, - Updated: item.Updated, - }) - } - } - - return hiddenACL, nil -} - func (a *accessControlDashboardGuardian) loadParentFolder(folderID int64) (*dashboards.Dashboard, error) { if folderID == 0 { return &dashboards.Dashboard{UID: accesscontrol.GeneralFolderUID}, nil diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 3b79250b9d7..4f2c6c922e5 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -15,7 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database" - accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -31,6 +30,7 @@ import ( ) const ( + orgID = 1 dashUID = "1" folderID = 42 folderUID = "42" @@ -1015,63 +1015,6 @@ func TestAccessControlDashboardGuardian_CanCreate(t *testing.T) { } } -type accessControlGuardianGetHiddenACLTestCase struct { - desc string - permissions []accesscontrol.ResourcePermission - hiddenUsers map[string]struct{} - isFolder bool -} - -func TestAccessControlDashboardGuardian_GetHiddenACL(t *testing.T) { - tests := []accessControlGuardianGetHiddenACLTestCase{ - { - desc: "should only return permissions containing hidden users", - permissions: []accesscontrol.ResourcePermission{ - {RoleName: "managed:users:1:permissions", UserId: 1, UserLogin: "user1", IsManaged: true}, - {RoleName: "managed:teams:1:permissions", TeamId: 1, Team: "team1", IsManaged: true}, - {RoleName: "managed:users:2:permissions", UserId: 2, UserLogin: "user2", IsManaged: true}, - {RoleName: "managed:users:3:permissions", UserId: 3, UserLogin: "user3", IsManaged: true}, - {RoleName: "managed:users:4:permissions", UserId: 4, UserLogin: "user4", IsManaged: true}, - }, - hiddenUsers: map[string]struct{}{"user2": {}, "user3": {}}, - }, - { - desc: "should only return permissions containing hidden users", - permissions: []accesscontrol.ResourcePermission{ - {RoleName: "managed:users:1:permissions", UserId: 1, UserLogin: "user1", IsManaged: true}, - {RoleName: "managed:teams:1:permissions", TeamId: 1, Team: "team1", IsManaged: true}, - {RoleName: "managed:users:2:permissions", UserId: 2, UserLogin: "user2", IsManaged: true}, - {RoleName: "managed:users:3:permissions", UserId: 3, UserLogin: "user3", IsManaged: true}, - {RoleName: "managed:users:4:permissions", UserId: 4, UserLogin: "user4", IsManaged: true}, - }, - hiddenUsers: map[string]struct{}{"user2": {}, "user3": {}}, - isFolder: true, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - mocked := accesscontrolmock.NewMockedPermissionsService() - mocked.On("MapActions", mock.Anything).Return("View") - mocked.On("GetPermissions", mock.Anything, mock.Anything, mock.Anything).Return(tt.permissions, nil) - guardian := setupAccessControlGuardianTest(t, &dashboards.Dashboard{OrgID: orgID, UID: "1", IsFolder: tt.isFolder}, nil, nil, mocked, mocked) - - cfg := setting.NewCfg() - cfg.HiddenUsers = tt.hiddenUsers - permissions, err := guardian.GetHiddenACL(cfg) - require.NoError(t, err) - var hiddenUserNames []string - for name := range tt.hiddenUsers { - hiddenUserNames = append(hiddenUserNames, name) - } - assert.Len(t, permissions, len(hiddenUserNames)) - for _, p := range permissions { - assert.Contains(t, hiddenUserNames, fmt.Sprintf("user%d", p.UserID)) - } - }) - } -} - func setupAccessControlGuardianTest(t *testing.T, d *dashboards.Dashboard, permissions []accesscontrol.Permission, cfg *setting.Cfg, diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index 66e34b4a7a1..d4770db4cc7 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -32,16 +32,6 @@ type DashboardGuardian interface { CanAdmin() (bool, error) CanDelete() (bool, error) CanCreate(folderID int64, isFolder bool) (bool, error) - CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) - - // GetACL returns ACL. - GetACL() ([]*dashboards.DashboardACLInfoDTO, error) - - // GetACLWithoutDuplicates returns ACL and strips any permission - // that already has an inherited permission with higher or equal - // permission. - GetACLWithoutDuplicates() ([]*dashboards.DashboardACLInfoDTO, error) - GetHiddenACL(*setting.Cfg) ([]*dashboards.DashboardACL, error) } type dashboardGuardianImpl struct { @@ -470,22 +460,6 @@ func (g *FakeDashboardGuardian) HasPermission(permission dashboards.PermissionTy return g.HasPermissionValue, nil } -func (g *FakeDashboardGuardian) CheckPermissionBeforeUpdate(permission dashboards.PermissionType, updatePermissions []*dashboards.DashboardACL) (bool, error) { - return g.CheckPermissionBeforeUpdateValue, g.CheckPermissionBeforeUpdateError -} - -func (g *FakeDashboardGuardian) GetACL() ([]*dashboards.DashboardACLInfoDTO, error) { - return g.GetACLValue, nil -} - -func (g *FakeDashboardGuardian) GetACLWithoutDuplicates() ([]*dashboards.DashboardACLInfoDTO, error) { - return g.GetACL() -} - -func (g *FakeDashboardGuardian) GetHiddenACL(cfg *setting.Cfg) ([]*dashboards.DashboardACL, error) { - return g.GetHiddenACLValue, nil -} - // nolint:unused func MockDashboardGuardian(mock *FakeDashboardGuardian) { New = func(_ context.Context, dashID int64, orgId int64, user *user.SignedInUser) (DashboardGuardian, error) { diff --git a/pkg/services/guardian/guardian_test.go b/pkg/services/guardian/guardian_test.go deleted file mode 100644 index 022eb93905f..00000000000 --- a/pkg/services/guardian/guardian_test.go +++ /dev/null @@ -1,797 +0,0 @@ -package guardian - -import ( - "context" - "errors" - "fmt" - "runtime" - "testing" - - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/team/teamtest" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" -) - -const ( - orgID = int64(1) - defaultDashboardID = int64(-1) - dashboardID = int64(1) - dashboardUID = "uid" - parentFolderID = int64(2) - childDashboardID = int64(3) - userID = int64(1) - otherUserID = int64(2) - teamID = int64(1) - otherTeamID = int64(2) -) - -var ( - adminRole = org.RoleAdmin - editorRole = org.RoleEditor - viewerRole = org.RoleViewer -) - -func TestGuardianAdmin(t *testing.T) { - orgRoleScenario("Given user has admin org role", t, org.RoleAdmin, func(sc *scenarioContext) { - // dashboard has default permissions - sc.defaultPermissionScenario(USER, FULL_ACCESS) - - // dashboard has user with permission - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // dashboard has team with permission - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // dashboard has editor role with permission - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // dashboard has viewer role with permission - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // parent folder has user with permission - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // parent folder has team with permission - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // parent folder has editor role with permission - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, FULL_ACCESS) - - // parent folder has viewer role with permission - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, FULL_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, FULL_ACCESS) - }) -} - -func TestGuardianEditor(t *testing.T) { - orgRoleScenario("Given user has editor org role", t, org.RoleEditor, func(sc *scenarioContext) { - // dashboard has default permissions - sc.defaultPermissionScenario(USER, EDITOR_ACCESS) - - // dashboard has user with permission - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_VIEW, CAN_VIEW) - - // dashboard has team with permission - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, CAN_VIEW) - - // dashboard has editor role with permission - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // dashboard has viewer role with permission - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, NO_ACCESS) - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, NO_ACCESS) - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, NO_ACCESS) - - // parent folder has user with permission - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // parent folder has team with permission - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // parent folder has editor role with permission - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // parent folder has viewer role with permission - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, NO_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, NO_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, NO_ACCESS) - }) -} - -func TestGuardianViewer(t *testing.T) { - orgRoleScenario("Given user has viewer org role", t, org.RoleViewer, func(sc *scenarioContext) { - // dashboard has default permissions - sc.defaultPermissionScenario(USER, VIEWER_ACCESS) - - // dashboard has user with permission - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(USER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // dashboard has team with permission - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // dashboard has editor role with permission - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, NO_ACCESS) - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, NO_ACCESS) - sc.dashboardPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, NO_ACCESS) - - // dashboard has viewer role with permission - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.dashboardPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // parent folder has user with permission - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(USER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // parent folder has team with permission - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(TEAM, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - - // parent folder has editor role with permission - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_ADMIN, NO_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_EDIT, NO_ACCESS) - sc.parentFolderPermissionScenario(EDITOR, dashboards.PERMISSION_VIEW, NO_ACCESS) - - // parent folder has viewer role with permission - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_ADMIN, FULL_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_EDIT, EDITOR_ACCESS) - sc.parentFolderPermissionScenario(VIEWER, dashboards.PERMISSION_VIEW, VIEWER_ACCESS) - }) - - apiKeyScenario("Given api key with viewer role", t, org.RoleViewer, func(sc *scenarioContext) { - // dashboard has default permissions - sc.defaultPermissionScenario(VIEWER, VIEWER_ACCESS) - }) -} - -func (sc *scenarioContext) defaultPermissionScenario(pt permissionType, flag permissionFlags) { - _, callerFile, callerLine, _ := runtime.Caller(1) - sc.callerFile = callerFile - sc.callerLine = callerLine - existingPermissions := []*dashboards.DashboardACLInfoDTO{ - toDto(newEditorRolePermission(defaultDashboardID, dashboards.PERMISSION_EDIT)), - toDto(newViewerRolePermission(defaultDashboardID, dashboards.PERMISSION_VIEW)), - } - - permissionScenario("and existing permissions are the default permissions (everyone with editor role can edit, everyone with viewer role can view)", - dashboardID, sc, existingPermissions, func(sc *scenarioContext) { - sc.expectedFlags = flag - sc.verifyExpectedPermissionsFlags() - sc.verifyDuplicatePermissionsShouldNotBeAllowed() - sc.verifyUpdateDashboardPermissionsShouldBeAllowed(pt) - sc.verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt) - }) -} - -func (sc *scenarioContext) dashboardPermissionScenario(pt permissionType, permission dashboards.PermissionType, flag permissionFlags) { - _, callerFile, callerLine, _ := runtime.Caller(1) - sc.callerFile = callerFile - sc.callerLine = callerLine - var existingPermissions []*dashboards.DashboardACLInfoDTO - - switch pt { - case USER: - existingPermissions = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: dashboardID, UserID: userID, Permission: permission}} - case TEAM: - existingPermissions = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: dashboardID, TeamID: teamID, Permission: permission}} - case EDITOR: - existingPermissions = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: dashboardID, Role: &editorRole, Permission: permission}} - case VIEWER: - existingPermissions = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: dashboardID, Role: &viewerRole, Permission: permission}} - } - - permissionScenario(fmt.Sprintf("and %s has permission to %s dashboard", pt.String(), permission.String()), - dashboardID, sc, existingPermissions, func(sc *scenarioContext) { - sc.expectedFlags = flag - sc.verifyExpectedPermissionsFlags() - sc.verifyDuplicatePermissionsShouldNotBeAllowed() - sc.verifyUpdateDashboardPermissionsShouldBeAllowed(pt) - sc.verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt) - }) -} - -func (sc *scenarioContext) parentFolderPermissionScenario(pt permissionType, permission dashboards.PermissionType, flag permissionFlags) { - _, callerFile, callerLine, _ := runtime.Caller(1) - sc.callerFile = callerFile - sc.callerLine = callerLine - var folderPermissionList []*dashboards.DashboardACLInfoDTO - - switch pt { - case USER: - folderPermissionList = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: parentFolderID, - UserID: userID, Permission: permission, Inherited: true}} - case TEAM: - folderPermissionList = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: parentFolderID, TeamID: teamID, - Permission: permission, Inherited: true}} - case EDITOR: - folderPermissionList = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: parentFolderID, - Role: &editorRole, Permission: permission, Inherited: true}} - case VIEWER: - folderPermissionList = []*dashboards.DashboardACLInfoDTO{{OrgID: orgID, DashboardID: parentFolderID, - Role: &viewerRole, Permission: permission, Inherited: true}} - } - - permissionScenario(fmt.Sprintf("and parent folder has %s with permission to %s", pt.String(), permission.String()), - childDashboardID, sc, folderPermissionList, func(sc *scenarioContext) { - sc.expectedFlags = flag - sc.verifyExpectedPermissionsFlags() - sc.verifyDuplicatePermissionsShouldNotBeAllowed() - sc.verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt, permission) - sc.verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt, permission) - sc.verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt, permission) - sc.verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt, permission) - }) -} - -func (sc *scenarioContext) verifyExpectedPermissionsFlags() { - tc := fmt.Sprintf("should have permissions to %s", sc.expectedFlags.String()) - sc.t.Run(tc, func(t *testing.T) { - canAdmin, err := sc.g.CanAdmin() - require.NoError(t, err) - canEdit, err := sc.g.CanEdit() - require.NoError(t, err) - canSave, err := sc.g.CanSave() - require.NoError(t, err) - canView, err := sc.g.CanView() - require.NoError(t, err) - - var actualFlag permissionFlags - - if canAdmin { - actualFlag |= CAN_ADMIN - } - - if canEdit { - actualFlag |= CAN_EDIT - } - - if canSave { - actualFlag |= CAN_SAVE - } - - if canView { - actualFlag |= CAN_VIEW - } - - if actualFlag.noAccess() { - actualFlag = NO_ACCESS - } - - if actualFlag&sc.expectedFlags != actualFlag { - sc.reportFailure(tc, sc.expectedFlags.String(), actualFlag.String()) - } - - sc.reportSuccess() - }) -} - -func (sc *scenarioContext) verifyDuplicatePermissionsShouldNotBeAllowed() { - if !sc.expectedFlags.canAdmin() { - return - } - - tc := "When updating dashboard permissions with duplicate permission for user should not be allowed" - sc.t.Run(tc, func(t *testing.T) { - p := []*dashboards.DashboardACL{ - newDefaultUserPermission(dashboardID, dashboards.PERMISSION_VIEW), - newDefaultUserPermission(dashboardID, dashboards.PERMISSION_ADMIN), - } - sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) - - if !errors.Is(err, ErrGuardianPermissionExists) { - sc.reportFailure(tc, ErrGuardianPermissionExists, err) - } - sc.reportSuccess() - }) - - tc = "When updating dashboard permissions with duplicate permission for team should not be allowed" - sc.t.Run(tc, func(t *testing.T) { - p := []*dashboards.DashboardACL{ - newDefaultTeamPermission(dashboardID, dashboards.PERMISSION_VIEW), - newDefaultTeamPermission(dashboardID, dashboards.PERMISSION_ADMIN), - } - sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) - if !errors.Is(err, ErrGuardianPermissionExists) { - sc.reportFailure(tc, ErrGuardianPermissionExists, err) - } - sc.reportSuccess() - }) - - tc = "When updating dashboard permissions with duplicate permission for editor role should not be allowed" - sc.t.Run(tc, func(t *testing.T) { - p := []*dashboards.DashboardACL{ - newEditorRolePermission(dashboardID, dashboards.PERMISSION_VIEW), - newEditorRolePermission(dashboardID, dashboards.PERMISSION_ADMIN), - } - sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) - - if !errors.Is(err, ErrGuardianPermissionExists) { - sc.reportFailure(tc, ErrGuardianPermissionExists, err) - } - sc.reportSuccess() - }) - - tc = "When updating dashboard permissions with duplicate permission for viewer role should not be allowed" - sc.t.Run(tc, func(t *testing.T) { - p := []*dashboards.DashboardACL{ - newViewerRolePermission(dashboardID, dashboards.PERMISSION_VIEW), - newViewerRolePermission(dashboardID, dashboards.PERMISSION_ADMIN), - } - sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) - if !errors.Is(err, ErrGuardianPermissionExists) { - sc.reportFailure(tc, ErrGuardianPermissionExists, err) - } - sc.reportSuccess() - }) - - tc = "When updating dashboard permissions with duplicate permission for admin role should not be allowed" - sc.t.Run(tc, func(t *testing.T) { - p := []*dashboards.DashboardACL{ - newAdminRolePermission(dashboardID, dashboards.PERMISSION_ADMIN), - } - sc.updatePermissions = p - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, p) - if !errors.Is(err, ErrGuardianPermissionExists) { - sc.reportFailure(tc, ErrGuardianPermissionExists, err) - } - sc.reportSuccess() - }) -} - -func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldBeAllowed(pt permissionType) { - if !sc.expectedFlags.canAdmin() { - return - } - - for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { - tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should be allowed", p.String()) - sc.t.Run(tc, func(t *testing.T) { - permissionList := []*dashboards.DashboardACL{} - switch pt { - case USER: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(dashboardID, p), - newViewerRolePermission(dashboardID, p), - newCustomUserPermission(dashboardID, otherUserID, p), - newDefaultTeamPermission(dashboardID, p), - } - case TEAM: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(dashboardID, p), - newViewerRolePermission(dashboardID, p), - newDefaultUserPermission(dashboardID, p), - newCustomTeamPermission(dashboardID, otherTeamID, p), - } - case EDITOR, VIEWER: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(dashboardID, p), - newViewerRolePermission(dashboardID, p), - newDefaultUserPermission(dashboardID, p), - newDefaultTeamPermission(dashboardID, p), - } - } - - sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if err != nil { - sc.reportFailure(tc, nil, err) - } - if !ok { - sc.reportFailure(tc, false, true) - } - sc.reportSuccess() - }) - } -} - -func (sc *scenarioContext) verifyUpdateDashboardPermissionsShouldNotBeAllowed(pt permissionType) { - if sc.expectedFlags.canAdmin() { - return - } - - for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { - tc := fmt.Sprintf("When updating dashboard permissions with %s permissions should NOT be allowed", p.String()) - sc.t.Run(tc, func(t *testing.T) { - permissionList := []*dashboards.DashboardACL{ - newEditorRolePermission(dashboardID, p), - newViewerRolePermission(dashboardID, p), - } - switch pt { - case USER: - permissionList = append(permissionList, []*dashboards.DashboardACL{ - newCustomUserPermission(dashboardID, otherUserID, p), - newDefaultTeamPermission(dashboardID, p), - }...) - case TEAM: - permissionList = append(permissionList, []*dashboards.DashboardACL{ - newDefaultUserPermission(dashboardID, p), - newCustomTeamPermission(dashboardID, otherTeamID, p), - }...) - default: - // TODO: Handle other cases? - } - - sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if err != nil { - sc.reportFailure(tc, nil, err) - } - if ok { - sc.reportFailure(tc, true, false) - } - sc.reportSuccess() - }) - } -} - -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { - if !sc.expectedFlags.canAdmin() { - return - } - - for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { - tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should be allowed", p.String()) - sc.t.Run(tc, func(t *testing.T) { - permissionList := []*dashboards.DashboardACL{} - switch pt { - case USER: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - newViewerRolePermission(childDashboardID, p), - newCustomUserPermission(childDashboardID, otherUserID, p), - newDefaultTeamPermission(childDashboardID, p), - } - case TEAM: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - newViewerRolePermission(childDashboardID, p), - newDefaultUserPermission(childDashboardID, p), - newCustomTeamPermission(childDashboardID, otherTeamID, p), - } - case EDITOR: - permissionList = []*dashboards.DashboardACL{ - newViewerRolePermission(childDashboardID, p), - newDefaultUserPermission(childDashboardID, p), - newDefaultTeamPermission(childDashboardID, p), - } - - // permission to update is higher than parent folder permission - if p > parentFolderPermission { - permissionList = append(permissionList, newEditorRolePermission(childDashboardID, p)) - } - case VIEWER: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - newDefaultUserPermission(childDashboardID, p), - newDefaultTeamPermission(childDashboardID, p), - } - - // permission to update is higher than parent folder permission - if p > parentFolderPermission { - permissionList = append(permissionList, newViewerRolePermission(childDashboardID, p)) - } - } - - sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if err != nil { - sc.reportFailure(tc, nil, err) - } - if !ok { - sc.reportFailure(tc, false, true) - } - sc.reportSuccess() - }) - } -} - -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsShouldNotBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { - if sc.expectedFlags.canAdmin() { - return - } - - for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { - tc := fmt.Sprintf("When updating child dashboard permissions with %s permissions should NOT be allowed", p.String()) - sc.t.Run(tc, func(t *testing.T) { - permissionList := []*dashboards.DashboardACL{} - switch pt { - case USER: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - newViewerRolePermission(childDashboardID, p), - newCustomUserPermission(childDashboardID, otherUserID, p), - newDefaultTeamPermission(childDashboardID, p), - } - case TEAM: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - newViewerRolePermission(childDashboardID, p), - newDefaultUserPermission(childDashboardID, p), - newCustomTeamPermission(childDashboardID, otherTeamID, p), - } - case EDITOR: - permissionList = []*dashboards.DashboardACL{ - newViewerRolePermission(childDashboardID, p), - newDefaultUserPermission(childDashboardID, p), - newDefaultTeamPermission(childDashboardID, p), - } - - // permission to update is higher than parent folder permission - if p > parentFolderPermission { - permissionList = append(permissionList, newEditorRolePermission(childDashboardID, p)) - } - case VIEWER: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - newDefaultUserPermission(childDashboardID, p), - newDefaultTeamPermission(childDashboardID, p), - } - - // permission to update is higher than parent folder permission - if p > parentFolderPermission { - permissionList = append(permissionList, newViewerRolePermission(childDashboardID, p)) - } - } - - sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if err != nil { - sc.reportFailure(tc, nil, err) - } - if ok { - sc.reportFailure(tc, true, false) - } - sc.reportSuccess() - }) - } -} - -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { - if !sc.expectedFlags.canAdmin() { - return - } - - for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { - // permission to update is higher than parent folder permission - if p > parentFolderPermission { - continue - } - - tc := fmt.Sprintf("When updating child dashboard permissions overriding parent %s permission with %s permission should NOT be allowed", pt.String(), p.String()) - sc.t.Run(tc, func(t *testing.T) { - permissionList := []*dashboards.DashboardACL{} - switch pt { - case USER: - permissionList = []*dashboards.DashboardACL{ - newDefaultUserPermission(childDashboardID, p), - } - case TEAM: - permissionList = []*dashboards.DashboardACL{ - newDefaultTeamPermission(childDashboardID, p), - } - case EDITOR: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - } - case VIEWER: - permissionList = []*dashboards.DashboardACL{ - newViewerRolePermission(childDashboardID, p), - } - } - - sc.updatePermissions = permissionList - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if !errors.Is(err, ErrGuardianOverride) { - sc.reportFailure(tc, ErrGuardianOverride, err) - } - sc.reportSuccess() - }) - } -} - -func (sc *scenarioContext) verifyUpdateChildDashboardPermissionsWithOverrideShouldNotBeAllowed(pt permissionType, parentFolderPermission dashboards.PermissionType) { - if !sc.expectedFlags.canAdmin() { - return - } - - for _, p := range []dashboards.PermissionType{dashboards.PERMISSION_ADMIN, dashboards.PERMISSION_EDIT, dashboards.PERMISSION_VIEW} { - // permission to update is lower than or equal to parent folder permission - if p <= parentFolderPermission { - continue - } - - tc := fmt.Sprintf( - "When updating child dashboard permissions overriding parent %s permission with %s permission should be allowed", - pt.String(), p.String(), - ) - sc.t.Run(tc, func(t *testing.T) { - permissionList := []*dashboards.DashboardACL{} - switch pt { - case USER: - permissionList = []*dashboards.DashboardACL{ - newDefaultUserPermission(childDashboardID, p), - } - case TEAM: - permissionList = []*dashboards.DashboardACL{ - newDefaultTeamPermission(childDashboardID, p), - } - case EDITOR: - permissionList = []*dashboards.DashboardACL{ - newEditorRolePermission(childDashboardID, p), - } - case VIEWER: - permissionList = []*dashboards.DashboardACL{ - newViewerRolePermission(childDashboardID, p), - } - } - - _, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if err != nil { - sc.reportFailure(tc, nil, err) - } - sc.updatePermissions = permissionList - ok, err := sc.g.CheckPermissionBeforeUpdate(dashboards.PERMISSION_ADMIN, permissionList) - if err != nil { - sc.reportFailure(tc, nil, err) - } - if !ok { - sc.reportFailure(tc, false, true) - } - sc.reportSuccess() - }) - } -} - -func TestGuardianGetHiddenACL(t *testing.T) { - t.Run("Get hidden ACL tests", func(t *testing.T) { - store := dbtest.NewFakeDB() - dashSvc := dashboards.NewFakeDashboardService(t) - qResult := []*dashboards.DashboardACLInfoDTO{ - {Inherited: false, UserID: 1, UserLogin: "user1", Permission: dashboards.PERMISSION_EDIT}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: dashboards.PERMISSION_ADMIN}, - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_VIEW}, - } - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) - var qResultDash *dashboards.Dashboard - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - qResultDash = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - OrgID: q.OrgID, - } - }).Return(qResultDash, nil) - - cfg := setting.NewCfg() - cfg.HiddenUsers = map[string]struct{}{"user2": {}} - - t.Run("Should get hidden acl", func(t *testing.T) { - user := &user.SignedInUser{ - OrgID: orgID, - UserID: 1, - Login: "user1", - } - g, err := newDashboardGuardian(context.Background(), cfg, dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) - require.NoError(t, err) - - hiddenACL, err := g.GetHiddenACL(cfg) - require.NoError(t, err) - - require.Equal(t, len(hiddenACL), 1) - require.Equal(t, hiddenACL[0].UserID, int64(2)) - }) - - t.Run("Grafana admin should not get hidden acl", func(t *testing.T) { - user := &user.SignedInUser{ - OrgID: orgID, - UserID: 1, - Login: "user1", - IsGrafanaAdmin: true, - } - dashSvc := dashboards.NewFakeDashboardService(t) - qResult := &dashboards.Dashboard{} - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - }).Return(qResult, nil) - g, err := newDashboardGuardian(context.Background(), cfg, dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) - require.NoError(t, err) - - hiddenACL, err := g.GetHiddenACL(cfg) - require.NoError(t, err) - - require.Equal(t, len(hiddenACL), 0) - }) - }) -} - -func TestGuardianGetACLWithoutDuplicates(t *testing.T) { - t.Run("Get hidden ACL tests", func(t *testing.T) { - store := dbtest.NewFakeDB() - dashSvc := dashboards.NewFakeDashboardService(t) - qResult := []*dashboards.DashboardACLInfoDTO{ - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_EDIT}, - {Inherited: false, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_VIEW}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: dashboards.PERMISSION_ADMIN}, - {Inherited: true, UserID: 4, UserLogin: "user4", Permission: dashboards.PERMISSION_ADMIN}, - {Inherited: false, UserID: 4, UserLogin: "user4", Permission: dashboards.PERMISSION_ADMIN}, - {Inherited: false, UserID: 5, UserLogin: "user5", Permission: dashboards.PERMISSION_EDIT}, - {Inherited: true, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_VIEW}, - {Inherited: false, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_EDIT}, - } - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) - qResultDash := &dashboards.Dashboard{} - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - qResultDash = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - OrgID: q.OrgID, - } - }).Return(qResultDash, nil) - - t.Run("Should get acl without duplicates", func(t *testing.T) { - user := &user.SignedInUser{ - OrgID: orgID, - UserID: 1, - Login: "user1", - } - g, err := newDashboardGuardian(context.Background(), setting.NewCfg(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) - require.NoError(t, err) - - acl, err := g.GetACLWithoutDuplicates() - require.NoError(t, err) - require.NotNil(t, acl) - require.Len(t, acl, 6) - require.ElementsMatch(t, []*dashboards.DashboardACLInfoDTO{ - {Inherited: true, UserID: 3, UserLogin: "user3", Permission: dashboards.PERMISSION_EDIT}, - {Inherited: true, UserID: 4, UserLogin: "user4", Permission: dashboards.PERMISSION_ADMIN}, - {Inherited: true, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_VIEW}, - {Inherited: false, UserID: 2, UserLogin: "user2", Permission: dashboards.PERMISSION_ADMIN}, - {Inherited: false, UserID: 5, UserLogin: "user5", Permission: dashboards.PERMISSION_EDIT}, - {Inherited: false, UserID: 6, UserLogin: "user6", Permission: dashboards.PERMISSION_EDIT}, - }, acl) - }) - }) -} diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go deleted file mode 100644 index 3f3374fc401..00000000000 --- a/pkg/services/guardian/guardian_util_test.go +++ /dev/null @@ -1,303 +0,0 @@ -package guardian - -import ( - "bytes" - "context" - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/infra/db/dbtest" - "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/team" - "github.com/grafana/grafana/pkg/services/team/teamtest" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" -) - -type scenarioContext struct { - t *testing.T - orgRoleScenario string - permissionScenario string - g DashboardGuardian - givenUser *user.SignedInUser - givenDashboardID int64 - givenPermissions []*dashboards.DashboardACLInfoDTO - givenTeams []*team.TeamDTO - updatePermissions []*dashboards.DashboardACL - expectedFlags permissionFlags - callerFile string - callerLine int -} - -type scenarioFunc func(c *scenarioContext) - -func orgRoleScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFunc) { - t.Run(desc, func(t *testing.T) { - user := &user.SignedInUser{ - UserID: userID, - OrgID: orgID, - OrgRole: role, - } - store := dbtest.NewFakeDB() - - fakeDashboardService := dashboards.NewFakeDashboardService(t) - var qResult *dashboards.Dashboard - fakeDashboardService.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - qResult = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - } - }).Return(qResult, nil) - guard, err := newDashboardGuardian(context.Background(), setting.NewCfg(), dashboardID, orgID, user, store, fakeDashboardService, &teamtest.FakeService{}) - require.NoError(t, err) - - sc := &scenarioContext{ - t: t, - orgRoleScenario: desc, - givenUser: user, - givenDashboardID: dashboardID, - g: guard, - } - fn(sc) - }) -} - -func apiKeyScenario(desc string, t *testing.T, role org.RoleType, fn scenarioFunc) { - t.Run(desc, func(t *testing.T) { - user := &user.SignedInUser{ - UserID: 0, - OrgID: orgID, - OrgRole: role, - ApiKeyID: 10, - } - store := dbtest.NewFakeDB() - dashSvc := dashboards.NewFakeDashboardService(t) - var qResult *dashboards.Dashboard - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - qResult = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - } - }).Return(qResult, nil) - guard, err := newDashboardGuardian(context.Background(), setting.NewCfg(), dashboardID, orgID, user, store, dashSvc, &teamtest.FakeService{}) - require.NoError(t, err) - - sc := &scenarioContext{ - t: t, - orgRoleScenario: desc, - givenUser: user, - givenDashboardID: dashboardID, - g: guard, - } - - fn(sc) - }) -} - -func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, - permissions []*dashboards.DashboardACLInfoDTO, fn scenarioFunc) { - sc.t.Run(desc, func(t *testing.T) { - store := dbtest.NewFakeDB() - teams := []*team.TeamDTO{} - - for _, p := range permissions { - if p.TeamID > 0 { - teams = append(teams, &team.TeamDTO{ID: p.TeamID}) - } - } - teamSvc := &teamtest.FakeService{ExpectedTeamsByUser: teams} - - dashSvc := dashboards.NewFakeDashboardService(t) - qResult := permissions - dashSvc.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(qResult, nil) - qResultDash := &dashboards.Dashboard{} - dashSvc.On("GetDashboard", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardQuery")).Run(func(args mock.Arguments) { - q := args.Get(1).(*dashboards.GetDashboardQuery) - qResultDash = &dashboards.Dashboard{ - ID: q.ID, - UID: q.UID, - OrgID: q.OrgID, - } - }).Return(qResultDash, nil) - - sc.permissionScenario = desc - g, err := newDashboardGuardian(context.Background(), setting.NewCfg(), dashboardID, sc.givenUser.OrgID, sc.givenUser, store, dashSvc, teamSvc) - require.NoError(t, err) - sc.g = g - - sc.givenDashboardID = dashboardID - sc.givenPermissions = permissions - sc.givenTeams = teams - - fn(sc) - }) -} - -type permissionType uint8 - -const ( - USER permissionType = 1 << iota - TEAM - EDITOR - VIEWER -) - -func (p permissionType) String() string { - names := map[uint8]string{ - uint8(USER): "user", - uint8(TEAM): "team", - uint8(EDITOR): "editor role", - uint8(VIEWER): "viewer role", - } - return names[uint8(p)] -} - -type permissionFlags uint8 - -const ( - NO_ACCESS permissionFlags = 1 << iota - CAN_ADMIN - CAN_EDIT - CAN_SAVE - CAN_VIEW - FULL_ACCESS = CAN_ADMIN | CAN_EDIT | CAN_SAVE | CAN_VIEW - EDITOR_ACCESS = CAN_EDIT | CAN_SAVE | CAN_VIEW - VIEWER_ACCESS = CAN_VIEW -) - -func (f permissionFlags) canAdmin() bool { - return f&CAN_ADMIN != 0 -} - -func (f permissionFlags) canEdit() bool { - return f&CAN_EDIT != 0 -} - -func (f permissionFlags) canSave() bool { - return f&CAN_SAVE != 0 -} - -func (f permissionFlags) canView() bool { - return f&CAN_VIEW != 0 -} - -func (f permissionFlags) noAccess() bool { - return f&(CAN_ADMIN|CAN_EDIT|CAN_SAVE|CAN_VIEW) == 0 -} - -func (f permissionFlags) String() string { - r := []string{} - - if f.canAdmin() { - r = append(r, "admin") - } - - if f.canEdit() { - r = append(r, "edit") - } - - if f.canSave() { - r = append(r, "save") - } - - if f.canView() { - r = append(r, "view") - } - - if f.noAccess() { - r = append(r, "") - } - - return strings.Join(r, ", ") -} - -func (sc *scenarioContext) reportSuccess() { - assert.True(sc.t, true) -} - -func (sc *scenarioContext) reportFailure(desc string, expected interface{}, actual interface{}) { - var buf bytes.Buffer - buf.WriteString("\n") - buf.WriteString(sc.orgRoleScenario) - buf.WriteString(" ") - buf.WriteString(sc.permissionScenario) - buf.WriteString("\n ") - buf.WriteString(desc) - buf.WriteString("\n") - buf.WriteString(fmt.Sprintf("Source test: %s:%d\n", sc.callerFile, sc.callerLine)) - buf.WriteString(fmt.Sprintf("Expected: %v\n", expected)) - buf.WriteString(fmt.Sprintf("Actual: %v\n", actual)) - buf.WriteString("Context:") - buf.WriteString(fmt.Sprintf("\n Given user: orgRole=%s, id=%d, orgId=%d", sc.givenUser.OrgRole, sc.givenUser.UserID, sc.givenUser.OrgID)) - buf.WriteString(fmt.Sprintf("\n Given dashboard id: %d", sc.givenDashboardID)) - - for i, p := range sc.givenPermissions { - r := "" - if p.Role != nil { - r = string(*p.Role) - } - buf.WriteString(fmt.Sprintf("\n Given permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardID, p.UserID, p.TeamID, r, p.Permission.String())) - } - - for i, t := range sc.givenTeams { - buf.WriteString(fmt.Sprintf("\n Given team (%d): id=%d", i, t.ID)) - } - - for i, p := range sc.updatePermissions { - r := "" - if p.Role != nil { - r = string(*p.Role) - } - buf.WriteString(fmt.Sprintf("\n Update permission (%d): dashboardId=%d, userId=%d, teamId=%d, role=%v, permission=%s", i, p.DashboardID, p.UserID, p.TeamID, r, p.Permission.String())) - } - - sc.t.Fatalf(buf.String()) -} - -func newCustomUserPermission(dashboardID int64, userID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, UserID: userID, Permission: permission} -} - -func newDefaultUserPermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return newCustomUserPermission(dashboardID, userID, permission) -} - -func newCustomTeamPermission(dashboardID int64, teamID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, TeamID: teamID, Permission: permission} -} - -func newDefaultTeamPermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return newCustomTeamPermission(dashboardID, teamID, permission) -} - -func newAdminRolePermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, Role: &adminRole, Permission: permission} -} - -func newEditorRolePermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, Role: &editorRole, Permission: permission} -} - -func newViewerRolePermission(dashboardID int64, permission dashboards.PermissionType) *dashboards.DashboardACL { - return &dashboards.DashboardACL{OrgID: orgID, DashboardID: dashboardID, Role: &viewerRole, Permission: permission} -} - -func toDto(acl *dashboards.DashboardACL) *dashboards.DashboardACLInfoDTO { - return &dashboards.DashboardACLInfoDTO{ - OrgID: acl.OrgID, - DashboardID: acl.DashboardID, - UserID: acl.UserID, - TeamID: acl.TeamID, - Role: acl.Role, - Permission: acl.Permission, - PermissionName: acl.Permission.String(), - } -}