mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix permissions for silences list view (#88908)
This commit is contained in:
parent
8aa1bbe27c
commit
b761153812
@ -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"})
|
||||
}
|
||||
|
||||
|
@ -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')
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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} />
|
||||
|
@ -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 && (
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user