Privilege Escalation / Access Control

MEDIUM
grafana/grafana
Commit: abec7e486615
Affected: <=12.3.x (pre-fix); 12.4.0+ includes the fix
2026-06-03 18:14 UTC

Description

The commit refactors authentication/authorization checks for Grafana alerting notification policies from scattered, separate permission lists (READ/READ_EXTERNAL and MODIFY) to centralized ability hooks. It introduces new ability hooks (useNotificationPolicyAbility, useTimeIntervalAbility, useAlertGroupAbility, etc.) and unifies route gating to rely on explicit abilities rather than legacy permission lists. This reduces the risk of authorization bypass due to inconsistent RBAC checks across routes and UI components. It is a real security fix addressing access control consistency for notification policies in the Alerting feature.

Commit Details

Author: Gilles De Mey

Date: 2026-06-03 17:35 UTC

Message:

Alerting: Migrate notification policies to use centralized ability hooks (3/N) (#125259)

Triage Assessment

Vulnerability Type: Privilege Escalation / Access Control

Confidence: MEDIUM

Reasoning:

The commit migrates notification policy access checks to centralized ability hooks and adjusts route and UI gating to rely on explicit abilities rather than older permission lists. This refactors RBAC checks and reduces the risk of improper access control by consolidating authorization logic, which constitutes a security fix to authorization bypass risks.

Verification Assessment

Vulnerability Type: Privilege Escalation / Access Control

Confidence: MEDIUM

Affected Versions: <=12.3.x (pre-fix); 12.4.0+ includes the fix

Code Diff

diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index 7e33fb926320f..d39af316227f1 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -10,12 +10,9 @@ import { PERMISSIONS_TIME_INTERVALS_MODIFY, PERMISSIONS_TIME_INTERVALS_READ, } from './unified/components/mute-timings/permissions'; -import { - PERMISSIONS_NOTIFICATION_POLICIES_MODIFY, - PERMISSIONS_NOTIFICATION_POLICIES_READ, -} from './unified/components/notification-policies/permissions'; import { PERMISSIONS_TEMPLATES } from './unified/components/templates/permissions'; import { shouldAllowRecoveringDeletedRules } from './unified/featureToggles'; +import { PERMISSIONS_NOTIFICATION_POLICIES } from './unified/hooks/abilities/alertmanager/useNotificationPolicyAbility'; import { evaluateAccess } from './unified/utils/access-control'; export function getAlertingRoutes(cfg = config): RouteDescriptor[] { @@ -44,8 +41,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { roles: evaluateAccess([ AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead, - ...PERMISSIONS_NOTIFICATION_POLICIES_READ, - ...PERMISSIONS_NOTIFICATION_POLICIES_MODIFY, + ...PERMISSIONS_NOTIFICATION_POLICIES, ...PERMISSIONS_TIME_INTERVALS_READ, ...PERMISSIONS_TIME_INTERVALS_MODIFY, ]), @@ -102,11 +98,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { }, { path: '/alerting/routes/policy/:name/edit', - roles: evaluateAccess([ - AccessControlAction.AlertingNotificationsRead, - ...PERMISSIONS_NOTIFICATION_POLICIES_READ, - ...PERMISSIONS_NOTIFICATION_POLICIES_MODIFY, - ]), + roles: evaluateAccess([AccessControlAction.AlertingNotificationsRead, ...PERMISSIONS_NOTIFICATION_POLICIES]), component: config.featureToggles.alertingMultiplePolicies ? importAlertingComponent( () => diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx index e92459612a06f..b5da2d2e37aab 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.test.tsx @@ -7,7 +7,7 @@ import { byLabelText, byRole, byTestId } from 'testing-library-selector'; import { config } from '@grafana/runtime'; import { mockComboboxRect } from '@grafana/test-utils'; import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList'; -import { PERMISSIONS_NOTIFICATION_POLICIES } from 'app/features/alerting/unified/components/notification-policies/permissions'; +import { PERMISSIONS_NOTIFICATION_POLICIES } from 'app/features/alerting/unified/hooks/abilities/alertmanager/useNotificationPolicyAbility'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { getErrorResponse, @@ -50,6 +50,7 @@ import { deleteRoutingTree, getRoutingTree, resetRoutingTreeMap, + setAllRoutingTreePermissions, setRoutingTree, } from './mocks/server/entities/k8s/routingtrees'; import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; @@ -181,7 +182,6 @@ describe.each([ { testName: 'PolicyPage', renderPage: renderPolicyPage(OtherPolicyName), routeName: OtherPolicyName }, ])('$testName - Policy: $routeName', ({ testName, renderPage, routeName }) => { beforeAll(() => { - // combobox hack :/ mockComboboxRect(); }); @@ -292,6 +292,9 @@ describe.each([ routes: [], }) ); + // createKubernetesRoutingTreeSpec builds a minimal tree without k8s access annotations. + // Set them explicitly so the entity-level canEditEntity / canAdminEntity checks pass. + setAllRoutingTreePermissions({ canWrite: true, canDelete: true, canAdmin: true }); const { user } = renderPage(); // Sanity check to make sure we actually have an undefined root route. @@ -325,6 +328,11 @@ describe.each([ AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead, ]); + // Entity annotations must reflect RBAC: a user without write permission gets canWrite: false + // from the backend. Without this the mock is in an impossible state (entity canWrite: true + // for a user who has no write RBAC), which previously only happened to hide the button + // because of a now-removed redundant global-RBAC double-gate. + setAllRoutingTreePermissions({ canWrite: false, canDelete: false, canAdmin: false }); const { user } = renderPage(); diff --git a/public/app/features/alerting/unified/NotificationPoliciesPage.tsx b/public/app/features/alerting/unified/NotificationPoliciesPage.tsx index b68045d0f38c0..2ae99559c1555 100644 --- a/public/app/features/alerting/unified/NotificationPoliciesPage.tsx +++ b/public/app/features/alerting/unified/NotificationPoliciesPage.tsx @@ -19,7 +19,12 @@ import { useCreatePolicyAction, useListNotificationPolicyRoutes, } from 'app/features/alerting/unified/components/notification-policies/useNotificationPolicyRoute'; -import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities'; +import { isGranted } from 'app/features/alerting/unified/hooks/abilities/abilityUtils'; +import { + AlertGroupAction, + NotificationPolicyAction, + TimeIntervalAction, +} from 'app/features/alerting/unified/hooks/abilities/types'; import { useRouteGroupsMatcher } from 'app/features/alerting/unified/useRouteGroupsMatcher'; import { type ObjectMatcher } from 'app/plugins/datasource/alertmanager/types'; @@ -35,6 +40,9 @@ import { trackNotificationPoliciesFilterPolicyTree, trackNotificationPoliciesToggledAll, } from './components/notification-policies/notificationPolicyAnalytics'; +import { useAlertGroupAbility } from './hooks/abilities/alertmanager/useAlertGroupAbility'; +import { useNotificationPolicyAbility } from './hooks/abilities/alertmanager/useNotificationPolicyAbility'; +import { useTimeIntervalAbility } from './hooks/abilities/alertmanager/useTimeIntervalAbility'; import { useNotificationPoliciesNav } from './navigation/useNotificationConfigNav'; import { useAlertmanager } from './state/AlertmanagerContext'; import { ROOT_ROUTE_NAME } from './utils/k8s/constants'; @@ -55,15 +63,19 @@ const NotificationPoliciesTabs = () => { // Alertmanager logic and data hooks const { selectedAlertmanager = '' } = useAlertmanager(); - const [policiesSupported, canSeePoliciesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationPolicyTree); - const [timingsSupported, canSeeTimingsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewTimeInterval); + const policiesAbility = useNotificationPolicyAbility({ action: NotificationPolicyAction.ViewTree }); + const timingsAbility = useTimeIntervalAbility({ action: TimeIntervalAction.View }); + const canAccessPolicies = isGranted(policiesAbility); + const canAccessTimings = isGranted(timingsAbility); + const availableTabs = [ - canSeePoliciesTab && ActiveTab.NotificationPolicies, - canSeeTimingsTab && ActiveTab.TimeIntervals, + canAccessPolicies && ActiveTab.NotificationPolicies, + canAccessTimings && ActiveTab.TimeIntervals, ].filter((tab) => !!tab); + const { data: muteTimings = [] } = useMuteTimings({ alertmanager: selectedAlertmanager, - skip: !canSeeTimingsTab, + skip: !canAccessTimings, }); // Tab state management @@ -93,7 +105,7 @@ const NotificationPoliciesTabs = () => { <GrafanaAlertmanagerWarning currentAlertmanager={selectedAlertmanager} /> <InhibitionRulesAlert alertmanagerSourceName={selectedAlertmanager} /> <TabsBar> - {policiesSupported && canSeePoliciesTab && ( + {canAccessPolicies && ( <Tab label={t('alerting.notification-policies-tabs.label-notification-policies', 'Notification Policies')} active={policyTreeTabActive} @@ -103,7 +115,7 @@ const NotificationPoliciesTabs = () => { }} /> )} - {timingsSupported && canSeeTimingsTab && ( + {canAccessTimings && ( <Tab label={t('alerting.notification-policies-tabs.label-time-intervals', 'Time intervals')} active={muteTimingsTabActive} @@ -133,7 +145,7 @@ const NotificationPoliciesTabs = () => { */ function PolicyTreeTab() { const { selectedAlertmanager = '', isGrafanaAlertmanager } = useAlertmanager(); - const [, canSeeAlertGroups] = useAlertmanagerAbility(AlertmanagerAction.ViewAlertGroups); + const { granted: canSeeAlertGroups } = useAlertGroupAbility(AlertGroupAction.View); // Single worker + alert groups query shared by all PoliciesTree instances const { getRouteGroupsMap } = useRouteGroupsMatcher(); diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index afdda11247124..6ca2d6a46c819 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -21,9 +21,10 @@ import { import MuteTimingsSelector from 'app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector'; import { ExternalAlertmanagerContactPointSelector } from 'app/features/alerting/unified/components/notification-policies/ContactPointSelector'; import { handleContactPointSelect } from 'app/features/alerting/unified/components/notification-policies/utils'; -import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities'; +import { TimeIntervalAction } from 'app/features/alerting/unified/hooks/abilities/types'; import { MatcherOperator, type RouteWithID } from 'app/plugins/datasource/alertmanager/types'; +import { useTimeIntervalAbility } from '../../hooks/abilities/alertmanager/useTimeIntervalAbility'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { type FormAmRoute } from '../../types/amroutes'; import { matcherFieldOptions } from '../../utils/alertmanager'; @@ -53,7 +54,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults const styles = useStyles2(getStyles); const formStyles = useStyles2(getFormStyles); const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager(); - const [, canSeeMuteTimings] = useAlertmanagerAbility(AlertmanagerAction.ViewTimeInterval); + const { granted: canSeeMuteTimings } = useTimeIntervalAbility({ action: TimeIntervalAction.View }); const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route?.group_by)); const emptyMatcher = [{ name: '', operator: MatcherOperator.equal, value: '' }]; diff --git a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx index 3bd5641f95ea2..f3c7bb6760efa 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Filters.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Filters.tsx @@ -10,9 +10,10 @@ import { type RoutingTree } from '@grafana/api-clients/rtkq/notifications.alerti import { Trans, t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; import { Button, Field, Icon, Input, Label, Stack, Tooltip } from '@grafana/ui'; -import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alerting/unified/hooks/useAbilities'; +import { ContactPointAction } from 'app/features/alerting/unified/hooks/abilities/types'; import { type ObjectMatcher, type RouteWithID } from 'app/plugins/datasource/alertmanager/types'; +import { useContactPointAbility } from '../../hooks/abilities/alertmanager/useContactPointAbility'; import { useURLSearchParams } from '../../hooks/useURLSearchParams'; import { useAlertmanager } from '../../state/AlertmanagerContext'; import { matcherToObjectMatcher } from '../../utils/alertmanager'; @@ -31,7 +32,7 @@ interface NotificationPoliciesFilterProps { } const NotificationPoliciesFilter = ({ onChangeReceiver, onChangeMatchers }: NotificationPoliciesFilterProps) => { - const [contactPointsSupported, canSeeContactPoints] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint); + const { granted: canSeeContactPoints } = useContactPointAbility({ action: ContactPointAction.View }); const { isGrafanaAlertmanager } = useAlertmanager(); const [searchParams, setSearchParams] = useURLSearchParams(); const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams); @@ -108,7 +109,7 @@ const NotificationPoliciesFilter = ({ onChangeReceiver, onChangeMatchers }: Noti value={queryString ?? ''} /> </Field> - {contactPointsSupported && canSeeContactPoints && ( + {canSeeContactPoints && ( <Field label={t('alerting.notification-policies-filter.label-search-by-contact-point', 'Contact point')} noMargin diff --git a/public/app/features/alerting/unified/components/notification-policies/PoliciesList.test.tsx b/public/app/features/alerting/unified/components/notification-policies/PoliciesList.test.tsx index 4da6ffd37d747..d73303ea7b4f0 100644 --- a/public/app/features/alerting/unified/components/notification-policies/PoliciesList.test.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/PoliciesList.test.tsx @@ -10,9 +10,22 @@ import { setupDataSources } from 'app/features/alerting/unified/testSetup/dataso import { AccessControlAction } from '../../../../../types/accessControl'; import NotificationPolicies from '../../NotificationPoliciesPage'; -import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; +import { useContactPointAbility } from '../../hooks/abilities/alertmanager/useContactPointAbility'; +import { useNotificationPolicyAbility } from '../../hooks/abilities/alertmanager/useNotificationPolicyAbility'; +import { + type Ability, + Granted, + InsufficientPermissions, + NotSupported, + NotificationPolicyAction, +} from '../../hooks/abilities/types'; import { grantUserPermissions, mockDataSource } from '../../mocks'; -import { getRoutingTree, getRoutingTreeList, resetRoutingTreeMap } from '../../mocks/server/entities/k8s/routingtrees'; +import { + getRoutingTree, + getRoutingTreeList, + resetRoutingTreeMap, + setAllRoutingTreePermissions, +} from '../../mocks/server/entities/k8s/routingtrees'; import { KnownProvenance } from '../../types/knownProvenanc ... [truncated]
← Back to Alerts View on GitHub →