Authorization bypass / Privilege escalation
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
*/