mirror of
https://github.com/grafana/grafana.git
synced 2024-11-23 01:16:31 -06:00
Alerting: useAbility hook for alert rules (#78231)
This commit is contained in:
parent
add096ac8c
commit
7dbbdc16a3
@ -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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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({}));
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
</>
|
||||
|
@ -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());
|
||||
|
@ -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}>
|
||||
|
@ -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)];
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user