diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 8c41fb18173..62d0a07e8c4 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -396,8 +396,15 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Notification policies", SubTitle: "Determine how alerts are routed to contact points", Id: "am-routes", Url: s.cfg.AppSubURL + "/alerting/routes", Icon: "sitemap"}) } - if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceRead), ac.EvalPermission(ac.ActionAlertingInstancesExternalRead))) { + if hasAccess(ac.EvalAny( + ac.EvalPermission(ac.ActionAlertingInstanceRead), + ac.EvalPermission(ac.ActionAlertingInstancesExternalRead), + ac.EvalPermission(ac.ActionAlertingSilencesRead), + )) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Silences", SubTitle: "Stop notifications from one or more alerting rules", Id: "silences", Url: s.cfg.AppSubURL + "/alerting/silences", Icon: "bell-slash"}) + } + + if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingInstanceRead), ac.EvalPermission(ac.ActionAlertingInstancesExternalRead))) { alertChildNavs = append(alertChildNavs, &navtree.NavLink{Text: "Alert groups", SubTitle: "See grouped alerts from an Alertmanager instance", Id: "groups", Url: s.cfg.AppSubURL + "/alerting/groups", Icon: "layer-group"}) } diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index f00ebfa8c20..a481cade4ab 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -62,6 +62,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] { roles: evaluateAccess([ AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead, + AccessControlAction.AlertingSilenceRead, ]), component: importAlertingComponent( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') diff --git a/public/app/features/alerting/unified/Silences.test.tsx b/public/app/features/alerting/unified/Silences.test.tsx index 7f338d58962..d3ce1e15afb 100644 --- a/public/app/features/alerting/unified/Silences.test.tsx +++ b/public/app/features/alerting/unified/Silences.test.tsx @@ -81,18 +81,6 @@ const ui = { }, }; -const resetMocks = () => { - jest.resetAllMocks(); - - grantUserPermissions([ - AccessControlAction.AlertingInstanceRead, - AccessControlAction.AlertingInstanceCreate, - AccessControlAction.AlertingInstanceUpdate, - AccessControlAction.AlertingInstancesExternalRead, - AccessControlAction.AlertingInstancesExternalWrite, - ]); -}; - const setUserLogged = (isLogged: boolean) => { config.bootData.user.isSignedIn = isLogged; config.bootData.user.name = isLogged ? 'admin' : ''; @@ -115,12 +103,18 @@ const server = setupMswServer(); beforeEach(() => { setupDataSources(dataSources.am, dataSources[MOCK_DATASOURCE_NAME_BROKEN_ALERTMANAGER]); + grantUserPermissions([ + AccessControlAction.AlertingInstanceRead, + AccessControlAction.AlertingInstanceCreate, + AccessControlAction.AlertingInstanceUpdate, + AccessControlAction.AlertingInstancesExternalRead, + AccessControlAction.AlertingInstancesExternalWrite, + ]); }); -describe('Silences', () => { - beforeAll(resetMocks); - afterEach(resetMocks); +afterEach(() => jest.resetAllMocks()); +describe('Silences', () => { it( 'loads and shows silences', async () => { @@ -211,8 +205,6 @@ describe('Silences', () => { describe('Silence create/edit', () => { const baseUrlPath = '/alerting/silence/new'; - beforeAll(resetMocks); - afterEach(resetMocks); beforeEach(() => { mockAlertRuleApi(server).getAlertRule(MOCK_SILENCE_ID_EXISTING_ALERT_RULE_UID, grafanaRulerRule); diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx index 90d33346d38..340bdd52ae7 100644 --- a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.test.tsx @@ -5,14 +5,20 @@ import { Provider } from 'react-redux'; import { setupMswServer } from 'app/features/alerting/unified/mockApi'; import { setAlertmanagerChoices } from 'app/features/alerting/unified/mocks/server/configure'; import { configureStore } from 'app/store/configureStore'; +import { AccessControlAction } from 'app/types/accessControl'; import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types'; +import { grantUserPermissions } from '../mocks'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; import { GrafanaAlertmanagerDeliveryWarning } from './GrafanaAlertmanagerDeliveryWarning'; setupMswServer(); describe('GrafanaAlertmanagerDeliveryWarning', () => { + beforeEach(() => { + grantUserPermissions([AccessControlAction.AlertingNotificationsRead]); + }); + it('Should not render when the datasource is not Grafana', () => { setAlertmanagerChoices(AlertmanagerChoice.External, 0); diff --git a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx index 03be297409f..38016674de7 100644 --- a/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx +++ b/public/app/features/alerting/unified/components/GrafanaAlertmanagerDeliveryWarning.tsx @@ -6,6 +6,7 @@ import { Alert, useStyles2 } from '@grafana/ui/src'; import { AlertmanagerChoice } from '../../../../plugins/datasource/alertmanager/types'; import { alertmanagerApi } from '../api/alertmanagerApi'; +import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities'; import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource'; interface GrafanaAlertmanagerDeliveryWarningProps { @@ -14,12 +15,17 @@ interface GrafanaAlertmanagerDeliveryWarningProps { export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: GrafanaAlertmanagerDeliveryWarningProps) { const styles = useStyles2(getStyles); - const viewingInternalAM = currentAlertmanager === GRAFANA_RULES_SOURCE_NAME; + const externalAlertmanager = currentAlertmanager !== GRAFANA_RULES_SOURCE_NAME; + + const [readConfigurationStatusSupported, readConfigurationStatusAllowed] = useAlertingAbility( + AlertingAction.ReadConfigurationStatus + ); + const canReadConfigurationStatus = readConfigurationStatusSupported && readConfigurationStatusAllowed; const { currentData: amChoiceStatus } = alertmanagerApi.endpoints.getGrafanaAlertingConfigurationStatus.useQuery( undefined, { - skip: !viewingInternalAM, + skip: externalAlertmanager || !canReadConfigurationStatus, } ); @@ -27,7 +33,7 @@ export function GrafanaAlertmanagerDeliveryWarning({ currentAlertmanager }: Graf amChoiceStatus?.alertmanagersChoice && [AlertmanagerChoice.External, AlertmanagerChoice.All].includes(amChoiceStatus?.alertmanagersChoice); - if (!interactsWithExternalAMs || !viewingInternalAM) { + if (!interactsWithExternalAMs || externalAlertmanager) { return null; } diff --git a/public/app/features/alerting/unified/components/silences/SilenceDetails.tsx b/public/app/features/alerting/unified/components/silences/SilenceDetails.tsx index 4dfc8ab0782..2a8865a0f54 100644 --- a/public/app/features/alerting/unified/components/silences/SilenceDetails.tsx +++ b/public/app/features/alerting/unified/components/silences/SilenceDetails.tsx @@ -29,7 +29,7 @@ export const SilenceDetails = ({ silence }: Props) => {
{duration}
Created by
{createdBy}
- {silencedAlerts.length > 0 && ( + {Array.isArray(silencedAlerts) && ( <>
Affected alerts
diff --git a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx index 8ffe1c4dd51..969d048e92e 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesEditor.tsx @@ -30,6 +30,7 @@ import { MATCHER_ALERT_RULE_UID } from 'app/features/alerting/unified/utils/cons import { getDatasourceAPIUid, GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource'; import { MatcherOperator, SilenceCreatePayload } from 'app/plugins/datasource/alertmanager/types'; +import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { SilenceFormFields } from '../../types/silence-form'; import { matcherFieldToMatcher } from '../../utils/alertmanager'; import { makeAMLink } from '../../utils/misc'; @@ -113,6 +114,11 @@ export const SilencesEditor = ({ onCancel, ruleUid, }: SilencesEditorProps) => { + const [previewAlertsSupported, previewAlertsAllowed] = useAlertmanagerAbility( + AlertmanagerAction.PreviewSilencedInstances + ); + const canPreview = previewAlertsSupported && previewAlertsAllowed; + const [createSilence, { isLoading }] = alertSilencesApi.endpoints.createSilence.useMutation(); const formAPI = useForm({ defaultValues: formValues }); const styles = useStyles2(getStyles); @@ -236,7 +242,9 @@ export const SilencesEditor = ({ /> )} - + {canPreview && ( + + )} {isLoading && ( diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index e37071e765c..f590cc4565a 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -16,12 +16,12 @@ import { import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { Trans } from 'app/core/internationalization'; import { alertSilencesApi } from 'app/features/alerting/unified/api/alertSilencesApi'; -import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; import { featureDiscoveryApi } from 'app/features/alerting/unified/api/featureDiscoveryApi'; import { MATCHER_ALERT_RULE_UID, SILENCES_POLL_INTERVAL_MS } from 'app/features/alerting/unified/utils/constants'; import { GRAFANA_RULES_SOURCE_NAME, getDatasourceAPIUid } from 'app/features/alerting/unified/utils/datasource'; import { AlertmanagerAlert, Silence, SilenceState } from 'app/plugins/datasource/alertmanager/types'; +import { alertmanagerApi } from '../../api/alertmanagerApi'; import { AlertmanagerAction, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { parseMatchers } from '../../utils/alertmanager'; import { getSilenceFiltersFromUrlParams, makeAMLink, stringifyErrorLike } from '../../utils/misc'; @@ -35,7 +35,7 @@ import { SilenceStateTag } from './SilenceStateTag'; import { SilencesFilter } from './SilencesFilter'; export interface SilenceTableItem extends Silence { - silencedAlerts: AlertmanagerAlert[]; + silencedAlerts: AlertmanagerAlert[] | undefined; } type SilenceTableColumnProps = DynamicTableColumnProps; @@ -47,10 +47,15 @@ interface Props { const API_QUERY_OPTIONS = { pollingInterval: SILENCES_POLL_INTERVAL_MS, refetchOnFocus: true }; const SilencesTable = ({ alertManagerSourceName }: Props) => { + const [previewAlertsSupported, previewAlertsAllowed] = useAlertmanagerAbility( + AlertmanagerAction.PreviewSilencedInstances + ); + const canPreview = previewAlertsSupported && previewAlertsAllowed; + const { data: alertManagerAlerts = [], isLoading: amAlertsIsLoading } = alertmanagerApi.endpoints.getAlertmanagerAlerts.useQuery( { amSourceName: alertManagerSourceName, filter: { silenced: true, active: true, inhibited: true } }, - API_QUERY_OPTIONS + { ...API_QUERY_OPTIONS, skip: !canPreview } ); const { @@ -83,26 +88,26 @@ const SilencesTable = ({ alertManagerSourceName }: Props) => { return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id)); }; return filteredSilencesNotExpired.map((silence) => { - const silencedAlerts = findSilencedAlerts(silence.id); + const silencedAlerts = canPreview ? findSilencedAlerts(silence.id) : undefined; return { id: silence.id, data: { ...silence, silencedAlerts }, }; }); - }, [filteredSilencesNotExpired, alertManagerAlerts]); + }, [filteredSilencesNotExpired, alertManagerAlerts, canPreview]); const itemsExpired = useMemo((): SilenceTableItemProps[] => { const findSilencedAlerts = (id: string) => { return alertManagerAlerts.filter((alert) => alert.status.silencedBy.includes(id)); }; return filteredSilencesExpired.map((silence) => { - const silencedAlerts = findSilencedAlerts(silence.id); + const silencedAlerts = canPreview ? findSilencedAlerts(silence.id) : undefined; return { id: silence.id, data: { ...silence, silencedAlerts }, }; }); - }, [filteredSilencesExpired, alertManagerAlerts]); + }, [filteredSilencesExpired, alertManagerAlerts, canPreview]); if (isLoading || amAlertsIsLoading) { return ; @@ -304,7 +309,7 @@ function useColumns(alertManagerSourceName: string) { id: 'alerts', label: 'Alerts silenced', renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) { - return {silencedAlerts.length}; + return {Array.isArray(silencedAlerts) ? silencedAlerts.length : '-'}; }, size: 2, }, diff --git a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap index 02137bf67eb..c67bb2b2285 100644 --- a/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap +++ b/public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap @@ -136,6 +136,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a false, false, ], + "preview-silenced-alerts": [ + true, + false, + ], "update-external-configuration": [ false, false, @@ -245,6 +249,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi false, true, ], + "preview-silenced-alerts": [ + true, + true, + ], "update-external-configuration": [ true, true, @@ -354,6 +362,10 @@ exports[`alertmanager abilities should report everything is supported for builti true, true, ], + "preview-silenced-alerts": [ + true, + true, + ], "update-external-configuration": [ true, false, diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index 84f77d1d03f..0c3bebba8b1 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -53,6 +53,7 @@ export enum AlertmanagerAction { CreateSilence = 'create-silence', ViewSilence = 'view-silence', UpdateSilence = 'update-silence', + PreviewSilencedInstances = 'preview-silenced-alerts', // mute timings ViewMuteTiming = 'view-mute-timing', @@ -83,6 +84,7 @@ export enum AlertingAction { 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', @@ -110,6 +112,11 @@ export const useAlertingAbilities = (): Abilities => { [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), @@ -241,6 +248,7 @@ export function useAllAlertmanagerAbilities(): Abilities { [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 timtings -- [AlertmanagerAction.CreateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.create), [AlertmanagerAction.ViewMuteTiming]: toAbility(AlwaysSupported, notificationsPermissions.read), diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index ea3a1021aac..edda81712ae 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -48,6 +48,21 @@ export const notificationsPermissions = { }, }; +export const silencesPermissions = { + read: { + grafana: AccessControlAction.AlertingSilenceRead, + external: AccessControlAction.AlertingInstanceRead, + }, + create: { + grafana: AccessControlAction.AlertingSilenceCreate, + external: AccessControlAction.AlertingInstancesExternalWrite, + }, + update: { + grafana: AccessControlAction.AlertingSilenceUpdate, + external: AccessControlAction.AlertingInstancesExternalWrite, + }, +}; + export const provisioningPermissions = { read: AccessControlAction.AlertingProvisioningRead, readSecrets: AccessControlAction.AlertingProvisioningReadSecrets, diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index 600d72d9245..f4aed4ceacc 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -14,7 +14,7 @@ import { alertmanagerApi } from '../api/alertmanagerApi'; import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources'; import { isAlertManagerWithConfigAPI } from '../state/AlertmanagerContext'; -import { instancesPermissions, notificationsPermissions } from './access-control'; +import { instancesPermissions, notificationsPermissions, silencesPermissions } from './access-control'; import { getAllDataSources } from './config'; export const GRAFANA_RULES_SOURCE_NAME = 'grafana'; @@ -144,9 +144,15 @@ export function getAlertManagerDataSourcesByPermission(permission: 'instance' | const permissions = { instance: instancesPermissions.read, notification: notificationsPermissions.read, + silence: silencesPermissions.read, }; - if (contextSrv.hasPermission(permissions[permission].grafana)) { + const builtinAlertmanagerPermissions = Object.values(permissions).flatMap((permissions) => permissions.grafana); + const hasPermissionsForInternalAlertmanager = builtinAlertmanagerPermissions.some((permission) => + contextSrv.hasPermission(permission) + ); + + if (hasPermissionsForInternalAlertmanager) { availableInternalDataSources.push(grafanaAlertManagerDataSource); }