import { useMemo } from 'react'; import { contextSrv as ctx } from 'app/core/services/context_srv'; import { PERMISSIONS_CONTACT_POINTS_READ } from 'app/features/alerting/unified/components/contact-points/permissions'; import { PERMISSIONS_TIME_INTERVALS_MODIFY, PERMISSIONS_TIME_INTERVALS_READ, } from 'app/features/alerting/unified/components/mute-timings/permissions'; import { PERMISSIONS_NOTIFICATION_POLICIES_MODIFY, PERMISSIONS_NOTIFICATION_POLICIES_READ, } from 'app/features/alerting/unified/components/notification-policies/permissions'; import { useFolder } from 'app/features/alerting/unified/hooks/useFolder'; import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types'; import { AccessControlAction } from 'app/types'; import { CombinedRule, RuleGroupIdentifierV2 } from 'app/types/unified-alerting'; import { RulerRuleDTO } from 'app/types/unified-alerting-dto'; import { alertmanagerApi } from '../api/alertmanagerApi'; import { useAlertmanager } from '../state/AlertmanagerContext'; import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control'; import { getRulesSourceName } from '../utils/datasource'; import { getGroupOriginName } from '../utils/groupIdentifier'; import { isAdmin } from '../utils/misc'; import { isFederatedRuleGroup, isGrafanaRecordingRule, isGrafanaRulerRule, isPluginProvidedRule } from '../utils/rules'; import { useIsRuleEditable } from './useIsRuleEditable'; /** * These hooks will determine if * 1. the action is supported in the current context (alertmanager, alert rule or general context) * 2. user is allowed to perform actions based on their set of permissions / assigned role */ // this enum lists all of the available actions we can perform within the context of an alertmanager export enum AlertmanagerAction { // configuration ViewExternalConfiguration = 'view-external-configuration', UpdateExternalConfiguration = 'update-external-configuration', // contact points CreateContactPoint = 'create-contact-point', ViewContactPoint = 'view-contact-point', UpdateContactPoint = 'edit-contact-points', DeleteContactPoint = 'delete-contact-point', ExportContactPoint = 'export-contact-point', // notification templates CreateNotificationTemplate = 'create-notification-template', ViewNotificationTemplate = 'view-notification-template', UpdateNotificationTemplate = 'edit-notification-template', DeleteNotificationTemplate = 'delete-notification-template', DecryptSecrets = 'decrypt-secrets', // notification policies CreateNotificationPolicy = 'create-notification-policy', ViewNotificationPolicyTree = 'view-notification-policy-tree', UpdateNotificationPolicyTree = 'update-notification-policy-tree', DeleteNotificationPolicy = 'delete-notification-policy', ExportNotificationPolicies = 'export-notification-policies', ViewAutogeneratedPolicyTree = 'view-autogenerated-policy-tree', // silences – these cannot be deleted only "expired" (updated) CreateSilence = 'create-silence', ViewSilence = 'view-silence', UpdateSilence = 'update-silence', PreviewSilencedInstances = 'preview-silenced-alerts', // mute timings ViewMuteTiming = 'view-mute-timing', CreateMuteTiming = 'create-mute-timing', UpdateMuteTiming = 'update-mute-timing', DeleteMuteTiming = 'delete-mute-timing', ExportMuteTimings = 'export-mute-timings', // Alert groups ViewAlertGroups = 'view-alert-groups', } // this enum lists all of the available actions we can take on a single alert rule export enum AlertRuleAction { Duplicate = 'duplicate-alert-rule', View = 'view-alert-rule', Update = 'update-alert-rule', Delete = 'delete-alert-rule', Explore = 'explore-alert-rule', Silence = 'silence-alert-rule', ModifyExport = 'modify-export-rule', Pause = 'pause-alert-rule', Restore = 'restore-alert-rule', } // 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 { // internal (Grafana managed) CreateAlertRule = 'create-alert-rule', ViewAlertRule = 'view-alert-rule', UpdateAlertRule = 'update-alert-rule', DeleteAlertRule = 'delete-alert-rule', ExportGrafanaManagedRules = 'export-grafana-managed-rules', ReadConfigurationStatus = 'read-configuration-status', // external (any compatible alerting data source) CreateExternalAlertRule = 'create-external-alert-rule', ViewExternalAlertRule = 'view-external-alert-rule', UpdateExternalAlertRule = 'update-external-alert-rule', DeleteExternalAlertRule = 'delete-external-alert-rule', } // these just makes it easier to read the code :) const AlwaysSupported = true; const NotSupported = false; export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction; export type Ability = [actionSupported: boolean, actionAllowed: boolean]; export type Abilities = Record; /** * This one will check for alerting abilities that don't apply to any particular alert source or alert rule */ export const useAlertingAbilities = (): Abilities => { return { // internal (Grafana managed) [AlertingAction.CreateAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleCreate), [AlertingAction.ViewAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleRead), [AlertingAction.UpdateAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleUpdate), [AlertingAction.DeleteAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleDelete), [AlertingAction.ExportGrafanaManagedRules]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleRead), [AlertingAction.ReadConfigurationStatus]: [ AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingInstanceRead) || ctx.hasPermission(AccessControlAction.AlertingNotificationsRead), ], // external [AlertingAction.CreateExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite), [AlertingAction.ViewExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalRead), [AlertingAction.UpdateExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite), [AlertingAction.DeleteExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite), }; }; export const useAlertingAbility = (action: AlertingAction): Ability => { const allAbilities = useAlertingAbilities(); return allAbilities[action]; }; /** * This hook will check if we support the action and have sufficient permissions for it on a single alert rule */ export function useAlertRuleAbility(rule: CombinedRule, action: AlertRuleAction): Ability { const abilities = useAllAlertRuleAbilities(rule); return useMemo(() => { return abilities[action]; }, [abilities, action]); } export function useAlertRuleAbilities(rule: CombinedRule, actions: AlertRuleAction[]): Ability[] { const abilities = useAllAlertRuleAbilities(rule); return useMemo(() => { return actions.map((action) => abilities[action]); }, [abilities, actions]); } export function useRulerRuleAbility( rule: RulerRuleDTO | undefined, groupIdentifier: RuleGroupIdentifierV2, action: AlertRuleAction ): Ability { const abilities = useAllRulerRuleAbilities(rule, groupIdentifier); return useMemo(() => { return abilities[action]; }, [abilities, action]); } export function useRulerRuleAbilities( rule: RulerRuleDTO, groupIdentifier: RuleGroupIdentifierV2, actions: AlertRuleAction[] ): Ability[] { const abilities = useAllRulerRuleAbilities(rule, groupIdentifier); return useMemo(() => { return actions.map((action) => abilities[action]); }, [abilities, actions]); } // This hook is being called a lot in different places // In some cases multiple times for ~80 rules (e.g. on the list page) // We need to investigate further if some of these calls are redundant // In the meantime, memoizing the result helps export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities { const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource); const { isEditable, isRemovable, isRulerAvailable = false, loading, } = useIsRuleEditable(rulesSourceName, rule.rulerRule); const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules); const canSilence = useCanSilence(rule.rulerRule); const abilities = useMemo>(() => { const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance); const isFederated = isFederatedRuleGroup(rule.group); const isGrafanaManagedAlertRule = isGrafanaRulerRule(rule.rulerRule); const isPluginProvided = isPluginProvidedRule(rule.rulerRule); // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited const immutableRule = isProvisioned || isFederated || isPluginProvided; // while we gather info, pretend it's not supported const MaybeSupported = loading ? NotSupported : isRulerAvailable; const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported; // Creating duplicates of plugin-provided rules does not seem to make a lot of sense const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported; const rulesPermissions = getRulesPermissions(rulesSourceName); const abilities: Abilities = { [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create), [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read), [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false], [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false], [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore), [AlertRuleAction.Silence]: canSilence, [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed], [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], [AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], }; return abilities; }, [rule, loading, isRulerAvailable, isEditable, isRemovable, rulesSourceName, exportAllowed, canSilence]); return abilities; } export function useAllRulerRuleAbilities( rule: RulerRuleDTO | undefined, groupIdentifier: RuleGroupIdentifierV2 ): Abilities { const rulesSourceName = getGroupOriginName(groupIdentifier); const { isEditable, isRemovable, isRulerAvailable = false, loading } = useIsRuleEditable(rulesSourceName, rule); const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules); const canSilence = useCanSilence(rule); const abilities = useMemo>(() => { const isProvisioned = isGrafanaRulerRule(rule) && Boolean(rule.grafana_alert.provenance); // const isFederated = isFederatedRuleGroup(); const isFederated = false; const isGrafanaManagedAlertRule = isGrafanaRulerRule(rule); const isPluginProvided = isPluginProvidedRule(rule); // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited const immutableRule = isProvisioned || isFederated || isPluginProvided; // while we gather info, pretend it's not supported const MaybeSupported = loading ? NotSupported : isRulerAvailable; const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported; // Creating duplicates of plugin-provided rules does not seem to make a lot of sense const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported; const rulesPermissions = getRulesPermissions(rulesSourceName); const abilities: Abilities = { [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create), [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read), [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false], [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false], [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore), [AlertRuleAction.Silence]: canSilence, [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed], [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], [AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false], }; return abilities; }, [rule, loading, isRulerAvailable, rulesSourceName, isEditable, isRemovable, canSilence, exportAllowed]); return abilities; } export function useAllAlertmanagerAbilities(): Abilities { const { selectedAlertmanager, hasConfigurationAPI, isGrafanaAlertmanager: isGrafanaFlavoredAlertmanager, } = useAlertmanager(); // These are used for interacting with Alertmanager resources where we apply alert.notifications: permissions. // There are different permissions based on wether the built-in alertmanager is selected (grafana) or an external one. const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!); const instancePermissions = getInstancesPermissions(selectedAlertmanager!); // list out all of the abilities, and if the user has permissions to perform them const abilities: Abilities = { // -- configuration -- [AlertmanagerAction.ViewExternalConfiguration]: toAbility( AlwaysSupported, AccessControlAction.AlertingNotificationsExternalRead ), [AlertmanagerAction.UpdateExternalConfiguration]: toAbility( hasConfigurationAPI, AccessControlAction.AlertingNotificationsExternalWrite ), // -- contact points -- [AlertmanagerAction.CreateContactPoint]: toAbility( hasConfigurationAPI, notificationsPermissions.create, // TODO: Move this into the permissions config and generalise that code to allow for an array of permissions ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversCreate] : []) ), [AlertmanagerAction.ViewContactPoint]: toAbility( AlwaysSupported, notificationsPermissions.read, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_CONTACT_POINTS_READ : []) ), [AlertmanagerAction.UpdateContactPoint]: toAbility( hasConfigurationAPI, notificationsPermissions.update, ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : []) ), [AlertmanagerAction.DeleteContactPoint]: toAbility( hasConfigurationAPI, notificationsPermissions.delete, ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : []) ), // At the time of writing, only Grafana flavored alertmanager supports exporting, // and if a user can view the contact point, then they can also export it // So the only check we make is if the alertmanager is Grafana flavored [AlertmanagerAction.ExportContactPoint]: [isGrafanaFlavoredAlertmanager, isGrafanaFlavoredAlertmanager], // -- notification templates -- [AlertmanagerAction.CreateNotificationTemplate]: toAbility( hasConfigurationAPI, notificationsPermissions.create, ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : []) ), [AlertmanagerAction.ViewNotificationTemplate]: toAbility( AlwaysSupported, notificationsPermissions.read, ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesRead] : []) ), [AlertmanagerAction.UpdateNotificationTemplate]: toAbility( hasConfigurationAPI, notificationsPermissions.update, ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : []) ), [AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete), // -- notification policies -- [AlertmanagerAction.CreateNotificationPolicy]: toAbility( hasConfigurationAPI, notificationsPermissions.create, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_MODIFY : []) ), [AlertmanagerAction.ViewNotificationPolicyTree]: toAbility( AlwaysSupported, notificationsPermissions.read, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_READ : []) ), [AlertmanagerAction.UpdateNotificationPolicyTree]: toAbility( hasConfigurationAPI, notificationsPermissions.update, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_MODIFY : []) ), [AlertmanagerAction.DeleteNotificationPolicy]: toAbility( hasConfigurationAPI, notificationsPermissions.delete, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_MODIFY : []) ), [AlertmanagerAction.ExportNotificationPolicies]: toAbility( isGrafanaFlavoredAlertmanager, notificationsPermissions.read ), [AlertmanagerAction.DecryptSecrets]: toAbility( isGrafanaFlavoredAlertmanager, notificationsPermissions.provisioning.readSecrets ), [AlertmanagerAction.ViewAutogeneratedPolicyTree]: [isGrafanaFlavoredAlertmanager, isAdmin()], // -- silences -- // for now, all supported Alertmanager flavors have API endpoints for managing silences [AlertmanagerAction.CreateSilence]: toAbility(AlwaysSupported, instancePermissions.create), [AlertmanagerAction.ViewSilence]: toAbility(AlwaysSupported, instancePermissions.read), [AlertmanagerAction.UpdateSilence]: toAbility(AlwaysSupported, instancePermissions.update), [AlertmanagerAction.PreviewSilencedInstances]: toAbility(AlwaysSupported, instancePermissions.read), // -- mute timings -- [AlertmanagerAction.CreateMuteTiming]: toAbility( hasConfigurationAPI, notificationsPermissions.create, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : []) ), [AlertmanagerAction.ViewMuteTiming]: toAbility( AlwaysSupported, notificationsPermissions.read, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_READ : []) ), [AlertmanagerAction.UpdateMuteTiming]: toAbility( hasConfigurationAPI, notificationsPermissions.update, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : []) ), [AlertmanagerAction.DeleteMuteTiming]: toAbility( hasConfigurationAPI, notificationsPermissions.delete, ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : []) ), [AlertmanagerAction.ExportMuteTimings]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read), [AlertmanagerAction.ViewAlertGroups]: toAbility(AlwaysSupported, instancePermissions.read), }; return abilities; } export function useAlertmanagerAbility(action: AlertmanagerAction): Ability { const abilities = useAllAlertmanagerAbilities(); return useMemo(() => { return abilities[action]; }, [abilities, action]); } export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability[] { const abilities = useAllAlertmanagerAbilities(); return useMemo(() => { return actions.map((action) => abilities[action]); }, [abilities, actions]); } const { useGetGrafanaAlertingConfigurationStatusQuery } = alertmanagerApi; /** * We don't want to show the silence button if either * 1. the user has no permissions to create silences * 2. the admin has configured to only send instances to external AMs */ function useCanSilence(rule?: RulerRuleDTO): [boolean, boolean] { const folderUID = isGrafanaRulerRule(rule) ? rule.grafana_alert.namespace_uid : undefined; const { loading: folderIsLoading, folder } = useFolder(folderUID); const isGrafanaManagedRule = rule && isGrafanaRulerRule(rule); const isGrafanaRecording = rule && isGrafanaRecordingRule(rule); const silenceSupported = useGrafanaRulesSilenceSupport(); const canSilenceInFolder = useCanSilenceInFolder(folderUID); if (!rule) { return [false, false]; } // we don't support silencing when the rule is not a Grafana managed alerting rule // we simply don't know what Alertmanager the ruler is sending alerts to if (!isGrafanaManagedRule || isGrafanaRecording || folderIsLoading || !folder) { return [false, false]; } return [silenceSupported, canSilenceInFolder]; } function useCanSilenceInFolder(folderUID?: string) { const folderPermissions = useFolderPermissions(folderUID); const hasFolderSilencePermission = folderPermissions[AccessControlAction.AlertingSilenceCreate] ?? false; const hasGlobalSilencePermission = ctx.hasPermission(AccessControlAction.AlertingInstanceCreate); // User is permitted to silence if they either have the "global" permissions of "AlertingInstanceCreate", // or the folder specific access control of "AlertingSilenceCreate" const allowedToSilence = hasGlobalSilencePermission || hasFolderSilencePermission; return allowedToSilence; } function useGrafanaRulesSilenceSupport() { const { currentData: amConfigStatus, isLoading } = useGetGrafanaAlertingConfigurationStatusQuery(undefined); const interactsOnlyWithExternalAMs = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External; const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All; const silenceSupported = !interactsOnlyWithExternalAMs || interactsWithAll; return isLoading ? false : silenceSupported; } function useFolderPermissions(folderUID?: string): Record { const { folder } = useFolder(folderUID); return folder?.accessControl ?? {}; } // just a convenient function const toAbility = ( supported: boolean, /** If user has any of these permissions, then they are allowed to perform the action */ ...actions: AccessControlAction[] ): Ability => [supported, actions.some((action) => action && ctx.hasPermission(action))];