Authorization bypass / Privilege escalation

HIGH
grafana/grafana
Commit: 91627cef57d6
Affected: 12.4.0 and earlier
2026-07-10 13:16 UTC

Description

The commit migrates folder actions in the alerting unified UI to centralized ability hooks (FolderAction, useFolderAbility, useGlobalRuleAbility) instead of ad-hoc permission checks (e.g., contextSrv.hasPermission). This refactor enforces authorization decisions through a unified capability system, reducing the risk of inconsistent or bypassable frontend checks when performing folder-related actions (create, export, pause, delete). While server-side authorization remains essential, the frontend gating now routes through centralized abilities, addressing potential authorization bypass scenarios caused by scattered or duplicated checks across components.

Commit Details

Author: Gilles De Mey

Date: 2026-07-10 12:59 UTC

Message:

Alerting: Migrate folder actions to use centralized ability hooks (10/N) (#126766)

Triage Assessment

Vulnerability Type: Authorization bypass / Privilege escalation

Confidence: HIGH

Reasoning:

The commit replaces direct permission checks (contextSrv.hasPermission) with centralized ability hooks for folder actions (create, export, pause, delete). This enforces authorization decisions through a unified capability system, reducing risk of improper access and potential privilege escalation. It introduces FolderAction and centralizes checks via useFolderAbility/useGlobalRuleAbility, indicating a security-focused refactor.

Verification Assessment

Vulnerability Type: Authorization bypass / Privilege escalation

Confidence: HIGH

Affected Versions: 12.4.0 and earlier

Code Diff

diff --git a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx index 7bd2c804a4e81..ed0feb6f95f26 100644 --- a/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx +++ b/public/app/features/alerting/unified/components/create-folder/CreateNewFolder.tsx @@ -7,9 +7,9 @@ import { Trans, t } from '@grafana/i18n'; import { Button, type ComponentSize, Field, Input, Label, Modal, Stack, useStyles2 } from '@grafana/ui'; import { useCreateFolder } from 'app/api/clients/folder/v1beta1/hooks'; import { useAppNotification } from 'app/core/copy/appNotification'; -import { contextSrv } from 'app/core/services/context_srv'; -import { AccessControlAction } from 'app/types/accessControl'; +import { useFolderAbility } from '../../hooks/abilities/otherAbilities'; +import { FolderAction } from '../../hooks/abilities/types'; import { type Folder } from '../../types/rule-form'; type ButtonFill = 'solid' | 'outline' | 'text'; @@ -25,6 +25,7 @@ export interface CreateNewFolderProps { */ export const CreateNewFolder = ({ onCreate, fill = 'outline', size = 'md' }: CreateNewFolderProps) => { const [isCreatingFolder, setIsCreatingFolder] = useState(false); + const { granted: canCreate } = useFolderAbility(FolderAction.Create); const handleCreate = (folder: Folder) => { onCreate(folder); setIsCreatingFolder(false); @@ -38,7 +39,7 @@ export const CreateNewFolder = ({ onCreate, fill = 'outline', size = 'md' }: Cre variant="secondary" fill={fill} size={size} - disabled={!contextSrv.hasPermission(AccessControlAction.FoldersCreate)} + disabled={!canCreate} > <Trans i18nKey="alerting.create-new-folder.new-folder">New folder</Trans> </Button> diff --git a/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx index 0b66b367c1ca4..6b6f9dba54edc 100644 --- a/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx +++ b/public/app/features/alerting/unified/components/folder-actions/FolderActionsButton.tsx @@ -7,12 +7,9 @@ import { useDispatch } from 'app/types/store'; import { alertingFolderActionsApi } from '../../api/alertingFolderActionsApi'; import { shouldUseAlertingListViewV2, shouldUsePrometheusRulesPrimary } from '../../featureToggles'; -import { - AlertingAction, - FolderBulkAction, - useAlertingAbility, - useFolderBulkActionAbility, -} from '../../hooks/useAbilities'; +import { useFolderBulkActionAbility } from '../../hooks/abilities/otherAbilities'; +import { useGlobalRuleAbility } from '../../hooks/abilities/rules/ruleAbilities'; +import { FolderBulkAction, RuleAction } from '../../hooks/abilities/types'; import { useFolder } from '../../hooks/useFolder'; import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../../state/actions'; import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource'; @@ -35,9 +32,7 @@ export const FolderActionsButton = ({ folderUID }: Props) => { const bulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI; const listView2Enabled = shouldUseAlertingListViewV2(); - const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules); - - const canExportRules = exportRulesSupported && exportRulesAllowed; + const { granted: canExportRules } = useGlobalRuleAbility(RuleAction.ExportRules); const [deleteGrafanaRulesFromFolder, deleteState] = alertingFolderActionsApi.endpoints.deleteGrafanaRulesFromFolder.useMutation(); @@ -134,11 +129,8 @@ function BulkActions({ const bulkActionsEnabled = config.featureToggles.alertingBulkActionsInUI; // abilities - const [pauseSupported, pauseAllowed] = useFolderBulkActionAbility(FolderBulkAction.Pause); - const [deleteSupported, deleteAllowed] = useFolderBulkActionAbility(FolderBulkAction.Delete); - - const canPause = pauseSupported && pauseAllowed; - const canDelete = deleteSupported && deleteAllowed; + const { granted: canPause } = useFolderBulkActionAbility(FolderBulkAction.Pause); + const { granted: canDelete } = useFolderBulkActionAbility(FolderBulkAction.Delete); // mutations const [pauseFolder, updateState] = alertingFolderActionsApi.endpoints.pauseFolder.useMutation(); diff --git a/public/app/features/alerting/unified/hooks/abilities/otherAbilities.ts b/public/app/features/alerting/unified/hooks/abilities/otherAbilities.ts index a9f312c778116..d1e92ad41719f 100644 --- a/public/app/features/alerting/unified/hooks/abilities/otherAbilities.ts +++ b/public/app/features/alerting/unified/hooks/abilities/otherAbilities.ts @@ -10,12 +10,34 @@ import { type Abilities, type Ability, EnrichmentAction, + FolderAction, FolderBulkAction, Granted, InsufficientPermissions, NotSupported, } from './types'; +// ── Folder abilities ────────────────────────────────────────────────────────── + +/** + * This hooks fetches all computed folder abilities for the logged in user + * @lintignore + */ +export function useFolderAbilities(): Abilities<FolderAction> { + const canCreate = ctx.hasPermission(AccessControlAction.FoldersCreate); + return useMemo( + () => ({ + [FolderAction.Create]: canCreate ? Granted : InsufficientPermissions([AccessControlAction.FoldersCreate]), + }), + [canCreate] + ); +} + +export function useFolderAbility(action: FolderAction): Ability { + const all = useFolderAbilities(); + return useMemo(() => all[action], [all, action]); +} + // ── Folder bulk action abilities ────────────────────────────────────────────── export function useFolderBulkActionAbilities(): Abilities<FolderBulkAction> { diff --git a/public/app/features/alerting/unified/hooks/abilities/types.ts b/public/app/features/alerting/unified/hooks/abilities/types.ts index a9f65e6129c59..3928862931863 100644 --- a/public/app/features/alerting/unified/hooks/abilities/types.ts +++ b/public/app/features/alerting/unified/hooks/abilities/types.ts @@ -108,6 +108,11 @@ export enum FolderBulkAction { Delete = 'delete-folder', } +// this enum lists all of the actions we can perform on a folder +export enum FolderAction { + Create = 'create-folder', +} + // this enum lists all of the available actions we can perform with enrichments export enum EnrichmentAction { Read = 'read-enrichment', @@ -120,6 +125,7 @@ type Action = | RuleAction | ExternalRuleAction | FolderBulkAction + | FolderAction | EnrichmentAction; // ── Ability ───────────────────────────────────────────────────────────── diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 2c8915cbc455b..437f217cee2f3 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -25,6 +25,7 @@ import { rulerRuleType, } from '../utils/rules'; +import { type FolderBulkAction } from './abilities/types'; import { useIsRuleEditable } from './useIsRuleEditable'; /** @@ -98,12 +99,6 @@ export enum EnrichmentAction { Write = 'write-enrichment', } -// this enum list all of the bulk actions we can perform on a folder -export enum FolderBulkAction { - Pause = 'pause-folder', // unpause permissions are the same as pause - Delete = 'delete-folder', -} - // this enum lists all of the actions we can perform within alerting in general, not linked to a specific // alert source, rule or alertmanager export enum AlertingAction { @@ -159,21 +154,6 @@ export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction | Fol export type Ability = [actionSupported: boolean, actionAllowed: boolean]; export type Abilities<T extends Action> = Record<T, Ability>; -/** - * This one will check for folder abilities - */ -const useFolderBulkActionAbilities = (): Abilities<FolderBulkAction> => { - return { - [FolderBulkAction.Pause]: [AlwaysSupported, isAdmin()], - [FolderBulkAction.Delete]: [AlwaysSupported, isAdmin()], - }; -}; - -export const useFolderBulkActionAbility = (action: FolderBulkAction): Ability => { - const allAbilities = useFolderBulkActionAbilities(); - return allAbilities[action]; -}; - /** * This one will check for alerting abilities that don't apply to any particular alert source or alert rule */
← Back to Alerts View on GitHub →