Alerting: useAbility hook for alert rules (#78231)

This commit is contained in:
Gilles De Mey 2023-11-28 10:48:42 +01:00 committed by GitHub
parent add096ac8c
commit 7dbbdc16a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 395 additions and 271 deletions

View File

@ -1,3 +1,4 @@
import { isEmpty } from 'lodash';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { useToggle } from 'react-use';
@ -7,35 +8,41 @@ import { Button, Dropdown, Icon, LinkButton, Menu, MenuItem } from '@grafana/ui'
import { logInfo, LogMessages } from './Analytics';
import { GrafanaRulesExporter } from './components/export/GrafanaRulesExporter';
import { AlertSourceAction, useAlertSourceAbility } from './hooks/useAbilities';
import { AlertingAction, useAlertingAbility } from './hooks/useAbilities';
interface Props {}
export function MoreActionsRuleButtons({}: Props) {
const [_, viewRuleAllowed] = useAlertSourceAbility(AlertSourceAction.ViewAlertRule);
const [createRuleSupported, createRuleAllowed] = useAlertSourceAbility(AlertSourceAction.CreateAlertRule);
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertSourceAbility(
AlertSourceAction.CreateExternalAlertRule
);
const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
const [createRuleSupported, createRuleAllowed] = useAlertingAbility(AlertingAction.CreateAlertRule);
const [createCloudRuleSupported, createCloudRuleAllowed] = useAlertingAbility(AlertingAction.CreateExternalAlertRule);
const [exportRulesSupported, exportRulesAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
const location = useLocation();
const [showExportDrawer, toggleShowExportDrawer] = useToggle(false);
const newMenu = (
<Menu>
{(canCreateGrafanaRules || canCreateCloudRules) && (
<MenuItem
url={urlUtil.renderUrl(`alerting/new/recording`, {
returnTo: location.pathname + location.search,
})}
label="New recording rule"
/>
)}
{viewRuleAllowed && <MenuItem onClick={toggleShowExportDrawer} label="Export all Grafana-managed rules" />}
</Menu>
);
const canCreateGrafanaRules = createRuleSupported && createRuleAllowed;
const canCreateCloudRules = createCloudRuleSupported && createCloudRuleAllowed;
const canExportRules = exportRulesSupported && exportRulesAllowed;
const menuItems: JSX.Element[] = [];
if (canCreateGrafanaRules || canCreateCloudRules) {
menuItems.push(
<MenuItem
label="New recording rule"
key="new-recording-rule"
url={urlUtil.renderUrl(`alerting/new/recording`, {
returnTo: location.pathname + location.search,
})}
/>
);
}
if (canExportRules) {
menuItems.push(
<MenuItem label="Export all Grafana-managed rules" key="export-all-rules" onClick={toggleShowExportDrawer} />
);
}
return (
<>
@ -49,13 +56,15 @@ export function MoreActionsRuleButtons({}: Props) {
</LinkButton>
)}
<Dropdown overlay={newMenu}>
<Button variant="secondary">
More
<Icon name="angle-down" />
</Button>
</Dropdown>
{showExportDrawer && <GrafanaRulesExporter onClose={toggleShowExportDrawer} />}
{!isEmpty(menuItems) && (
<Dropdown overlay={<Menu>{menuItems}</Menu>}>
<Button variant="secondary">
More
<Icon name="angle-down" />
</Button>
</Dropdown>
)}
{canExportRules && showExportDrawer && <GrafanaRulesExporter onClose={toggleShowExportDrawer} />}
</>
);
}

View File

@ -704,7 +704,7 @@ describe('RuleList', () => {
describe('RBAC Enabled', () => {
describe('Export button', () => {
it('Export button should be visible when the user has alert read permissions', async () => {
grantUserPermissions([AccessControlAction.AlertingRuleRead, AccessControlAction.FoldersRead]);
grantUserPermissions([AccessControlAction.AlertingRuleRead]);
mocks.getAllDataSourcesMock.mockReturnValue([]);
setDataSourceSrv(new MockDataSourceSrv({}));

View File

@ -4,19 +4,19 @@ import React, { PropsWithChildren } from 'react';
import {
Abilities,
Action,
AlertingAction,
AlertmanagerAction,
AlertSourceAction,
useAlertSourceAbilities,
useAlertingAbilities,
useAllAlertmanagerAbilities,
} from '../hooks/useAbilities';
interface AuthorizeProps extends PropsWithChildren {
actions: AlertmanagerAction[] | AlertSourceAction[];
actions: AlertmanagerAction[] | AlertingAction[];
}
export const Authorize = ({ actions, children }: AuthorizeProps) => {
const alertmanagerActions = filter(actions, isAlertmanagerAction) as AlertmanagerAction[];
const alertSourceActions = filter(actions, isAlertSourceAction) as AlertSourceAction[];
const alertSourceActions = filter(actions, isAlertingAction) as AlertingAction[];
if (alertmanagerActions.length) {
return <AuthorizeAlertmanager actions={alertmanagerActions}>{children}</AuthorizeAlertmanager>;
@ -44,8 +44,8 @@ const AuthorizeAlertmanager = ({ actions, children }: ActionsProps<AlertmanagerA
}
};
const AuthorizeAlertsource = ({ actions, children }: ActionsProps<AlertSourceAction>) => {
const alertSourceAbilities = useAlertSourceAbilities();
const AuthorizeAlertsource = ({ actions, children }: ActionsProps<AlertingAction>) => {
const alertSourceAbilities = useAlertingAbilities();
const allowed = actionsAllowed(alertSourceAbilities, actions);
if (allowed) {
@ -55,6 +55,8 @@ const AuthorizeAlertsource = ({ actions, children }: ActionsProps<AlertSourceAct
}
};
// TODO add some authorize helper components for alert source and individual alert rules
// check if some action is allowed from the abilities
function actionsAllowed<T extends Action>(abilities: Abilities<T>, actions: T[]) {
return chain(abilities)
@ -68,6 +70,6 @@ function isAlertmanagerAction(action: AlertmanagerAction) {
return Object.values(AlertmanagerAction).includes(action);
}
function isAlertSourceAction(action: AlertSourceAction) {
return Object.values(AlertSourceAction).includes(action);
function isAlertingAction(action: AlertingAction) {
return Object.values(AlertingAction).includes(action);
}

View File

@ -15,7 +15,7 @@ import { CombinedRule } from 'app/types/unified-alerting';
import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto';
import { discoverFeatures } from '../../api/buildInfo';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
import { mockAlertRuleApi, setupMswServer } from '../../mockApi';
import {
getCloudRule,
@ -53,12 +53,12 @@ jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getPluginLinkExtensions: jest.fn(),
}));
jest.mock('../../hooks/useIsRuleEditable');
jest.mock('../../hooks/useAbilities');
jest.mock('../../api/buildInfo');
const mocks = {
getPluginLinkExtensionsMock: jest.mocked(getPluginLinkExtensions),
useIsRuleEditable: jest.mocked(useIsRuleEditable),
useAlertRuleAbility: jest.mocked(useAlertRuleAbility),
};
const ui = {
@ -86,6 +86,7 @@ const renderRuleViewer = async (ruleId: string) => {
};
const server = setupMswServer();
const user = userEvent.setup();
const dsName = 'prometheus';
const rulerRule = mockRulerAlertingRule({ alert: 'cloud test alert' });
@ -173,7 +174,7 @@ describe('RuleViewer', () => {
});
it('should render page with grafana alert', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
mocks.useAlertRuleAbility.mockReturnValue([true, true]);
await renderRuleViewer('test1');
expect(screen.getByText(/test alert/i)).toBeInTheDocument();
@ -186,7 +187,7 @@ describe('RuleViewer', () => {
.mocked(discoverFeatures)
.mockResolvedValue({ application: PromApplication.Mimir, features: { rulerApiEnabled: true } });
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
mocks.useAlertRuleAbility.mockReturnValue([true, true]);
await renderRuleViewer(ruleId.stringifyIdentifier(rulerRuleIdentifier));
expect(screen.getByText(/cloud test alert/i)).toBeInTheDocument();
@ -206,7 +207,9 @@ describe('RuleDetails RBAC', () => {
});
it('Should render Edit button for users with the update permission', async () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
mockCombinedRule.mockReturnValue({
result: mockGrafanaRule as CombinedRule,
loading: false,
@ -231,9 +234,9 @@ describe('RuleDetails RBAC', () => {
requestId: 'A',
error: undefined,
});
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
const user = userEvent.setup();
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
// Act
await renderRuleViewer('test1');
@ -265,6 +268,7 @@ describe('RuleDetails RBAC', () => {
it('Should render Silence button for users with the instance create permissions', async () => {
// Arrange
mocks.useAlertRuleAbility.mockReturnValue([true, true]);
mockCombinedRule.mockReturnValue({
result: mockGrafanaRule as CombinedRule,
loading: false,
@ -281,12 +285,14 @@ describe('RuleDetails RBAC', () => {
// Assert
await waitFor(() => {
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
expect(ui.actionButtons.silence.get()).toBeInTheDocument();
});
});
it('Should render clone button for users having create rule permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Duplicate ? [true, true] : [false, false];
});
mockCombinedRule.mockReturnValue({
result: getGrafanaRule({ name: 'Grafana rule' }),
loading: false,
@ -294,8 +300,6 @@ describe('RuleDetails RBAC', () => {
});
grantUserPermissions([AccessControlAction.AlertingRuleCreate]);
const user = userEvent.setup();
await renderRuleViewer('test1');
await user.click(ui.moreButton.get());
@ -303,7 +307,9 @@ describe('RuleDetails RBAC', () => {
});
it('Should NOT render clone button for users without create rule permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Duplicate ? [true, false] : [true, true];
});
mockCombinedRule.mockReturnValue({
result: getGrafanaRule({ name: 'Grafana rule' }),
loading: false,
@ -312,7 +318,6 @@ describe('RuleDetails RBAC', () => {
const { AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete } = AccessControlAction;
grantUserPermissions([AlertingRuleRead, AlertingRuleUpdate, AlertingRuleDelete]);
const user = userEvent.setup();
await renderRuleViewer('test1');
await user.click(ui.moreButton.get());
@ -320,19 +325,19 @@ describe('RuleDetails RBAC', () => {
expect(ui.moreButtons.duplicate.query()).not.toBeInTheDocument();
});
});
describe('Cloud rules action buttons', () => {
let mockCombinedRule = jest.fn();
beforeEach(() => {
// mockCombinedRule = jest.mocked(useCombinedRule);
});
describe('Cloud rules action buttons', () => {
const mockCombinedRule = jest.fn();
afterEach(() => {
mockCombinedRule.mockReset();
});
it('Should render edit button for users with the update permission', async () => {
// Arrange
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
mockCombinedRule.mockReturnValue({
result: mockCloudRule as CombinedRule,
loading: false,
@ -350,6 +355,9 @@ describe('RuleDetails RBAC', () => {
it('Should render Delete button for users with the delete permission', async () => {
// Arrange
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
mockCombinedRule.mockReturnValue({
result: mockCloudRule as CombinedRule,
loading: false,
@ -357,9 +365,6 @@ describe('RuleDetails RBAC', () => {
requestId: 'A',
error: undefined,
});
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
const user = userEvent.setup();
// Act
await renderRuleViewer('test1');

View File

@ -20,12 +20,12 @@ import { useAppNotification } from 'app/core/copy/appNotification';
import { useDispatch } from 'app/types';
import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
import { deleteRuleAction } from '../../state/actions';
import { getRulesSourceName } from '../../utils/datasource';
import { createShareLink, createViewLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id';
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../../utils/rules';
import { isGrafanaRulerRule } from '../../utils/rules';
import { createUrl } from '../../utils/url';
import { RedirectToCloneRule } from './CloneRule';
@ -50,18 +50,24 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
const { namespace, group, rulerRule } = rule;
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
const rulesSourceName = getRulesSourceName(rulesSource);
const returnTo = location.pathname + location.search;
const isViewMode = inViewMode(location.pathname);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const [editRuleSupported, editRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
const [deleteRuleSupported, deleteRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
const [duplicateRuleSupported, duplicateRuleAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
const [modifyExportSupported, modifyExportAllowed] = useAlertRuleAbility(rule, AlertRuleAction.ModifyExport);
const canEditRule = editRuleSupported && editRuleAllowed;
const canDeleteRule = deleteRuleSupported && deleteRuleAllowed;
const canDuplicateRule = duplicateRuleSupported && duplicateRuleAllowed;
const canModifyExport = modifyExportSupported && modifyExportAllowed;
const buttons: JSX.Element[] = [];
const moreActions: JSX.Element[] = [];
const isFederated = isFederatedRuleGroup(group);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const returnTo = location.pathname + location.search;
const isViewMode = inViewMode(location.pathname);
const deleteRule = () => {
if (ruleToDelete && ruleToDelete.rulerRule) {
const identifier = ruleId.fromRulerRule(
@ -96,53 +102,53 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
);
}
if (rulerRule && !isFederated) {
if (rulerRule) {
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
if (isEditable) {
if (!isProvisioned) {
const editURL = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, {
returnTo,
});
if (canEditRule) {
const editURL = createUrl(`/alerting/${encodeURIComponent(ruleId.stringifyIdentifier(identifier))}/edit`, {
returnTo,
});
if (isViewMode) {
buttons.push(
<ClipboardButton
key="copy"
icon="copy"
onClipboardError={(copiedText) => {
notifyApp.error('Error while copying URL', copiedText);
}}
className={style.button}
size="sm"
getText={buildShareUrl}
>
Copy link to rule
</ClipboardButton>
);
}
buttons.push(
<Tooltip placement="top" content={'Edit'}>
<LinkButton
title="Edit"
className={style.button}
size="sm"
key="edit"
variant="secondary"
icon="pen"
href={editURL}
/>
</Tooltip>
);
}
buttons.push(
<Tooltip placement="top" content={'Edit'}>
<LinkButton
title="Edit"
className={style.button}
size="sm"
key="edit"
variant="secondary"
icon="pen"
href={editURL}
/>
</Tooltip>
);
}
if (isViewMode) {
buttons.push(
<ClipboardButton
key="copy"
icon="copy"
onClipboardError={(copiedText) => {
notifyApp.error('Error while copying URL', copiedText);
}}
className={style.button}
size="sm"
getText={buildShareUrl}
>
Copy link to rule
</ClipboardButton>
);
}
if (canDuplicateRule) {
moreActions.push(
<Menu.Item label="Duplicate" icon="copy" onClick={() => setRedirectToClone({ identifier, isProvisioned })} />
);
}
if (isGrafanaRulerRule(rulerRule)) {
if (canModifyExport) {
moreActions.push(
<Menu.Item
label="Modify export"
@ -153,10 +159,10 @@ export const RuleActionsButtons = ({ rule, rulesSource }: Props) => {
/>
);
}
}
if (isRemovable && rulerRule && !isFederated && !isProvisioned) {
moreActions.push(<Menu.Item label="Delete" icon="trash-alt" onClick={() => setRuleToDelete(rule)} />);
if (canDeleteRule) {
moreActions.push(<Menu.Item label="Delete" icon="trash-alt" onClick={() => setRuleToDelete(rule)} />);
}
}
if (buttons.length || moreActions.length) {

View File

@ -17,17 +17,13 @@ import {
useStyles2,
} from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction, useDispatch } from 'app/types';
import { useDispatch } from 'app/types';
import { CombinedRule, RuleIdentifier, RulesSource } from 'app/types/unified-alerting';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { alertmanagerApi } from '../../api/alertmanagerApi';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
import { deleteRuleAction } from '../../state/actions';
import { getRulesPermissions } from '../../utils/access-control';
import { getAlertmanagerByUid } from '../../utils/alertmanager';
import { Annotation } from '../../utils/constants';
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
@ -68,7 +64,11 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
? rulesSource
: getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name;
const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
const [duplicateSupported, duplicateAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Duplicate);
const [silenceSupported, silenceAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Silence);
const [exploreSupported, exploreAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Explore);
const [deleteSupported, deleteAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Delete);
const [editSupported, editAllowed] = useAlertRuleAbility(rule, AlertRuleAction.Update);
const buttons: JSX.Element[] = [];
const rightButtons: JSX.Element[] = [];
@ -89,22 +89,21 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
};
const isFederated = isFederatedRuleGroup(group);
const rulesSourceName = getRulesSourceName(rulesSource);
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const isFiringRule = isAlertingRule(rule.promRule) && rule.promRule.state === PromAlertingRuleState.Firing;
const rulesPermissions = getRulesPermissions(rulesSourceName);
const hasCreateRulePermission = contextSrv.hasPermission(rulesPermissions.create);
const { isEditable, isRemovable } = useIsRuleEditable(rulesSourceName, rulerRule);
const canSilence = useCanSilence(rule);
const canDelete = deleteSupported && deleteAllowed;
const canEdit = editSupported && editAllowed;
const canSilence = silenceSupported && silenceAllowed && alertmanagerSourceName;
const canDuplicateRule = duplicateSupported && duplicateAllowed && !isFederated;
const buildShareUrl = () => createShareLink(rulesSource, rule);
const returnTo = location.pathname + location.search;
// explore does not support grafana rule queries atm
// neither do "federated rules"
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
if (isCloudRulesSource(rulesSource) && exploreSupported && exploreAllowed && !isFederated) {
buttons.push(
<LinkButton
size="sm"
@ -165,7 +164,7 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
}
}
if (canSilence && alertmanagerSourceName) {
if (canSilence) {
buttons.push(
<LinkButton
size="sm"
@ -206,7 +205,7 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
const sourceName = getRulesSourceName(rulesSource);
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
if (isEditable && !isFederated) {
if (canEdit) {
rightButtons.push(
<ClipboardButton
key="copy"
@ -245,13 +244,13 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
moreActionsButtons.push(<Menu.Item label="Modify export" icon="edit" url={modifyUrl} />);
}
if (hasCreateRulePermission && !isFederated) {
if (canDuplicateRule) {
moreActionsButtons.push(
<Menu.Item label="Duplicate" icon="copy" onClick={() => setRedirectToClone({ identifier, isProvisioned })} />
);
}
if (isRemovable && !isFederated && !isProvisioned) {
if (canDelete) {
moreActionsButtons.push(<Menu.Divider />);
moreActionsButtons.push(
<Menu.Item key="delete" label="Delete" icon="trash-alt" onClick={() => setRuleToDelete(rule)} />
@ -317,31 +316,6 @@ function shouldShowDeclareIncidentButton() {
return !isOpenSourceEdition() || isLocalDevEnv();
}
/**
* We don't want to show the silence button if either
* 1. the user has no permissions to create silences
* 2. the admin has configured to only send instances to external AMs
*/
function useCanSilence(rule: CombinedRule) {
const isGrafanaManagedRule = isGrafanaRulerRule(rule.rulerRule);
const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi;
const { currentData: amConfigStatus, isLoading } = useGetAlertmanagerChoiceStatusQuery(undefined, {
skip: !isGrafanaManagedRule,
});
if (!isGrafanaManagedRule || isLoading) {
return false;
}
const hasPermissions = contextSrv.hasPermission(AccessControlAction.AlertingInstanceCreate);
const interactsOnlyWithExternalAMs = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External;
const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All;
return hasPermissions && (!interactsOnlyWithExternalAMs || interactsWithAll);
}
export const getStyles = (theme: GrafanaTheme2) => ({
wrapper: css`
padding: ${theme.spacing(2)} 0;

View File

@ -3,7 +3,7 @@ import React, { useEffect, useMemo } from 'react';
import { CombinedRuleNamespace } from 'app/types/unified-alerting';
import { LogMessages, logInfo } from '../../Analytics';
import { AlertSourceAction } from '../../hooks/useAbilities';
import { AlertingAction } from '../../hooks/useAbilities';
import { isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { Authorize } from '../Authorize';
@ -35,10 +35,10 @@ export const RuleListGroupView = ({ namespaces, expandAll }: Props) => {
return (
<>
<Authorize actions={[AlertSourceAction.ViewAlertRule]}>
<Authorize actions={[AlertingAction.ViewAlertRule]}>
<GrafanaRules namespaces={grafanaNamespaces} expandAll={expandAll} />
</Authorize>
<Authorize actions={[AlertSourceAction.ViewExternalAlertRule]}>
<Authorize actions={[AlertingAction.ViewExternalAlertRule]}>
<CloudRules namespaces={cloudNamespaces} expandAll={expandAll} />
</Authorize>
</>

View File

@ -8,15 +8,15 @@ import { byRole } from 'testing-library-selector';
import { configureStore } from 'app/store/configureStore';
import { CombinedRule } from 'app/types/unified-alerting';
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
import { AlertRuleAction, useAlertRuleAbility } from '../../hooks/useAbilities';
import { getCloudRule, getGrafanaRule } from '../../mocks';
import { RulesTable } from './RulesTable';
jest.mock('../../hooks/useIsRuleEditable');
jest.mock('../../hooks/useAbilities');
const mocks = {
useIsRuleEditable: jest.mocked(useIsRuleEditable),
useAlertRuleAbility: jest.mocked(useAlertRuleAbility),
};
const ui = {
@ -42,18 +42,25 @@ function renderRulesTable(rule: CombinedRule) {
);
}
const user = userEvent.setup();
describe('RulesTable RBAC', () => {
describe('Grafana rules action buttons', () => {
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
it('Should not render Edit button for users without the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
it('Should not render Edit button for users without the update permission', async () => {
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
renderRulesTable(grafanaRule);
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users without the delete permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
const user = userEvent.setup();
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
});
renderRulesTable(grafanaRule);
await user.click(ui.actionButtons.more.get());
@ -62,14 +69,17 @@ describe('RulesTable RBAC', () => {
});
it('Should render Edit button for users with the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
renderRulesTable(grafanaRule);
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
const user = userEvent.setup();
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
renderRulesTable(grafanaRule);
@ -81,28 +91,39 @@ describe('RulesTable RBAC', () => {
describe('Cloud rules action buttons', () => {
const cloudRule = getCloudRule({ name: 'Cloud' });
it('Should not render Edit button for users without the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: false });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, false] : [true, true];
});
renderRulesTable(cloudRule);
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
});
it('Should not render Delete button for users without the delete permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: false });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, false] : [true, true];
});
renderRulesTable(cloudRule);
expect(ui.actionButtons.more.query()).not.toBeInTheDocument();
await user.click(ui.actionButtons.more.get());
expect(ui.moreActionItems.delete.query()).not.toBeInTheDocument();
});
it('Should render Edit button for users with the update permission', () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Update ? [true, true] : [false, false];
});
renderRulesTable(cloudRule);
expect(ui.actionButtons.edit.get()).toBeInTheDocument();
});
it('Should render Delete button for users with the delete permission', async () => {
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isRemovable: true });
const user = userEvent.setup();
mocks.useAlertRuleAbility.mockImplementation((_rule, action) => {
return action === AlertRuleAction.Delete ? [true, true] : [false, false];
});
renderRulesTable(cloudRule);
await user.click(ui.actionButtons.more.get());

View File

@ -1,12 +1,13 @@
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { createBrowserHistory } from 'history';
import React, { PropsWithChildren } from 'react';
import { Router } from 'react-router-dom';
import { TestProvider } from 'test/helpers/TestProvider';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { grantUserPermissions, mockDataSource } from '../mocks';
import { getGrafanaRule, grantUserPermissions, mockDataSource } from '../mocks';
import { AlertmanagerProvider } from '../state/AlertmanagerContext';
import { setupDataSources } from '../testSetup/datasources';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
@ -15,6 +16,7 @@ import {
AlertmanagerAction,
useAlertmanagerAbilities,
useAlertmanagerAbility,
useAllAlertRuleAbilities,
useAllAlertmanagerAbilities,
} from './useAbilities';
@ -32,7 +34,9 @@ describe('alertmanager abilities', () => {
})
);
const abilities = renderHook(() => useAllAlertmanagerAbilities(), { wrapper: createWrapper('does-not-exist') });
const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
wrapper: createAlertmanagerWrapper('does-not-exist'),
});
expect(abilities.result.current).toMatchSnapshot();
});
@ -47,7 +51,7 @@ describe('alertmanager abilities', () => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingInstanceRead]);
const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
});
Object.values(abilities.result.current).forEach(([supported]) => {
@ -56,7 +60,7 @@ describe('alertmanager abilities', () => {
// since we only granted "read" permissions, only those should be allowed
const viewAbility = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
});
const [viewSupported, viewAllowed] = viewAbility.result.current;
@ -66,7 +70,7 @@ describe('alertmanager abilities', () => {
// editing should not be allowed, but supported
const editAbility = renderHook(() => useAlertmanagerAbility(AlertmanagerAction.ViewSilence), {
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
});
const [editSupported, editAllowed] = editAbility.result.current;
@ -97,7 +101,7 @@ describe('alertmanager abilities', () => {
]);
const abilities = renderHook(() => useAllAlertmanagerAbilities(), {
wrapper: createWrapper('mimir'),
wrapper: createAlertmanagerWrapper('mimir'),
});
expect(abilities.result.current).toMatchSnapshot();
@ -121,7 +125,7 @@ describe('alertmanager abilities', () => {
AlertmanagerAction.ExportContactPoint,
]),
{
wrapper: createWrapper(GRAFANA_RULES_SOURCE_NAME),
wrapper: createAlertmanagerWrapper(GRAFANA_RULES_SOURCE_NAME),
}
);
@ -132,7 +136,29 @@ describe('alertmanager abilities', () => {
});
});
function createWrapper(alertmanagerSourceName: string) {
describe('rule permissions', () => {
it('should report that all actions are supported for a Grafana Managed alert rule', async () => {
const rule = getGrafanaRule();
const abilities = renderHook(() => useAllAlertRuleAbilities(rule), { wrapper: TestProvider });
await waitFor(() => {
const results = Object.values(abilities.result.current);
for (const [supported, _allowed] of results) {
expect(supported).toBe(true);
}
});
});
it('should report the correct set of supported actions for an external rule with ruler API', async () => {});
it('should not allow certain actions for provisioned rules', () => {});
it('should not allow certain actions for federated rules', () => {});
});
function createAlertmanagerWrapper(alertmanagerSourceName: string) {
const wrapper = (props: PropsWithChildren) => (
<Router history={createBrowserHistory()}>
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertmanagerSourceName}>

View File

@ -1,16 +1,25 @@
import { useMemo } from 'react';
import { contextSrv as ctx } from 'app/core/services/context_srv';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
import { alertmanagerApi } from '../api/alertmanagerApi';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { getInstancesPermissions, getNotificationsPermissions } from '../utils/access-control';
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
import { isFederatedRuleGroup, isGrafanaRulerRule } from '../utils/rules';
import { useIsRuleEditable } from './useIsRuleEditable';
/**
* These hooks will determine if
* 1. the action is supported in the current alertmanager or data source context
* 1. the action is supported in the current context (alertmanager, alert rule or general context)
* 2. user is allowed to perform actions based on their set of permissions / assigned role
*/
// this enum lists all of the available actions we can perform within the context of an alertmanager
export enum AlertmanagerAction {
// configuration
ViewExternalConfiguration = 'view-external-configuration',
@ -49,12 +58,27 @@ export enum AlertmanagerAction {
DeleteMuteTiming = 'delete-mute-timing',
}
export enum AlertSourceAction {
// this enum lists all of the available actions we can take on a single alert rule
export enum AlertRuleAction {
Duplicate = 'duplicate-alert-rule',
View = 'view-alert-rule',
Update = 'update-alert-rule',
Delete = 'delete-alert-rule',
Explore = 'explore-alert-rule',
Silence = 'silence-alert-rule',
ModifyExport = 'modify-export-rule',
}
// this enum lists all of the actions we can perform within alerting in general, not linked to a specific
// alert source, rule or alertmanager
export enum AlertingAction {
// internal (Grafana managed)
CreateAlertRule = 'create-alert-rule',
ViewAlertRule = 'view-alert-rule',
UpdateAlertRule = 'update-alert-rule',
DeleteAlertRule = 'delete-alert-rule',
ExportGrafanaManagedRules = 'export-grafana-managed-rules',
// external (any compatible alerting data source)
CreateExternalAlertRule = 'create-external-alert-rule',
ViewExternalAlertRule = 'view-external-alert-rule',
@ -62,39 +86,92 @@ export enum AlertSourceAction {
DeleteExternalAlertRule = 'delete-external-alert-rule',
}
const AlwaysSupported = true; // this just makes it easier to understand the code
export type Action = AlertmanagerAction | AlertSourceAction;
// these just makes it easier to read the code :)
const AlwaysSupported = true;
const NotSupported = false;
export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction;
export type Ability = [actionSupported: boolean, actionAllowed: boolean];
export type Abilities<T extends Action> = Record<T, Ability>;
export function useAlertSourceAbilities(): Abilities<AlertSourceAction> {
// TODO add the "supported" booleans here, we currently only do authorization
/**
* This one will check for alerting abilities that don't apply to any particular alert source or alert rule
*/
export const useAlertingAbilities = (): Abilities<AlertingAction> => {
return {
// internal (Grafana managed)
[AlertingAction.CreateAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleCreate),
[AlertingAction.ViewAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleRead),
[AlertingAction.UpdateAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleUpdate),
[AlertingAction.DeleteAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleDelete),
[AlertingAction.ExportGrafanaManagedRules]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleRead),
const abilities: Abilities<AlertSourceAction> = {
// -- Grafana managed alert rules --
[AlertSourceAction.CreateAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleCreate)],
[AlertSourceAction.ViewAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleRead)],
[AlertSourceAction.UpdateAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleUpdate)],
[AlertSourceAction.DeleteAlertRule]: [AlwaysSupported, ctx.hasPermission(AccessControlAction.AlertingRuleDelete)],
// -- External alert rules (Mimir / Loki / etc) --
// for these we only have "read" and "write" permissions
[AlertSourceAction.CreateExternalAlertRule]: [
AlwaysSupported,
ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
],
[AlertSourceAction.ViewExternalAlertRule]: [
AlwaysSupported,
ctx.hasPermission(AccessControlAction.AlertingRuleExternalRead),
],
[AlertSourceAction.UpdateExternalAlertRule]: [
AlwaysSupported,
ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
],
[AlertSourceAction.DeleteExternalAlertRule]: [
AlwaysSupported,
ctx.hasPermission(AccessControlAction.AlertingRuleExternalWrite),
],
// external
[AlertingAction.CreateExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite),
[AlertingAction.ViewExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalRead),
[AlertingAction.UpdateExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite),
[AlertingAction.DeleteExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite),
};
};
export const useAlertingAbility = (action: AlertingAction): Ability => {
const allAbilities = useAlertingAbilities();
return allAbilities[action];
};
/**
* This hook will check if we support the action and have sufficient permissions for it on a single alert rule
*/
export function useAlertRuleAbility(rule: CombinedRule, action: AlertRuleAction): Ability {
const abilities = useAllAlertRuleAbilities(rule);
return useMemo(() => {
return abilities[action];
}, [abilities, action]);
}
export function useAlertRuleAbilities(rule: CombinedRule, actions: AlertRuleAction[]): Ability[] {
const abilities = useAllAlertRuleAbilities(rule);
return useMemo(() => {
return actions.map((action) => abilities[action]);
}, [abilities, actions]);
}
export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRuleAction> {
const rulesSource = rule.namespace.rulesSource;
const rulesSourceName = typeof rulesSource === 'string' ? rulesSource : rulesSource.name;
const isProvisioned = isGrafanaRulerRule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
const isFederated = isFederatedRuleGroup(rule.group);
// if a rule is either provisioned or a federated rule, we don't allow it to be removed or edited
const immutableRule = isProvisioned || isFederated;
// TODO refactor this hook maybe
const {
isEditable,
isRemovable,
isRulerAvailable = false,
loading,
} = useIsRuleEditable(rulesSourceName, rule.rulerRule);
const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
// while we gather info, pretend it's not supported
const MaybeSupported = loading ? NotSupported : isRulerAvailable;
const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;
const rulesPermissions = getRulesPermissions(rulesSourceName);
const canSilence = useCanSilence(rulesSource);
const abilities: Abilities<AlertRuleAction> = {
[AlertRuleAction.Duplicate]: toAbility(MaybeSupported, rulesPermissions.create),
[AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read),
[AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
[AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
[AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
[AlertRuleAction.Silence]: canSilence,
[AlertRuleAction.ModifyExport]: [MaybeSupported, exportAllowed],
};
return abilities;
@ -115,72 +192,48 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
// list out all of the abilities, and if the user has permissions to perform them
const abilities: Abilities<AlertmanagerAction> = {
// -- configuration --
[AlertmanagerAction.ViewExternalConfiguration]: [
[AlertmanagerAction.ViewExternalConfiguration]: toAbility(
AlwaysSupported,
ctx.hasPermission(AccessControlAction.AlertingNotificationsExternalRead),
],
[AlertmanagerAction.UpdateExternalConfiguration]: [
AccessControlAction.AlertingNotificationsExternalRead
),
[AlertmanagerAction.UpdateExternalConfiguration]: toAbility(
hasConfigurationAPI,
ctx.hasPermission(AccessControlAction.AlertingNotificationsExternalWrite),
],
AccessControlAction.AlertingNotificationsExternalWrite
),
// -- contact points --
[AlertmanagerAction.CreateContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.create)],
[AlertmanagerAction.ViewContactPoint]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
[AlertmanagerAction.UpdateContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.update)],
[AlertmanagerAction.DeleteContactPoint]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.delete)],
[AlertmanagerAction.CreateContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
[AlertmanagerAction.ViewContactPoint]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.DeleteContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
// only Grafana flavored alertmanager supports exporting
[AlertmanagerAction.ExportContactPoint]: [
isGrafanaFlavoredAlertmanager,
ctx.hasPermission(notificationsPermissions.read),
],
[AlertmanagerAction.ExportContactPoint]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
// -- notification templates --
[AlertmanagerAction.CreateNotificationTemplate]: [
hasConfigurationAPI,
ctx.hasPermission(notificationsPermissions.create),
],
[AlertmanagerAction.ViewNotificationTemplate]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
[AlertmanagerAction.UpdateNotificationTemplate]: [
hasConfigurationAPI,
ctx.hasPermission(notificationsPermissions.update),
],
[AlertmanagerAction.DeleteNotificationTemplate]: [
hasConfigurationAPI,
ctx.hasPermission(notificationsPermissions.delete),
],
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
// -- notification policies --
[AlertmanagerAction.CreateNotificationPolicy]: [
hasConfigurationAPI,
ctx.hasPermission(notificationsPermissions.create),
],
[AlertmanagerAction.ViewNotificationPolicyTree]: [
AlwaysSupported,
ctx.hasPermission(notificationsPermissions.read),
],
[AlertmanagerAction.UpdateNotificationPolicyTree]: [
hasConfigurationAPI,
ctx.hasPermission(notificationsPermissions.update),
],
[AlertmanagerAction.DeleteNotificationPolicy]: [
hasConfigurationAPI,
ctx.hasPermission(notificationsPermissions.delete),
],
[AlertmanagerAction.ExportNotificationPolicies]: [
[AlertmanagerAction.CreateNotificationPolicy]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
[AlertmanagerAction.ViewNotificationPolicyTree]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateNotificationPolicyTree]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.DeleteNotificationPolicy]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
[AlertmanagerAction.ExportNotificationPolicies]: toAbility(
isGrafanaFlavoredAlertmanager,
ctx.hasPermission(notificationsPermissions.read),
],
[AlertmanagerAction.DecryptSecrets]: [
notificationsPermissions.read
),
[AlertmanagerAction.DecryptSecrets]: toAbility(
isGrafanaFlavoredAlertmanager,
ctx.hasPermission(notificationsPermissions.provisioning.readSecrets),
],
notificationsPermissions.provisioning.readSecrets
),
// -- silences --
[AlertmanagerAction.CreateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.create)],
[AlertmanagerAction.ViewSilence]: [AlwaysSupported, ctx.hasPermission(instancePermissions.read)],
[AlertmanagerAction.UpdateSilence]: [hasConfigurationAPI, ctx.hasPermission(instancePermissions.update)],
[AlertmanagerAction.CreateSilence]: toAbility(hasConfigurationAPI, instancePermissions.create),
[AlertmanagerAction.ViewSilence]: toAbility(AlwaysSupported, instancePermissions.read),
[AlertmanagerAction.UpdateSilence]: toAbility(hasConfigurationAPI, instancePermissions.update),
// -- mute timtings --
[AlertmanagerAction.CreateMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.create)],
[AlertmanagerAction.ViewMuteTiming]: [AlwaysSupported, ctx.hasPermission(notificationsPermissions.read)],
[AlertmanagerAction.UpdateMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.update)],
[AlertmanagerAction.DeleteMuteTiming]: [hasConfigurationAPI, ctx.hasPermission(notificationsPermissions.delete)],
[AlertmanagerAction.CreateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
[AlertmanagerAction.ViewMuteTiming]: toAbility(AlwaysSupported, notificationsPermissions.read),
[AlertmanagerAction.UpdateMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
[AlertmanagerAction.DeleteMuteTiming]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
};
return abilities;
@ -202,7 +255,31 @@ export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability
}, [abilities, actions]);
}
export function useAlertSourceAbility(action: AlertSourceAction): Ability {
const abilities = useAlertSourceAbilities();
return useMemo(() => abilities[action], [abilities, action]);
/**
* We don't want to show the silence button if either
* 1. the user has no permissions to create silences
* 2. the admin has configured to only send instances to external AMs
*/
function useCanSilence(rulesSource: RulesSource): [boolean, boolean] {
const isGrafanaManagedRule = rulesSource === GRAFANA_RULES_SOURCE_NAME;
const { useGetAlertmanagerChoiceStatusQuery } = alertmanagerApi;
const { currentData: amConfigStatus, isLoading } = useGetAlertmanagerChoiceStatusQuery(undefined, {
skip: !isGrafanaManagedRule,
});
// we don't support silencing when the rule is not a Grafana managed rule
// we simply don't know what Alertmanager the ruler is sending alerts to
if (!isGrafanaManagedRule || isLoading) {
return [false, false];
}
const interactsOnlyWithExternalAMs = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External;
const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All;
const silenceSupported = !interactsOnlyWithExternalAMs || interactsWithAll;
return toAbility(silenceSupported, AccessControlAction.AlertingInstanceCreate);
}
// just a convenient function
const toAbility = (supported: boolean, action: AccessControlAction): Ability => [supported, ctx.hasPermission(action)];

View File

@ -9,6 +9,7 @@ import { useFolder } from './useFolder';
import { useUnifiedAlertingSelector } from './useUnifiedAlertingSelector';
interface ResultBag {
isRulerAvailable?: boolean;
isEditable?: boolean;
isRemovable?: boolean;
loading: boolean;
@ -42,6 +43,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
if (!folder) {
// Loading or invalid folder UID
return {
isRulerAvailable: true,
isEditable: false,
isRemovable: false,
loading,
@ -52,6 +54,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
const canRemoveGrafanaRules = contextSrv.hasPermissionInMetadata(rulePermission.delete, folder);
return {
isRulerAvailable: true,
isEditable: canEditGrafanaRules,
isRemovable: canRemoveGrafanaRules,
loading: loading || isLoading,
@ -65,6 +68,7 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
const canRemoveCloudRules = contextSrv.hasPermission(rulePermission.delete);
return {
isRulerAvailable,
isEditable: canEditCloudRules && isRulerAvailable,
isRemovable: canRemoveCloudRules && isRulerAvailable,
loading: isLoading || dataSources[rulesSourceName]?.loading,

View File

@ -211,7 +211,7 @@ export const mockGrafanaRulerRule = (partial: Partial<GrafanaRuleDefinition> = {
grafana_alert: {
uid: '',
title: 'my rule',
namespace_uid: '',
namespace_uid: 'NAMESPACE_UID',
namespace_id: 0,
condition: '',
no_data_state: GrafanaAlertStateDecision.NoData,