diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index 35c4b9cd657..feb864e6d91 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -163,7 +163,7 @@ func ProvideDashboardPermissions( } return append([]string{parentScope}, nestedScopes...), nil } - return []string{}, nil + return []string{dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, nil }, Assignments: resourcepermissions.Assignments{ Users: true, diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 9c1fa1d93fc..cf12f5bcd05 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -5,6 +5,8 @@ import ( "fmt" "strings" + "golang.org/x/exp/slices" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/dashboards" @@ -229,6 +231,11 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { } } builder.WriteString(") AND NOT dashboard.is_folder)") + + // Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder) + if hasAccessToRoot(toCheck, f.user) { + builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") + } } else { builder.WriteString("NOT dashboard.is_folder") } @@ -423,3 +430,14 @@ func getAllowedUIDs(actions []string, user identity.Requester, scopePrefix strin } return args } + +// Checks if the user has the required permissions on the root (used to be the General folder) +func hasAccessToRoot(actionsToCheck []any, user identity.Requester) bool { + generalFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID) + for _, action := range actionsToCheck { + if !slices.Contains(user.GetPermissions()[action.(string)], generalFolderScope) { + return false + } + } + return true +} diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go index 7b885755ae1..2507c21fba9 100644 --- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go +++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go @@ -146,6 +146,11 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() } builder.WriteString(" AND NOT dashboard.is_folder)") } + + // Include all the dashboards under the root if the user has the required permissions on the root (used to be the General folder) + if hasAccessToRoot(toCheck, f.user) { + builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") + } } else { builder.WriteString("NOT dashboard.is_folder") } diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 2862cbbdc58..505deb1977f 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -52,7 +52,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, - expectedResult: 100, + expectedResult: 110, }, { desc: "Should be able to view all dashboards with folder wildcard scope", @@ -60,7 +60,32 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, - expectedResult: 100, + expectedResult: 110, + }, + { + desc: "Should be able to view dashboards under the root with folders:uid:general scope", + permission: dashboards.PERMISSION_VIEW, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, + }, + { + desc: "Should not be able to view editable dashboards under the root with folders:uid:general scope if missing write action", + permission: dashboards.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 0, + }, + { + desc: "Should be able to view editable dashboards under the root with folders:uid:general scope if has write action", + permission: dashboards.PERMISSION_EDIT, + permissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, }, { desc: "Should be able to view a subset of dashboards with dashboard scopes", @@ -163,7 +188,7 @@ func TestIntegration_DashboardPermissionFilter(t *testing.T) { } for _, tt := range tests { - store := setupTest(t, 10, 100, tt.permissions) + store := setupTest(t, 10, 110, tt.permissions) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() require.NoError(t, err) @@ -219,7 +244,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t signedInUserPermissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll}, }, - expectedResult: 100, + expectedResult: 110, }, { desc: "Should be able to view all dashboards with folder wildcard scope", @@ -227,7 +252,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t signedInUserPermissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, - expectedResult: 100, + expectedResult: 110, }, { desc: "Should not be able to view any dashboards or folders without any permissions", @@ -258,6 +283,31 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t }, expectedResult: 20, }, + { + desc: "Should be able to view dashboards under the root with folders:uid:general scope", + permission: dashboards.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, + }, + { + desc: "Should not be able to view editable dashboards under the root with folders:uid:general scope if missing write action", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 0, + }, + { + desc: "Should be able to view editable dashboards under the root with folders:uid:general scope if has write action", + permission: dashboards.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + {Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID)}, + }, + expectedResult: 10, + }, { desc: "Should be able to view all folders with folder wildcard", permission: dashboards.PERMISSION_VIEW, @@ -337,7 +387,7 @@ func TestIntegration_DashboardPermissionFilter_WithSelfContainedPermissions(t *t } for _, tt := range tests { - store := setupTest(t, 10, 100, []accesscontrol.Permission{}) + store := setupTest(t, 10, 110, []accesscontrol.Permission{}) recursiveQueriesAreSupported, err := store.RecursiveQueriesAreSupported() require.NoError(t, err) @@ -717,12 +767,12 @@ func setupTest(t *testing.T, numFolders, numDashboards int, permissions []access Updated: time.Now(), }) } - // Seed 100 dashboard + // Seed dashboards for i := numFolders + 1; i <= numFolders+numDashboards; i++ { str := strconv.Itoa(i) - folderID := numFolders - if i%numFolders != 0 { - folderID = i % numFolders + folderID := 0 + if i%(numFolders+1) != 0 { + folderID = i % (numFolders + 1) } dashes = append(dashes, dashboards.Dashboard{ OrgID: 1,