package permissions import ( "bytes" "context" "fmt" "slices" "strings" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" ) // maximum possible capacity for recursive queries array: one query for folder and one for dashboard actions const maximumRecursiveQueries = 2 type clause struct { string params []any } type accessControlDashboardPermissionFilter struct { user identity.Requester dashboardAction string dashboardActionSets []string folderAction string folderActionSets []string features featuremgmt.FeatureToggles where clause // any recursive CTE queries (if supported) recQueries []clause recursiveQueriesAreSupported bool } type PermissionsFilter interface { LeftJoin() string With() (string, []any) Where() (string, []any) buildClauses() nestedFoldersSelectors(permSelector string, permSelectorArgs []any, leftTable string, Col string, rightTableCol string, orgID int64) (string, []any) } // NewAccessControlDashboardPermissionFilter creates a new AccessControlDashboardPermissionFilter that is configured with specific actions calculated based on the dashboardaccess.PermissionType and query type // The filter is configured to use the new permissions filter (without subqueries) if the feature flag is enabled // The filter is configured to use the old permissions filter (with subqueries) if the feature flag is disabled func NewAccessControlDashboardPermissionFilter(user identity.Requester, permissionLevel dashboardaccess.PermissionType, queryType string, features featuremgmt.FeatureToggles, recursiveQueriesAreSupported bool) PermissionsFilter { needEdit := permissionLevel > dashboardaccess.PERMISSION_VIEW var folderAction string var dashboardAction string var folderActionSets []string var dashboardActionSets []string if queryType == searchstore.TypeFolder { 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"} } } } var f PermissionsFilter if features.IsEnabledGlobally(featuremgmt.FlagPermissionsFilterRemoveSubquery) { f = &accessControlDashboardPermissionFilterNoFolderSubquery{ accessControlDashboardPermissionFilter: accessControlDashboardPermissionFilter{ user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets, features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported, }, } } else { f = &accessControlDashboardPermissionFilter{user: user, folderAction: folderAction, folderActionSets: folderActionSets, dashboardAction: dashboardAction, dashboardActionSets: dashboardActionSets, features: features, recursiveQueriesAreSupported: recursiveQueriesAreSupported, } } f.buildClauses() return f } func (f *accessControlDashboardPermissionFilter) LeftJoin() string { return "" } // Where returns: // - a where clause for filtering dashboards with expected permissions // - an array with the query parameters func (f *accessControlDashboardPermissionFilter) Where() (string, []any) { return f.where.string, f.where.params } // Check if user has no permissions required for search to skip expensive query func (f *accessControlDashboardPermissionFilter) hasRequiredActions() bool { permissions := f.user.GetPermissions() requiredActions := []string{f.folderAction, f.dashboardAction} for _, action := range requiredActions { if _, ok := permissions[action]; ok { return true } } return false } func (f *accessControlDashboardPermissionFilter) buildClauses() { if f.user == nil || f.user.IsNil() || !f.hasRequiredActions() { f.where = clause{string: "(1 = 0)"} return } dashWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeDashboardsPrefix) folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) var userID int64 if id, err := identity.UserIdentifier(f.user.GetID()); err == nil { userID = id } orgID := f.user.GetOrgID() filter, params := accesscontrol.UserRolesFilter(orgID, userID, f.user.GetTeams(), accesscontrol.GetOrgRoles(f.user)) rolesFilter := " AND role_id IN(SELECT id FROM role " + filter + ") " var args []any builder := strings.Builder{} builder.WriteRune('(') permSelector := strings.Builder{} var permSelectorArgs []any // useSelfContainedPermissions is true if the user's permissions are stored and set from the JWT token // 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 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(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...) 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 { args = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeDashboardsPrefix) // Only add the IN clause if we have any dashboards to check if len(args) > 0 { builder.WriteString("(dashboard.uid IN (?" + strings.Repeat(", ?", len(args)-1) + "") builder.WriteString(") AND NOT dashboard.is_folder)") } else { builder.WriteString("(1 = 0)") } } builder.WriteString(" OR ") if !useSelfContainedPermissions { permSelector.WriteString("(SELECT identifier FROM permission WHERE kind = 'folders' AND attribute = 'uid'") permSelector.WriteString(rolesFilter) permSelectorArgs = append(permSelectorArgs, params...) 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 { permSelectorArgs = getAllowedUIDs(f.dashboardAction, f.user, dashboards.ScopeFoldersPrefix) // Only add the IN clause if we have any folders to check if len(permSelectorArgs) > 0 { permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "") } else { permSelector.WriteString("(") } } permSelector.WriteRune(')') switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) { case true: if len(permSelectorArgs) > 0 { switch f.recursiveQueriesAreSupported { case true: builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ") recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries)) f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID) builder.WriteString(fmt.Sprintf("WHERE d.org_id = ? AND d.uid IN (SELECT uid FROM %s)", recQueryName)) args = append(args, orgID) default: nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "folder_id", "d.id", orgID) builder.WriteRune('(') builder.WriteString(nestedFoldersSelectors) args = append(args, nestedFoldersArgs...) } } else { builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ") builder.WriteString("WHERE 1 = 0") } default: builder.WriteString("(dashboard.folder_id IN (SELECT d.id FROM dashboard as d ") if len(permSelectorArgs) > 0 { builder.WriteString("WHERE d.org_id = ? AND d.uid IN ") args = append(args, orgID) builder.WriteString(permSelector.String()) args = append(args, permSelectorArgs...) } else { builder.WriteString("WHERE 1 = 0") } } 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(f.dashboardAction, f.user) { builder.WriteString(" OR (dashboard.folder_id = 0 AND NOT dashboard.is_folder)") } } else { builder.WriteString("NOT dashboard.is_folder") } } // recycle and reuse permSelector.Reset() permSelectorArgs = permSelectorArgs[:0] if f.folderAction != "" { if f.dashboardAction != "" { builder.WriteString(" OR ") } 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...) 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 { permSelectorArgs = getAllowedUIDs(f.folderAction, f.user, dashboards.ScopeFoldersPrefix) if len(permSelectorArgs) > 0 { permSelector.WriteString("(?" + strings.Repeat(", ?", len(permSelectorArgs)-1) + "") } else { permSelector.WriteString("(") } } permSelector.WriteRune(')') switch f.features.IsEnabledGlobally(featuremgmt.FlagNestedFolders) { case true: if len(permSelectorArgs) > 0 { switch f.recursiveQueriesAreSupported { case true: recQueryName := fmt.Sprintf("RecQry%d", len(f.recQueries)) f.addRecQry(recQueryName, permSelector.String(), permSelectorArgs, orgID) builder.WriteString("(dashboard.uid IN ") builder.WriteString(fmt.Sprintf("(SELECT uid FROM %s)", recQueryName)) default: nestedFoldersSelectors, nestedFoldersArgs := f.nestedFoldersSelectors(permSelector.String(), permSelectorArgs, "dashboard", "uid", "d.uid", orgID) builder.WriteRune('(') builder.WriteString(nestedFoldersSelectors) builder.WriteRune(')') args = append(args, nestedFoldersArgs...) } } else { builder.WriteString("(1 = 0") } default: if len(permSelectorArgs) > 0 { builder.WriteString("(dashboard.uid IN ") builder.WriteString(permSelector.String()) args = append(args, permSelectorArgs...) } else { builder.WriteString("(1 = 0") } } builder.WriteString(" AND dashboard.is_folder)") } else { builder.WriteString("dashboard.is_folder") } } builder.WriteRune(')') f.where = clause{string: builder.String(), params: args} } // With returns: // - a with clause for fetching folders with inherited permissions if nested folders are enabled or an empty string func (f *accessControlDashboardPermissionFilter) With() (string, []any) { var sb bytes.Buffer var params []any if len(f.recQueries) > 0 { sb.WriteString("WITH RECURSIVE ") sb.WriteString(f.recQueries[0].string) params = append(params, f.recQueries[0].params...) for _, r := range f.recQueries[1:] { sb.WriteRune(',') sb.WriteString(r.string) params = append(params, r.params...) } } return sb.String(), params } func (f *accessControlDashboardPermissionFilter) addRecQry(queryName string, whereUIDSelect string, whereParams []any, orgID int64) { if f.recQueries == nil { f.recQueries = make([]clause, 0, maximumRecursiveQueries) } c := make([]any, len(whereParams)) copy(c, whereParams) c = append([]any{orgID}, c...) f.recQueries = append(f.recQueries, clause{ // covered by UQE_folder_org_id_uid and UQE_folder_org_id_parent_uid_title string: fmt.Sprintf(`%s AS ( SELECT uid, parent_uid, org_id FROM folder WHERE org_id = ? AND uid IN %s UNION ALL SELECT f.uid, f.parent_uid, f.org_id FROM folder f INNER JOIN %s r ON f.parent_uid = r.uid and f.org_id = r.org_id )`, queryName, whereUIDSelect, queryName), params: c, }) } 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) { return []any{} } } } 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) { wheres := make([]string, 0, folder.MaxNestedFolderDepth+1) args := make([]any, 0, len(permSelectorArgs)*(folder.MaxNestedFolderDepth+1)) joins := make([]string, 0, folder.MaxNestedFolderDepth+2) // covered by UQE_folder_org_id_uid tmpl := "INNER JOIN folder %s ON %s.%s = %s.uid AND %s.org_id = %s.org_id " prev := "d" onCol := "uid" for i := 1; i <= folder.MaxNestedFolderDepth+2; i++ { t := fmt.Sprintf("f%d", i) s := fmt.Sprintf(tmpl, t, prev, onCol, t, prev, t) joins = append(joins, s) // covered by UQE_folder_org_id_uid wheres = append(wheres, fmt.Sprintf("(%s.org_id = ? AND %s.%s IN (SELECT %s FROM dashboard d %s WHERE %s.org_id = ? AND %s.uid IN %s)", leftTable, leftTable, leftCol, rightTableCol, strings.Join(joins, " "), t, t, permSelector)) args = append(args, orgID, orgID) args = append(args, permSelectorArgs...) prev = t onCol = "parent_uid" } return strings.Join(wheres, ") OR "), args } func getAllowedUIDs(action string, user identity.Requester, scopePrefix string) []any { uidScopes := user.GetPermissions()[action] args := make([]any, 0, len(uidScopes)) for _, uidScope := range uidScopes { if !strings.HasPrefix(uidScope, scopePrefix) { continue } uid := strings.TrimPrefix(uidScope, scopePrefix) 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(actionToCheck string, user identity.Requester) bool { generalFolderScope := dashboards.ScopeFoldersProvider.GetResourceScopeUID(folder.GeneralFolderUID) return slices.Contains(user.GetPermissions()[actionToCheck], generalFolderScope) }