mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add FGAC for Silences (#46479)
* add FGAC actions for silences table * redirect users without permissions * hide silence button in rules list * add permissions checks to routes * add read action for silences page * add permissions checks to navigation * add additional access checks for rule viewing * create authorize component * add tests for silences * hide alerting nav for users without access * nolint: gocyclo * add permission check to alert details * add check for external instances * remove unecessary new lines * use correct actions for alert details * fix failing tests Co-authored-by: Yuriy Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
parent
2ade8b56dd
commit
5a25ada3d0
@ -4,6 +4,8 @@ import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynami
|
|||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { RouteDescriptor } from 'app/core/navigation/types';
|
import { RouteDescriptor } from 'app/core/navigation/types';
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
|
import { contextSrv } from 'app/core/core';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
const commonRoutes: RouteDescriptor[] = [
|
const commonRoutes: RouteDescriptor[] = [
|
||||||
{
|
{
|
||||||
@ -114,20 +116,21 @@ const unifiedRoutes: RouteDescriptor[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silences',
|
path: '/alerting/silences',
|
||||||
|
roles: () => contextSrv.evaluatePermission(() => [], [AccessControlAction.AlertingInstanceRead]),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silence/new',
|
path: '/alerting/silence/new',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceCreate]),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/alerting/silence/:id/edit',
|
path: '/alerting/silence/:id/edit',
|
||||||
roles: () => ['Editor', 'Admin'],
|
roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceUpdate]),
|
||||||
component: SafeDynamicImport(
|
component: SafeDynamicImport(
|
||||||
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
|
||||||
),
|
),
|
||||||
|
@ -12,7 +12,12 @@ import { DataSourceType } from './utils/datasource';
|
|||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
jest.mock('./api/alertmanager');
|
jest.mock('./api/alertmanager');
|
||||||
|
jest.mock('app/core/services/context_srv', () => ({
|
||||||
|
contextSrv: {
|
||||||
|
isEditor: true,
|
||||||
|
hasAccess: () => true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
const mocks = {
|
const mocks = {
|
||||||
api: {
|
api: {
|
||||||
fetchAlertGroups: jest.mocked(fetchAlertGroups),
|
fetchAlertGroups: jest.mocked(fetchAlertGroups),
|
||||||
|
@ -13,8 +13,11 @@ import { parseMatchers } from './utils/alertmanager';
|
|||||||
import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertState, MatcherOperator } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
|
import { byLabelText, byPlaceholderText, byRole, byTestId, byText } from 'testing-library-selector';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
jest.mock('./api/alertmanager');
|
jest.mock('./api/alertmanager');
|
||||||
|
jest.mock('app/core/services/context_srv');
|
||||||
|
|
||||||
const TEST_TIMEOUT = 60000;
|
const TEST_TIMEOUT = 60000;
|
||||||
|
|
||||||
@ -24,6 +27,7 @@ const mocks = {
|
|||||||
fetchAlerts: jest.mocked(fetchAlerts),
|
fetchAlerts: jest.mocked(fetchAlerts),
|
||||||
createOrUpdateSilence: jest.mocked(createOrUpdateSilence),
|
createOrUpdateSilence: jest.mocked(createOrUpdateSilence),
|
||||||
},
|
},
|
||||||
|
contextSrv: jest.mocked(contextSrv),
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderSilences = (location = '/alerting/silences/') => {
|
const renderSilences = (location = '/alerting/silences/') => {
|
||||||
@ -50,6 +54,7 @@ const ui = {
|
|||||||
silencesTable: byTestId('dynamic-table'),
|
silencesTable: byTestId('dynamic-table'),
|
||||||
silenceRow: byTestId('row'),
|
silenceRow: byTestId('row'),
|
||||||
silencedAlertCell: byTestId('alerts'),
|
silencedAlertCell: byTestId('alerts'),
|
||||||
|
addSilenceButton: byRole('button', { name: /new silence/i }),
|
||||||
queryBar: byPlaceholderText('Search'),
|
queryBar: byPlaceholderText('Search'),
|
||||||
editor: {
|
editor: {
|
||||||
timeRange: byLabelText('Timepicker', { exact: false }),
|
timeRange: byLabelText('Timepicker', { exact: false }),
|
||||||
@ -89,6 +94,20 @@ const resetMocks = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence());
|
mocks.api.createOrUpdateSilence.mockResolvedValue(mockSilence());
|
||||||
|
|
||||||
|
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
|
||||||
|
mocks.contextSrv.hasPermission.mockImplementation((action) => {
|
||||||
|
const permissions = [
|
||||||
|
AccessControlAction.AlertingInstanceRead,
|
||||||
|
AccessControlAction.AlertingInstanceCreate,
|
||||||
|
AccessControlAction.AlertingInstanceUpdate,
|
||||||
|
AccessControlAction.AlertingInstancesExternalRead,
|
||||||
|
AccessControlAction.AlertingInstancesExternalWrite,
|
||||||
|
];
|
||||||
|
return permissions.includes(action as AccessControlAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation(() => true);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Silences', () => {
|
describe('Silences', () => {
|
||||||
@ -158,6 +177,28 @@ describe('Silences', () => {
|
|||||||
},
|
},
|
||||||
TEST_TIMEOUT
|
TEST_TIMEOUT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
it('shows creating a silence button for users with access', async () => {
|
||||||
|
renderSilences();
|
||||||
|
|
||||||
|
await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
|
||||||
|
await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
|
||||||
|
|
||||||
|
expect(ui.addSilenceButton.get()).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides actions for creating a silence for users without access', async () => {
|
||||||
|
mocks.contextSrv.hasAccess.mockImplementation((action) => {
|
||||||
|
const permissions = [AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead];
|
||||||
|
return permissions.includes(action as AccessControlAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
renderSilences();
|
||||||
|
await waitFor(() => expect(mocks.api.fetchSilences).toHaveBeenCalled());
|
||||||
|
await waitFor(() => expect(mocks.api.fetchAlerts).toHaveBeenCalled());
|
||||||
|
|
||||||
|
expect(ui.addSilenceButton.query()).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Silence edit', () => {
|
describe('Silence edit', () => {
|
||||||
|
@ -13,6 +13,8 @@ import { AsyncRequestState, initialAsyncRequestState } from './utils/redux';
|
|||||||
import SilencesEditor from './components/silences/SilencesEditor';
|
import SilencesEditor from './components/silences/SilencesEditor';
|
||||||
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
import { AlertManagerPicker } from './components/AlertManagerPicker';
|
||||||
import { Silence } from 'app/plugins/datasource/alertmanager/types';
|
import { Silence } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
import { Authorize } from './components/Authorize';
|
||||||
|
|
||||||
const Silences: FC = () => {
|
const Silences: FC = () => {
|
||||||
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
const [alertManagerSourceName, setAlertManagerSourceName] = useAlertManagerSourceName();
|
||||||
@ -51,7 +53,9 @@ const Silences: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AlertingPageWrapper pageId="silences">
|
<AlertingPageWrapper pageId="silences">
|
||||||
|
<Authorize actions={[AccessControlAction.AlertingInstancesExternalRead]}>
|
||||||
<AlertManagerPicker disabled={!isRoot} current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
<AlertManagerPicker disabled={!isRoot} current={alertManagerSourceName} onChange={setAlertManagerSourceName} />
|
||||||
|
</Authorize>
|
||||||
{error && !loading && (
|
{error && !loading && (
|
||||||
<Alert severity="error" title="Error loading silences">
|
<Alert severity="error" title="Error loading silences">
|
||||||
{error.message || 'Unknown error.'}
|
{error.message || 'Unknown error.'}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
actions: AccessControlAction[];
|
||||||
|
fallback?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Authorize: FC<Props> = ({ actions, children, fallback = true }) => {
|
||||||
|
if (actions.some((action) => contextSrv.hasAccess(action, fallback))) {
|
||||||
|
return <>{children}</>;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
@ -1,10 +1,14 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||||
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import { isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
|
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
|
||||||
import { AnnotationDetailsField } from '../AnnotationDetailsField';
|
import { AnnotationDetailsField } from '../AnnotationDetailsField';
|
||||||
|
import { Authorize } from '../Authorize';
|
||||||
|
|
||||||
interface AmNotificationsAlertDetailsProps {
|
interface AmNotificationsAlertDetailsProps {
|
||||||
alertManagerSourceName: string;
|
alertManagerSourceName: string;
|
||||||
@ -13,9 +17,18 @@ interface AmNotificationsAlertDetailsProps {
|
|||||||
|
|
||||||
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={styles.actionsRow}>
|
<div className={styles.actionsRow}>
|
||||||
|
<Authorize
|
||||||
|
actions={
|
||||||
|
isExternalAM
|
||||||
|
? [AccessControlAction.AlertingInstancesExternalWrite]
|
||||||
|
: [AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstanceUpdate]
|
||||||
|
}
|
||||||
|
fallback={contextSrv.isEditor}
|
||||||
|
>
|
||||||
{alert.status.state === AlertState.Suppressed && (
|
{alert.status.state === AlertState.Suppressed && (
|
||||||
<LinkButton
|
<LinkButton
|
||||||
href={`${makeAMLink(
|
href={`${makeAMLink(
|
||||||
@ -39,11 +52,16 @@ export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, aler
|
|||||||
Silence
|
Silence
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
|
</Authorize>
|
||||||
|
<Authorize
|
||||||
|
actions={isExternalAM ? [AccessControlAction.DataSourcesExplore] : [AccessControlAction.AlertingInstanceRead]}
|
||||||
|
>
|
||||||
{alert.generatorURL && (
|
{alert.generatorURL && (
|
||||||
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
||||||
See source
|
See source
|
||||||
</LinkButton>
|
</LinkButton>
|
||||||
)}
|
)}
|
||||||
|
</Authorize>
|
||||||
</div>
|
</div>
|
||||||
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => (
|
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => (
|
||||||
<AnnotationDetailsField key={annotationKey} annotationKey={annotationKey} value={annotationValue} />
|
<AnnotationDetailsField key={annotationKey} annotationKey={annotationKey} value={annotationValue} />
|
||||||
|
@ -18,6 +18,7 @@ import { getAlertmanagerByUid } from '../../utils/alertmanager';
|
|||||||
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
||||||
import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||||
import { isFederatedRuleGroup } from '../../utils/rules';
|
import { isFederatedRuleGroup } from '../../utils/rules';
|
||||||
|
import { AccessControlAction } from 'app/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
rule: CombinedRule;
|
rule: CombinedRule;
|
||||||
@ -138,7 +139,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (alertmanagerSourceName) {
|
if (alertmanagerSourceName && contextSrv.hasAccess(AccessControlAction.AlertingInstanceCreate, contextSrv.isEditor)) {
|
||||||
leftButtons.push(
|
leftButtons.push(
|
||||||
<LinkButton
|
<LinkButton
|
||||||
className={style.button}
|
className={style.button}
|
||||||
|
@ -18,6 +18,9 @@ import { useDispatch } from 'react-redux';
|
|||||||
import { expireSilenceAction } from '../../state/actions';
|
import { expireSilenceAction } from '../../state/actions';
|
||||||
import { SilenceDetails } from './SilenceDetails';
|
import { SilenceDetails } from './SilenceDetails';
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
|
import { AccessControlAction } from '../../../../../types';
|
||||||
|
import { Authorize } from '../Authorize';
|
||||||
|
import { isGrafanaRulesSource } from '../../utils/datasource';
|
||||||
|
|
||||||
export interface SilenceTableItem extends Silence {
|
export interface SilenceTableItem extends Silence {
|
||||||
silencedAlerts: AlertmanagerAlert[];
|
silencedAlerts: AlertmanagerAlert[];
|
||||||
@ -35,6 +38,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [queryParams] = useQueryParams();
|
const [queryParams] = useQueryParams();
|
||||||
const filteredSilences = useFilteredSilences(silences);
|
const filteredSilences = useFilteredSilences(silences);
|
||||||
|
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
||||||
|
|
||||||
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
|
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
|
||||||
|
|
||||||
@ -61,7 +65,14 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
{!!silences.length && (
|
{!!silences.length && (
|
||||||
<>
|
<>
|
||||||
<SilencesFilter />
|
<SilencesFilter />
|
||||||
{contextSrv.isEditor && (
|
<Authorize
|
||||||
|
actions={
|
||||||
|
isExternalAM
|
||||||
|
? [AccessControlAction.AlertingInstancesExternalWrite]
|
||||||
|
: [AccessControlAction.AlertingInstanceCreate]
|
||||||
|
}
|
||||||
|
fallback={contextSrv.isEditor}
|
||||||
|
>
|
||||||
<div className={styles.topButtonContainer}>
|
<div className={styles.topButtonContainer}>
|
||||||
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
||||||
<Button className={styles.addNewSilence} icon="plus">
|
<Button className={styles.addNewSilence} icon="plus">
|
||||||
@ -69,7 +80,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
|||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</Authorize>
|
||||||
{!!items.length ? (
|
{!!items.length ? (
|
||||||
<>
|
<>
|
||||||
<DynamicTable
|
<DynamicTable
|
||||||
@ -167,11 +178,15 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
function useColumns(alertManagerSourceName: string) {
|
function useColumns(alertManagerSourceName: string) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
||||||
return useMemo((): SilenceTableColumnProps[] => {
|
return useMemo((): SilenceTableColumnProps[] => {
|
||||||
const handleExpireSilenceClick = (id: string) => {
|
const handleExpireSilenceClick = (id: string) => {
|
||||||
dispatch(expireSilenceAction(alertManagerSourceName, id));
|
dispatch(expireSilenceAction(alertManagerSourceName, id));
|
||||||
};
|
};
|
||||||
const showActions = contextSrv.isEditor;
|
const showActions = contextSrv.hasAccess(
|
||||||
|
isExternalAM ? AccessControlAction.AlertingInstancesExternalWrite : AccessControlAction.AlertingInstanceUpdate,
|
||||||
|
contextSrv.isEditor
|
||||||
|
);
|
||||||
const columns: SilenceTableColumnProps[] = [
|
const columns: SilenceTableColumnProps[] = [
|
||||||
{
|
{
|
||||||
id: 'state',
|
id: 'state',
|
||||||
@ -247,7 +262,7 @@ function useColumns(alertManagerSourceName: string) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return columns;
|
return columns;
|
||||||
}, [alertManagerSourceName, dispatch, styles]);
|
}, [alertManagerSourceName, dispatch, styles, isExternalAM]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SilencesTable;
|
export default SilencesTable;
|
||||||
|
@ -80,6 +80,35 @@ export enum AccessControlAction {
|
|||||||
FoldersCreate = 'folders:create',
|
FoldersCreate = 'folders:create',
|
||||||
FoldersPermissionsRead = 'folders.permissions:read',
|
FoldersPermissionsRead = 'folders.permissions:read',
|
||||||
FoldersPermissionsWrite = 'folders.permissions:read',
|
FoldersPermissionsWrite = 'folders.permissions:read',
|
||||||
|
|
||||||
|
// Alerting rules
|
||||||
|
AlertingRuleCreate = 'alert.rules:create',
|
||||||
|
AlertingRuleRead = 'alert.rules:read',
|
||||||
|
AlertingRuleUpdate = 'alert.rules:update',
|
||||||
|
AlertingRuleDelete = 'alert.rules:delete',
|
||||||
|
|
||||||
|
// Alerting instances (+silences)
|
||||||
|
AlertingInstanceCreate = 'alert.instances:create',
|
||||||
|
AlertingInstanceUpdate = 'alert.instances:update',
|
||||||
|
AlertingInstanceRead = 'alert.instances:read',
|
||||||
|
|
||||||
|
// Alerting Notification policies
|
||||||
|
AlertingNotificationsCreate = 'alert.notifications:create',
|
||||||
|
AlertingNotificationsRead = 'alert.notifications:read',
|
||||||
|
AlertingNotificationsUpdate = 'alert.notifications:update',
|
||||||
|
AlertingNotificationsDelete = 'alert.notifications:delete',
|
||||||
|
|
||||||
|
// External alerting rule actions.
|
||||||
|
AlertingRuleExternalWrite = 'alert.rules.external:write',
|
||||||
|
AlertingRuleExternalRead = 'alert.rules.external:read',
|
||||||
|
|
||||||
|
// External alerting instances actions.
|
||||||
|
AlertingInstancesExternalWrite = 'alert.instances.external:write',
|
||||||
|
AlertingInstancesExternalRead = 'alert.instances.external:read',
|
||||||
|
|
||||||
|
// External alerting notifications actions.
|
||||||
|
AlertingNotificationsExternalWrite = 'alert.notifications.external:write',
|
||||||
|
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
|
Loading…
Reference in New Issue
Block a user