diff --git a/pkg/api/accesscontrol.go b/pkg/api/accesscontrol.go index 0ad8d8eeedc..f991eec04b4 100644 --- a/pkg/api/accesscontrol.go +++ b/pkg/api/accesscontrol.go @@ -71,6 +71,10 @@ func (hs *HTTPServer) declareFixedRoles() error { Grants: []string{string(org.RoleEditor)}, } + if hs.Cfg.ViewersCanEdit { + datasourcesExplorerRole.Grants = append(datasourcesExplorerRole.Grants, string(org.RoleViewer)) + } + datasourcesReaderRole := ac.RoleRegistration{ Role: ac.RoleDTO{ Name: "fixed:datasources:reader", diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index 88d29ec72b1..4282ac4e29e 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -221,6 +221,10 @@ func (a *accessControlDashboardGuardian) CanEdit() (bool, error) { return false, ErrGuardianDashboardNotFound.Errorf("failed to check edit permissions for dashboard") } + if a.cfg.ViewersCanEdit { + return a.CanView() + } + return a.evaluate( accesscontrol.EvalPermission(dashboards.ActionDashboardsWrite, dashboards.ScopeDashboardsProvider.GetResourceScopeUID(a.dashboard.UID)), ) @@ -231,6 +235,10 @@ func (a *accessControlFolderGuardian) CanEdit() (bool, error) { return false, ErrGuardianFolderNotFound.Errorf("failed to check edit permissions for folder") } + if a.cfg.ViewersCanEdit { + return a.CanView() + } + return a.evaluate(accesscontrol.EvalPermission(dashboards.ActionFoldersWrite, dashboards.ScopeFoldersProvider.GetResourceScopeUID(a.folder.UID))) } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 2171d2bbbac..5de4e91da24 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -36,10 +36,11 @@ var ( ) type accessControlGuardianTestCase struct { - desc string - dashboard *dashboards.Dashboard - permissions []accesscontrol.Permission - expected bool + desc string + dashboard *dashboards.Dashboard + permissions []accesscontrol.Permission + viewersCanEdit bool + expected bool } func TestAccessControlDashboardGuardian_CanSave(t *testing.T) { @@ -256,6 +257,18 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { }, expected: false, }, + { + desc: "should be able to edit dashboard with read action when viewer_can_edit is true", + dashboard: dashboard, + permissions: []accesscontrol.Permission{ + { + Action: dashboards.ActionDashboardsRead, + Scope: "dashboards:uid:1", + }, + }, + viewersCanEdit: true, + expected: true, + }, { desc: "should not be able to edit folder with folder write and dashboard wildcard scope", dashboard: fldr, @@ -311,11 +324,24 @@ func TestAccessControlDashboardGuardian_CanEdit(t *testing.T) { }, expected: false, }, + { + desc: "should be able to edit folder with folder read action when viewer_can_edit is true", + dashboard: fldr, + permissions: []accesscontrol.Permission{ + { + Action: dashboards.ActionFoldersRead, + Scope: folderUIDScope, + }, + }, + viewersCanEdit: true, + expected: true, + }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { cfg := setting.NewCfg() + cfg.ViewersCanEdit = tt.viewersCanEdit guardian := setupAccessControlGuardianTest(t, tt.dashboard, tt.permissions, cfg) can, err := guardian.CanEdit() diff --git a/pkg/services/queryhistory/api.go b/pkg/services/queryhistory/api.go index 0b39e4c9f30..ef8ca98ad4d 100644 --- a/pkg/services/queryhistory/api.go +++ b/pkg/services/queryhistory/api.go @@ -30,7 +30,7 @@ type CallbackHandler func(c *contextmodel.ReqContext) response.Response func (s *QueryHistoryService) permissionsMiddleware(handler CallbackHandler, errorMessage string) CallbackHandler { return func(c *contextmodel.ReqContext) response.Response { hasAccess := ac.HasAccess(s.accessControl, c) - if c.GetOrgRole() == org.RoleViewer && !hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { + if c.GetOrgRole() == org.RoleViewer && !s.Cfg.ViewersCanEdit && !hasAccess(ac.EvalPermission(ac.ActionDatasourcesExplore)) { return response.Error(http.StatusUnauthorized, errorMessage, nil) } return handler(c) diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index b3c4c75afe8..d5564aa2814 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -20,7 +20,6 @@ import { } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; -import grafanaConfig from 'app/core/config'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { contextSrv } from 'app/core/core'; import { Trans, t } from 'app/core/internationalization'; @@ -79,11 +78,6 @@ export function ToolbarActions({ dashboard }: Props) { const showScopesSelector = config.featureToggles.scopeFilters && !isEditing; const dashboardNewLayouts = config.featureToggles.dashboardNewLayouts; - // Internal only; - // allows viewer editing without ability to save - // used for grafana play - const canEdit = grafanaConfig.viewersCanEdit; - if (!isEditingPanel) { // This adds the presence indicators in enterprise addDynamicActions(toolbarActions, dynamicDashNavActions.left, 'left-actions'); @@ -360,7 +354,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'main-buttons', - condition: !isEditing && (dashboard.canEditDashboard() || canEdit) && !isViewingPanel && !isPlaying && editable, + condition: !isEditing && dashboard.canEditDashboard() && !isViewingPanel && !isPlaying && editable, render: () => (