diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index c89eb2a612f..1dc5fe990ff 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -2,6 +2,7 @@ package permissions import ( "bytes" + "context" "fmt" "slices" "strings" @@ -25,10 +26,12 @@ type clause struct { } type accessControlDashboardPermissionFilter struct { - user identity.Requester - dashboardAction string - folderAction string - features featuremgmt.FeatureToggles + user identity.Requester + dashboardAction string + dashboardActionSets []string + folderAction string + folderActionSets []string + features featuremgmt.FeatureToggles where clause // any recursive CTE queries (if supported) @@ -53,30 +56,63 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi var folderAction string var dashboardAction string + var folderActionSets []string + var dashboardActionSets []string if queryType == searchstore.TypeFolder { folderAction = dashboards.ActionFoldersRead - //folderAction = append(folderAction, dashboards.ActionFoldersRead) + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"} + } if needEdit { folderAction = dashboards.ActionDashboardsCreate + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:edit", "folders:admin"} + } } } else if queryType == searchstore.TypeDashboard { dashboardAction = dashboards.ActionDashboardsRead + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"} + dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"} + } if needEdit { dashboardAction = dashboards.ActionDashboardsWrite + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:edit", "folders:admin"} + dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"} + } } } else if queryType == searchstore.TypeAlertFolder { folderAction = accesscontrol.ActionAlertingRuleRead + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"} + } if needEdit { folderAction = accesscontrol.ActionAlertingRuleCreate + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:edit", "folders:admin"} + } } } else if queryType == searchstore.TypeAnnotation { dashboardAction = accesscontrol.ActionAnnotationsRead + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"} + dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"} + } } else { folderAction = dashboards.ActionFoldersRead dashboardAction = dashboards.ActionDashboardsRead + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:view", "folders:edit", "folders:admin"} + dashboardActionSets = []string{"dashboards:view", "dashboards:edit", "dashboards:admin"} + } if needEdit { folderAction = dashboards.ActionDashboardsCreate dashboardAction = dashboards.ActionDashboardsWrite + if features.IsEnabled(context.Background(), featuremgmt.FlagAccessActionSets) { + folderActionSets = []string{"folders:edit", "folders:admin"} + dashboardActionSets = []string{"dashboards:edit", "dashboards:admin"} + } } } @@ -84,13 +120,13 @@ func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissi if features.IsEnabledGlobally(featuremgmt.FlagPermissionsFilterRemoveSubquery) { f = &accessControlDashboardPermissionFilterNoFolderSubquery{ accessControlDashboardPermissionFilter: accessControlDashboardPermissionFilter{ - user: user, folderAction: folderAction, dashboardAction: dashboardAction, features: features, - recursiveQueriesAreSupported: recursiveQueriesAreSupported, + user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets, + features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported, }, } } else { - f = &accessControlDashboardPermissionFilter{user: user, folderAction: folderAction, dashboardAction: dashboardAction, features: features, - recursiveQueriesAreSupported: recursiveQueriesAreSupported, + f = &accessControlDashboardPermissionFilter{user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets, + features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported, } } f.buildClauses() @@ -149,20 +185,24 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { // currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana) useSelfContainedPermissions := f.user.IsAuthenticatedBy(login.ExtendedJWTModule) - if len(f.dashboardAction) > 0 { - toCheck := actionsToCheck(f.dashboardAction, f.user.GetPermissions(), dashWildcards, folderWildcards) + if f.dashboardAction != "" { + toCheckDashboards := actionsToCheck(f.dashboardAction, f.dashboardActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards) + toCheckFolders := actionsToCheck(f.dashboardAction, f.folderActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards) - if len(toCheck) > 0 { + if len(toCheckDashboards) > 0 { if !useSelfContainedPermissions { builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'") builder.WriteString(rolesFilter) args = append(args, params...) - builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)") - args = append(args, toCheck[0]) + if len(toCheckDashboards) == 1 { + builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)") + args = append(args, toCheckDashboards[0]) + } else { + builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")) AND NOT dashboard.is_folder)") + args = append(args, toCheckDashboards...) + } } else { - actions := parseStringSliceFromInterfaceSlice(toCheck) - - args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix) + args = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeDashboardsPrefix) // Only add the IN clause if we have any dashboards to check if len(args) > 0 { @@ -179,12 +219,15 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) - permSelector.WriteString(" AND action = ?") - permSelectorArgs = append(permSelectorArgs, toCheck[0]) + if len(toCheckDashboards) == 1 { + permSelector.WriteString(" AND action = ?") + permSelectorArgs = append(permSelectorArgs, toCheckDashboards[0]) + } else { + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")") + permSelectorArgs = append(permSelectorArgs, toCheckFolders...) + } } else { - actions := parseStringSliceFromInterfaceSlice(toCheck) - - permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix) + permSelectorArgs = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeFoldersPrefix) // Only add the IN clause if we have any folders to check if len(permSelectorArgs) > 0 { @@ -229,7 +272,7 @@ 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) { + if hasAccessToRoot(f.dashboardAction, f.user) { builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") } } else { @@ -241,23 +284,27 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { permSelector.Reset() permSelectorArgs = permSelectorArgs[:0] - if len(f.folderAction) > 0 { - if len(f.dashboardAction) > 0 { + if f.folderAction != "" { + if f.dashboardAction != "" { builder.WriteString(" OR ") } - toCheck := actionsToCheck(f.folderAction, f.user.GetPermissions(), folderWildcards) + toCheck := actionsToCheck(f.folderAction, f.folderActionSets, f.user.GetPermissions(), folderWildcards) + if len(toCheck) > 0 { if !useSelfContainedPermissions { permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) - permSelector.WriteString(" AND action = ?") - permSelectorArgs = append(permSelectorArgs, toCheck[0]) + if len(toCheck) == 1 { + permSelector.WriteString(" AND action = ?") + permSelectorArgs = append(permSelectorArgs, toCheck[0]) + } else { + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ")") + permSelectorArgs = append(permSelectorArgs, toCheck...) + } } else { - actions := parseStringSliceFromInterfaceSlice(toCheck) - - permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix) + permSelectorArgs = getAllowedUIDs(f.folderAction, f.user, dashboards.ScopeFoldersPrefix) if len(permSelectorArgs) > 0 { permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "") @@ -342,7 +389,7 @@ func (f *accessControlDashboardPermissionFilter) addRecQry(queryName string, whe }) } -func actionsToCheck(action string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []any { +func actionsToCheck(action string, actionSets []string, permissions map[string][]string, wildcards ...accesscontrol.Wildcards) []any { for _, scope := range permissions[action] { for _, w := range wildcards { if w.Contains(scope) { @@ -351,7 +398,12 @@ func actionsToCheck(action string, permissions map[string][]string, wildcards .. } } - return []any{action} + toCheck := []any{action} + for _, a := range actionSets { + toCheck = append(toCheck, a) + } + + return toCheck } func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTable string, leftCol string, rightTableCol string, orgID int64) (string, []any) { @@ -382,46 +434,21 @@ func (f *accessControlDashboardPermissionFilter) nestedFoldersSelectors(permSele return strings.Join(wheres, ") OR "), args } -func parseStringSliceFromInterfaceSlice(slice []any) []string { - result := make([]string, 0, len(slice)) - for _, s := range slice { - result = append(result, s.(string)) - } - return result -} - -func getAllowedUIDs(actions []string, user identity.Requester, scopePrefix string) []any { - uidToActions := make(map[string]map[string]struct{}) - for _, action := range actions { - for _, uidScope := range user.GetPermissions()[action] { - if !strings.HasPrefix(uidScope, scopePrefix) { - continue - } - uid := strings.TrimPrefix(uidScope, scopePrefix) - if _, exists := uidToActions[uid]; !exists { - uidToActions[uid] = make(map[string]struct{}) - } - uidToActions[uid][action] = struct{}{} +func getAllowedUIDs(action string, user identity.Requester, scopePrefix string) []any { + var args []any + for _, uidScope := range user.GetPermissions()[action] { + if !strings.HasPrefix(uidScope, scopePrefix) { + continue } + uid := strings.TrimPrefix(uidScope, scopePrefix) + args = append(args, uid) } - // args max capacity is the length of the different uids - args := make([]any, 0, len(uidToActions)) - for uid, assignedActions := range uidToActions { - if len(assignedActions) == len(actions) { - args = append(args, uid) - } - } 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 { +func hasAccessToRoot(actionToCheck string, 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 + return slices.Contains(user.GetPermissions()[actionToCheck], generalFolderScope) } diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go index 524bb5b4760..5cba8a0184e 100644 --- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go +++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go @@ -53,20 +53,24 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() // currently it's used for the extended JWT module (when the user is authenticated via a JWT token generated by Grafana) useSelfContainedPermissions := f.user.GetAuthenticatedBy() == login.ExtendedJWTModule - if len(f.dashboardAction) > 0 { - toCheck := actionsToCheck(f.dashboardAction, f.user.GetPermissions(), dashWildcards, folderWildcards) + if f.dashboardAction != "" { + toCheckDashboards := actionsToCheck(f.dashboardAction, f.dashboardActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards) + toCheckFolders := actionsToCheck(f.dashboardAction, f.folderActionSets, f.user.GetPermissions(), dashWildcards, folderWildcards) - if len(toCheck) > 0 { + if len(toCheckDashboards) > 0 { if !useSelfContainedPermissions { builder.WriteString("(dashboard.uid IN (SELECT identifier FROM permission WHERE kind = 'dashboards' AND attribute = 'uid'") builder.WriteString(rolesFilter) args = append(args, params...) - builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)") - args = append(args, toCheck[0]) + if len(toCheckDashboards) == 1 { + builder.WriteString(" AND action = ?) AND NOT dashboard.is_folder)") + args = append(args, toCheckDashboards[0]) + } else { + builder.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckDashboards)-1) + ")) AND NOT dashboard.is_folder)") + args = append(args, toCheckDashboards...) + } } else { - actions := parseStringSliceFromInterfaceSlice(toCheck) - - args = getAllowedUIDs(actions, f.user, dashboards.ScopeDashboardsPrefix) + args = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeDashboardsPrefix) // Only add the IN clause if we have any dashboards to check if len(args) > 0 { @@ -83,12 +87,15 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) - permSelector.WriteString(" AND action = ?") - permSelectorArgs = append(permSelectorArgs, toCheck[0]) + if len(toCheckFolders) == 1 { + permSelector.WriteString(" AND action = ?") + permSelectorArgs = append(permSelectorArgs, toCheckFolders[0]) + } else { + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheckFolders)-1) + ")") + permSelectorArgs = append(permSelectorArgs, toCheckFolders...) + } } else { - actions := parseStringSliceFromInterfaceSlice(toCheck) - - permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix) + permSelectorArgs = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeFoldersPrefix) // Only add the IN clause if we have any folders to check if len(permSelectorArgs) > 0 { @@ -133,7 +140,7 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() } // 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) { + if hasAccessToRoot(f.dashboardAction, f.user) { builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") } } else { @@ -145,23 +152,26 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() permSelector.Reset() permSelectorArgs = permSelectorArgs[:0] - if len(f.folderAction) > 0 { - if len(f.dashboardAction) > 0 { + if f.folderAction != "" { + if f.dashboardAction != "" { builder.WriteString(" OR ") } - toCheck := actionsToCheck(f.folderAction, f.user.GetPermissions(), folderWildcards) + toCheck := actionsToCheck(f.folderAction, f.folderActionSets, f.user.GetPermissions(), folderWildcards) if len(toCheck) > 0 { if !useSelfContainedPermissions { permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) - permSelector.WriteString(" AND action = ?") - permSelectorArgs = append(permSelectorArgs, toCheck[0]) + if len(toCheck) == 1 { + permSelector.WriteString(" AND action = ?") + permSelectorArgs = append(permSelectorArgs, toCheck[0]) + } else { + permSelector.WriteString(" AND action IN (?" + strings.Repeat(", ?", len(toCheck)-1) + ")") + permSelectorArgs = append(permSelectorArgs, toCheck...) + } } else { - actions := parseStringSliceFromInterfaceSlice(toCheck) - - permSelectorArgs = getAllowedUIDs(actions, f.user, dashboards.ScopeFoldersPrefix) + permSelectorArgs = getAllowedUIDs(f.folderAction, f.user, dashboards.ScopeFoldersPrefix) if len(permSelectorArgs) > 0 { permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "") diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index ec17492d485..640254d5def 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -416,8 +416,8 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { permissions: []accesscontrol.Permission{ {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, - features: []any{featuremgmt.FlagNestedFolders}, - expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"}, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"}, }, { desc: "Should be able to view inherited folders if nested folders are enabled", @@ -458,7 +458,7 @@ func TestIntegration_DashboardNestedPermissionFilter(t *testing.T) { }) usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.permissions)}} - for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} { + for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagAccessActionSets)...), featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} { m := features.GetEnabled(context.Background()) keys := make([]string, 0, len(m)) for k := range m { @@ -530,7 +530,7 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission {Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeFoldersAll}, }, features: []any{featuremgmt.FlagNestedFolders}, - expectedResult: []string{"dashboard under parent folder", "dashboard under subfolder"}, + expectedResult: []string{"dashboard under the root", "dashboard under parent folder", "dashboard under subfolder"}, }, { desc: "Should be able to view inherited folders if nested folders are enabled", @@ -608,6 +608,125 @@ func TestIntegration_DashboardNestedPermissionFilter_WithSelfContainedPermission } } +func TestIntegration_DashboardNestedPermissionFilter_WithActionSets(t *testing.T) { + testCases := []struct { + desc string + queryType string + permission dashboardaccess.PermissionType + signedInUserPermissions []accesscontrol.Permission + expectedResult []string + features []any + }{ + { + desc: "Should not list any dashboards if user has no permissions", + permission: dashboardaccess.PERMISSION_VIEW, + signedInUserPermissions: nil, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: nil, + }, + { + desc: "Should not list any folders if user has no permissions", + permission: dashboardaccess.PERMISSION_VIEW, + signedInUserPermissions: nil, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: nil, + }, + { + desc: "Should be able to view folders if user has `folders:read` access to them", + queryType: searchstore.TypeFolder, + permission: dashboardaccess.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll}, + }, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: []string{"parent", "subfolder"}, + }, + { + desc: "Should be able to view folders if user has action set access to them", + queryType: searchstore.TypeFolder, + permission: dashboardaccess.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: "folders:view", Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"}, + }, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: []string{"parent", "subfolder"}, + }, + { + desc: "Should be able to view only the subfolder if user has action set access to it", + queryType: searchstore.TypeFolder, + permission: dashboardaccess.PERMISSION_VIEW, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: "folders:admin", Scope: "folders:uid:subfolder", Kind: "folders", Identifier: "subfolder"}, + }, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: []string{"subfolder"}, + }, + { + desc: "Should be able to filter for folders that user has write access to", + queryType: searchstore.TypeFolder, + permission: dashboardaccess.PERMISSION_EDIT, + signedInUserPermissions: []accesscontrol.Permission{ + {Action: "folders:edit", Scope: "folders:uid:subfolder", Kind: "folders", Identifier: "subfolder"}, + {Action: "folders:view", Scope: "folders:uid:parent", Kind: "folders", Identifier: "parent"}, + }, + features: []any{featuremgmt.FlagNestedFolders, featuremgmt.FlagAccessActionSets}, + expectedResult: []string{"subfolder"}, + }, + } + + origNewGuardian := guardian.New + guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanViewValue: true, CanSaveValue: true}) + t.Cleanup(func() { + guardian.New = origNewGuardian + }) + + var orgID int64 = 1 + + for _, tc := range testCases { + tc.signedInUserPermissions = append(tc.signedInUserPermissions, accesscontrol.Permission{ + Action: dashboards.ActionFoldersCreate, + }, accesscontrol.Permission{ + Action: dashboards.ActionFoldersWrite, + Scope: dashboards.ScopeFoldersAll, + }, accesscontrol.Permission{ + Action: dashboards.ActionFoldersRead, + Scope: "folders:uid:unrelated"}, accesscontrol.Permission{ + Action: dashboards.ActionDashboardsCreate, + Scope: "folders:uid:unrelated"}) + usr := &user.SignedInUser{OrgID: orgID, OrgRole: org.RoleViewer, Permissions: map[int64]map[string][]string{orgID: accesscontrol.GroupScopesByAction(tc.signedInUserPermissions)}} + + for _, features := range []featuremgmt.FeatureToggles{featuremgmt.WithFeatures(tc.features...), featuremgmt.WithFeatures(append(tc.features, featuremgmt.FlagPermissionsFilterRemoveSubquery)...)} { + m := features.GetEnabled(context.Background()) + keys := make([]string, 0, len(m)) + for k := range m { + keys = append(keys, k) + } + + t.Run(tc.desc+" with features "+strings.Join(keys, ","), func(t *testing.T) { + db := setupNestedTest(t, usr, tc.signedInUserPermissions, orgID, features) + recursiveQueriesAreSupported, err := db.RecursiveQueriesAreSupported() + require.NoError(t, err) + filter := permissions.NewAccessControlDashboardPermissionFilter(usr, tc.permission, tc.queryType, features, recursiveQueriesAreSupported) + var result []string + err = db.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + q, params := filter.Where() + recQry, recQryParams := filter.With() + params = append(recQryParams, params...) + s := recQry + "\nSELECT dashboard.title FROM dashboard WHERE " + q + leftJoin := filter.LeftJoin() + if leftJoin != "" { + s = recQry + "\nSELECT dashboard.title FROM dashboard LEFT OUTER JOIN " + leftJoin + " WHERE " + q + "ORDER BY dashboard.id ASC" + } + err := sess.SQL(s, params...).Find(&result) + return err + }) + require.NoError(t, err) + assert.Equal(t, tc.expectedResult, result) + }) + } + } +} + func setupTest(t *testing.T, numFolders, numDashboards int, permissions []accesscontrol.Permission) db.DB { t.Helper() @@ -724,6 +843,15 @@ func setupNestedTest(t *testing.T, usr *user.SignedInUser, perms []accesscontrol }) require.NoError(t, err) + // create a root level dashboard + _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ + OrgID: orgID, + Dashboard: simplejson.NewFromAny(map[string]any{ + "title": "dashboard under the root", + }), + }) + require.NoError(t, err) + // create dashboard under parent folder _, err = dashStore.SaveDashboard(context.Background(), dashboards.SaveDashboardCommand{ OrgID: orgID,