mirror of
https://github.com/grafana/grafana.git
synced 2024-12-02 05:29:42 -06:00
Alerting: FGAC for alert rules page (#47418)
* Apply FGAC on the alert rules list page * Add tests for edit, delete and silence buttons * Unify access-control helpers * Fix import * Add route permissions for alert groups, unify access control helpers * Improve buttons with data source explore permission * Fix test
This commit is contained in:
parent
f1a1070d41
commit
87383b1c8b
@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
|
||||
import { uniq } from 'lodash';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { OrgRole } from '@grafana/data';
|
||||
import { evaluateAccess } from './unified/utils/access-control';
|
||||
|
||||
const commonRoutes: RouteDescriptor[] = [
|
||||
@ -90,6 +91,10 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
...commonRoutes,
|
||||
{
|
||||
path: '/alerting/list',
|
||||
roles: evaluateAccess(
|
||||
[AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead],
|
||||
[OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
|
||||
),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList')
|
||||
),
|
||||
@ -214,6 +219,10 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
},
|
||||
{
|
||||
path: '/alerting/groups/',
|
||||
roles: evaluateAccess(
|
||||
[AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead],
|
||||
[OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
|
||||
),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
|
||||
),
|
||||
@ -221,6 +230,10 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
{
|
||||
path: '/alerting/new',
|
||||
pageClass: 'page-alerting',
|
||||
roles: evaluateAccess(
|
||||
[AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite],
|
||||
[OrgRole.Editor, OrgRole.Admin]
|
||||
),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
|
||||
),
|
||||
@ -228,6 +241,10 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
{
|
||||
path: '/alerting/:id/edit',
|
||||
pageClass: 'page-alerting',
|
||||
roles: evaluateAccess(
|
||||
[AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite],
|
||||
[OrgRole.Editor, OrgRole.Admin]
|
||||
),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
|
||||
),
|
||||
@ -235,6 +252,10 @@ const unifiedRoutes: RouteDescriptor[] = [
|
||||
{
|
||||
path: '/alerting/:sourceName/:id/view',
|
||||
pageClass: 'page-alerting',
|
||||
roles: evaluateAccess(
|
||||
[AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead],
|
||||
[OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
|
||||
),
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer')
|
||||
),
|
||||
|
@ -16,6 +16,7 @@ jest.mock('app/core/services/context_srv', () => ({
|
||||
contextSrv: {
|
||||
isEditor: true,
|
||||
hasAccess: () => true,
|
||||
hasPermission: () => true,
|
||||
},
|
||||
}));
|
||||
const mocks = {
|
||||
|
@ -3,11 +3,13 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import React, { FC } from 'react';
|
||||
import { getInstancesPermissions } from '../../utils/access-control';
|
||||
import { isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
|
||||
import { AnnotationDetailsField } from '../AnnotationDetailsField';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { getInstancesPermissions } from '../../utils/access-control';
|
||||
|
||||
interface AmNotificationsAlertDetailsProps {
|
||||
alertManagerSourceName: string;
|
||||
@ -16,11 +18,19 @@ interface AmNotificationsAlertDetailsProps {
|
||||
|
||||
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||
const instancePermissions = getInstancesPermissions(alertManagerSourceName);
|
||||
|
||||
// For Grafana Managed alerts the Generator URL redirects to the alert rule edit page, so update permission is required
|
||||
// For external alert manager the Generator URL redirects to an external service which we don't control
|
||||
const isGrafanaSource = isGrafanaRulesSource(alertManagerSourceName);
|
||||
const isSeeSourceButtonEnabled = isGrafanaSource
|
||||
? contextSrv.hasPermission(AccessControlAction.AlertingRuleUpdate)
|
||||
: true;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.actionsRow}>
|
||||
<Authorize actions={[permissions.update, permissions.create]} fallback={contextSrv.isEditor}>
|
||||
<Authorize actions={[instancePermissions.update, instancePermissions.create]} fallback={contextSrv.isEditor}>
|
||||
{alert.status.state === AlertState.Suppressed && (
|
||||
<LinkButton
|
||||
href={`${makeAMLink(
|
||||
@ -45,13 +55,11 @@ export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, aler
|
||||
</LinkButton>
|
||||
)}
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.viewSource]}>
|
||||
{alert.generatorURL && (
|
||||
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
||||
See source
|
||||
</LinkButton>
|
||||
)}
|
||||
</Authorize>
|
||||
{isSeeSourceButtonEnabled && alert.generatorURL && (
|
||||
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
||||
See source
|
||||
</LinkButton>
|
||||
)}
|
||||
</div>
|
||||
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => (
|
||||
<AnnotationDetailsField key={annotationKey} annotationKey={annotationKey} value={annotationValue} />
|
||||
|
@ -9,6 +9,8 @@ import { AlertQuery } from 'app/types/unified-alerting-dto';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup';
|
||||
import { TABLE, TIMESERIES } from '../../utils/constants';
|
||||
import { Authorize } from '../Authorize';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
type RuleViewerVisualizationProps = {
|
||||
data?: PanelData;
|
||||
@ -91,20 +93,22 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS
|
||||
/>
|
||||
) : null}
|
||||
<PanelPluginsButtonGroup onChange={setPanel} value={panel} size="md" />
|
||||
{!isExpressionQuery(query.model) && (
|
||||
<>
|
||||
<div className={styles.spacing} />
|
||||
<LinkButton
|
||||
size="md"
|
||||
variant="secondary"
|
||||
icon="compass"
|
||||
target="_blank"
|
||||
href={createExploreLink(dsSettings, query)}
|
||||
>
|
||||
View in Explore
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
<Authorize actions={[AccessControlAction.DataSourcesExplore]}>
|
||||
{!isExpressionQuery(query.model) && (
|
||||
<>
|
||||
<div className={styles.spacing} />
|
||||
<LinkButton
|
||||
size="md"
|
||||
variant="secondary"
|
||||
icon="compass"
|
||||
target="_blank"
|
||||
href={createExploreLink(dsSettings, query)}
|
||||
>
|
||||
View in Explore
|
||||
</LinkButton>
|
||||
</>
|
||||
)}
|
||||
</Authorize>
|
||||
</div>
|
||||
</div>
|
||||
<PanelRenderer
|
||||
|
@ -0,0 +1,194 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { byRole } from 'testing-library-selector';
|
||||
import { mockCombinedRule, mockDataSource, mockPromAlertingRule, mockRulerAlertingRule } from '../../mocks';
|
||||
import { RuleDetails } from './RuleDetails';
|
||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||
|
||||
jest.mock('../../hooks/useIsRuleEditable');
|
||||
|
||||
const mocks = {
|
||||
useIsRuleEditable: jest.mocked(useIsRuleEditable),
|
||||
};
|
||||
|
||||
const ui = {
|
||||
actionButtons: {
|
||||
edit: byRole('link', { name: 'Edit' }),
|
||||
delete: byRole('button', { name: 'Delete' }),
|
||||
silence: byRole('link', { name: 'Silence' }),
|
||||
},
|
||||
};
|
||||
|
||||
jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true);
|
||||
|
||||
describe('RuleDetails FGAC', () => {
|
||||
mocks.useIsRuleEditable.mockReturnValue({ loading: false, isEditable: true });
|
||||
|
||||
describe('Grafana rules action buttons', () => {
|
||||
const grafanaRule = getGrafanaRule({ name: 'Grafana' });
|
||||
it('Should not render Edit button for users without the update permission', () => {
|
||||
// Arrange
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', () => {
|
||||
// Arrange
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', () => {
|
||||
// Arrange
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingRuleUpdate);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', () => {
|
||||
// Arrange
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingRuleDelete);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render Silence button for users wihout the instance create permission', () => {
|
||||
// Arrange
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.silence.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Silence button for users with the instance create permissions', () => {
|
||||
// Arrange
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingInstanceCreate);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(grafanaRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.silence.query()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cloud rules action buttons', () => {
|
||||
const cloudRule = getCloudRule({ name: 'Cloud' });
|
||||
it('Should not render Edit button for users without the update permission', () => {
|
||||
// Arrange
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should not render Delete button for users without the delete permission', () => {
|
||||
// Arrange
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Edit button for users with the update permission', () => {
|
||||
// Arrange
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.edit.query()).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Should render Delete button for users with the delete permission', () => {
|
||||
// Arrange
|
||||
jest
|
||||
.spyOn(contextSrv, 'hasPermission')
|
||||
.mockImplementation((action) => action === AccessControlAction.AlertingRuleExternalWrite);
|
||||
|
||||
// Act
|
||||
renderRuleDetails(cloudRule);
|
||||
|
||||
// Assert
|
||||
expect(ui.actionButtons.delete.query()).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function renderRuleDetails(rule: CombinedRule) {
|
||||
const store = configureStore();
|
||||
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<MemoryRouter>
|
||||
<RuleDetails rule={rule} />
|
||||
</MemoryRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function getGrafanaRule(override?: Partial<CombinedRule>) {
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Grafana',
|
||||
rulesSource: 'grafana',
|
||||
},
|
||||
...override,
|
||||
});
|
||||
}
|
||||
|
||||
function getCloudRule(override?: Partial<CombinedRule>) {
|
||||
return mockCombinedRule({
|
||||
namespace: {
|
||||
groups: [],
|
||||
name: 'Cortex',
|
||||
rulesSource: mockDataSource(),
|
||||
},
|
||||
promRule: mockPromAlertingRule(),
|
||||
rulerRule: mockRulerAlertingRule(),
|
||||
...override,
|
||||
});
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import React, { FC } from 'react';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { CombinedRule } from 'app/types/unified-alerting';
|
||||
import React, { FC } from 'react';
|
||||
import { AlertLabels } from '../AlertLabels';
|
||||
import { DetailsField } from '../DetailsField';
|
||||
import { RuleDetailsActionButtons } from './RuleDetailsActionButtons';
|
||||
import { RuleDetailsDataSources } from './RuleDetailsDataSources';
|
||||
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
|
||||
import { RuleDetailsExpression } from './RuleDetailsExpression';
|
||||
import { RuleDetailsAnnotations } from './RuleDetailsAnnotations';
|
||||
import { RuleDetailsDataSources } from './RuleDetailsDataSources';
|
||||
import { RuleDetailsExpression } from './RuleDetailsExpression';
|
||||
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
|
||||
|
||||
interface Props {
|
||||
rule: CombinedRule;
|
||||
|
@ -1,24 +1,25 @@
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, ConfirmModal, ClipboardButton, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { useAppNotification } from 'app/core/copy/appNotification';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { getRulesPermissions } from 'app/features/alerting/unified/utils/access-control';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||
import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import React, { FC, Fragment, useState } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useIsRuleEditable } from '../../hooks/useIsRuleEditable';
|
||||
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
||||
import { deleteRuleAction } from '../../state/actions';
|
||||
import { getAlertmanagerByUid } from '../../utils/alertmanager';
|
||||
import { Annotation } from '../../utils/constants';
|
||||
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
|
||||
import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc';
|
||||
import * as ruleId from '../../utils/rule-id';
|
||||
import { deleteRuleAction } from '../../state/actions';
|
||||
import { CombinedRule, RulesSource } from 'app/types/unified-alerting';
|
||||
import { getAlertmanagerByUid } from '../../utils/alertmanager';
|
||||
import { useStateHistoryModal } from '../../hooks/useStateHistoryModal';
|
||||
import { RulerGrafanaRuleDTO, RulerRuleDTO } from 'app/types/unified-alerting-dto';
|
||||
import { isFederatedRuleGroup } from '../../utils/rules';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
rule: CombinedRule;
|
||||
@ -38,12 +39,18 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
|
||||
? rulesSource
|
||||
: getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name;
|
||||
const rulesSourceName = getRulesSourceName(rulesSource);
|
||||
|
||||
const rulesPermissions = getRulesPermissions(rulesSourceName);
|
||||
const hasEditPermission = contextSrv.hasPermission(rulesPermissions.update);
|
||||
const hasDeletePermission = contextSrv.hasPermission(rulesPermissions.delete);
|
||||
const hasExplorePermission = contextSrv.hasPermission(AccessControlAction.DataSourcesExplore);
|
||||
|
||||
const leftButtons: JSX.Element[] = [];
|
||||
const rightButtons: JSX.Element[] = [];
|
||||
|
||||
const isFederated = isFederatedRuleGroup(group);
|
||||
const { isEditable } = useIsRuleEditable(getRulesSourceName(rulesSource), rulerRule);
|
||||
const { isEditable } = useIsRuleEditable(rulesSourceName, rulerRule);
|
||||
const returnTo = location.pathname + location.search;
|
||||
const isViewMode = inViewMode(location.pathname);
|
||||
|
||||
@ -74,7 +81,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
|
||||
// explore does not support grafana rule queries atm
|
||||
// neither do "federated rules"
|
||||
if (isCloudRulesSource(rulesSource) && contextSrv.isEditor && !isFederated) {
|
||||
if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
|
||||
leftButtons.push(
|
||||
<LinkButton
|
||||
className={style.button}
|
||||
@ -180,6 +187,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// TODO Maybe there is a way to unify isEditable with FGAC permissions
|
||||
if (isEditable && rulerRule && !isFederated) {
|
||||
const sourceName = getRulesSourceName(rulesSource);
|
||||
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
|
||||
@ -210,22 +218,28 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
|
||||
);
|
||||
}
|
||||
|
||||
rightButtons.push(
|
||||
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
|
||||
Edit
|
||||
</LinkButton>,
|
||||
<Button
|
||||
className={style.button}
|
||||
size="xs"
|
||||
type="button"
|
||||
key="delete"
|
||||
variant="secondary"
|
||||
icon="trash-alt"
|
||||
onClick={() => setRuleToDelete(rule)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
if (hasEditPermission) {
|
||||
rightButtons.push(
|
||||
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
|
||||
Edit
|
||||
</LinkButton>
|
||||
);
|
||||
}
|
||||
if (hasDeletePermission) {
|
||||
rightButtons.push(
|
||||
<Button
|
||||
className={style.button}
|
||||
size="xs"
|
||||
type="button"
|
||||
key="delete"
|
||||
variant="secondary"
|
||||
icon="trash-alt"
|
||||
onClick={() => setRuleToDelete(rule)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (leftButtons.length || rightButtons.length) {
|
||||
return (
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
|
||||
import pluralize from 'pluralize';
|
||||
import React, { FC, useEffect, useState } from 'react';
|
||||
@ -32,6 +34,8 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }
|
||||
const [isDeletingGroup, setIsDeletingGroup] = useState(false);
|
||||
const [isCollapsed, setIsCollapsed] = useState(!expandAll);
|
||||
|
||||
const canEditCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
|
||||
|
||||
useEffect(() => {
|
||||
setIsCollapsed(!expandAll);
|
||||
}, [expandAll]);
|
||||
@ -88,7 +92,7 @@ export const RulesGroup: FC<Props> = React.memo(({ group, namespace, expandAll }
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (hasRuler(rulesSource)) {
|
||||
} else if (canEditCloudRules && hasRuler(rulesSource)) {
|
||||
if (!isFederated) {
|
||||
actionIcons.push(
|
||||
<ActionIcon
|
||||
|
@ -2,12 +2,14 @@ import { AccessControlAction } from 'app/types';
|
||||
import { isGrafanaRulesSource } from './datasource';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
|
||||
function getAMversion(alertManagerSourceName: string) {
|
||||
type RulesSourceType = 'grafana' | 'external';
|
||||
|
||||
function getRulesSourceType(alertManagerSourceName: string): RulesSourceType {
|
||||
return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
|
||||
}
|
||||
|
||||
export function getInstancesPermissions(alertManagerSourceName: string) {
|
||||
const amVersion = getAMversion(alertManagerSourceName);
|
||||
export function getInstancesPermissions(rulesSourceName: string) {
|
||||
const sourceType = getRulesSourceType(rulesSourceName);
|
||||
|
||||
const permissions = {
|
||||
read: {
|
||||
@ -26,23 +28,18 @@ export function getInstancesPermissions(alertManagerSourceName: string) {
|
||||
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
viewSource: {
|
||||
grafana: AccessControlAction.AlertingInstanceRead,
|
||||
external: AccessControlAction.DataSourcesExplore,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
read: permissions.read[amVersion],
|
||||
create: permissions.create[amVersion],
|
||||
update: permissions.update[amVersion],
|
||||
delete: permissions.delete[amVersion],
|
||||
viewSource: permissions.viewSource[amVersion],
|
||||
read: permissions.read[sourceType],
|
||||
create: permissions.create[sourceType],
|
||||
update: permissions.update[sourceType],
|
||||
delete: permissions.delete[sourceType],
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotificationsPermissions(alertManagerSourceName: string) {
|
||||
const amVersion = getAMversion(alertManagerSourceName);
|
||||
export function getNotificationsPermissions(rulesSourceName: string) {
|
||||
const sourceType = getRulesSourceType(rulesSourceName);
|
||||
|
||||
const permissions = {
|
||||
read: {
|
||||
@ -64,10 +61,40 @@ export function getNotificationsPermissions(alertManagerSourceName: string) {
|
||||
};
|
||||
|
||||
return {
|
||||
read: permissions.read[amVersion],
|
||||
create: permissions.create[amVersion],
|
||||
update: permissions.update[amVersion],
|
||||
delete: permissions.delete[amVersion],
|
||||
read: permissions.read[sourceType],
|
||||
create: permissions.create[sourceType],
|
||||
update: permissions.update[sourceType],
|
||||
delete: permissions.delete[sourceType],
|
||||
};
|
||||
}
|
||||
|
||||
export function getRulesPermissions(rulesSourceName: string) {
|
||||
const sourceType = getRulesSourceType(rulesSourceName);
|
||||
|
||||
const permissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingRuleRead,
|
||||
external: AccessControlAction.AlertingRuleExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingRuleCreate,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingRuleUpdate,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingRuleDelete,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
read: permissions.read[sourceType],
|
||||
create: permissions.create[sourceType],
|
||||
update: permissions.update[sourceType],
|
||||
delete: permissions.delete[sourceType],
|
||||
};
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user