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);
}