Alerting: Fix permissions for silences list view (#88908)

This commit is contained in:
Sonia Aguilar 2024-06-07 18:19:28 +02:00 committed by GitHub
parent 8aa1bbe27c
commit b761153812
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 99 additions and 33 deletions

View File

@ -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"})
}

View File

@ -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')

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ export const SilenceDetails = ({ silence }: Props) => {
<div>{duration}</div>
<div className={styles.title}>Created by</div>
<div>{createdBy}</div>
{silencedAlerts.length > 0 && (
{Array.isArray(silencedAlerts) && (
<>
<div className={styles.title}>Affected alerts</div>
<SilencedAlertsTable silencedAlerts={silencedAlerts} />

View File

@ -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 = ({
/>
</Field>
)}
<SilencedInstancesPreview amSourceName={alertManagerSourceName} matchers={matchers} ruleUid={ruleUid} />
{canPreview && (
<SilencedInstancesPreview amSourceName={alertManagerSourceName} matchers={matchers} ruleUid={ruleUid} />
)}
</FieldSet>
<Stack gap={1}>
{isLoading && (

View File

@ -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<SilenceTableItem>;
@ -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 <LoadingPlaceholder text="Loading silences..." />;
@ -304,7 +309,7 @@ function useColumns(alertManagerSourceName: string) {
id: 'alerts',
label: 'Alerts silenced',
renderCell: function renderSilencedAlerts({ data: { silencedAlerts } }) {
return <span data-testid="alerts">{silencedAlerts.length}</span>;
return <span data-testid="alerts">{Array.isArray(silencedAlerts) ? silencedAlerts.length : '-'}</span>;
},
size: 2,
},

View File

@ -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,

View File

@ -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> => {
[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> {
[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),

View File

@ -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,

View File

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