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:
Konrad Lalik
2022-04-08 13:34:02 +02:00
committed by GitHub
parent f1a1070d41
commit 87383b1c8b
9 changed files with 351 additions and 78 deletions
+21
View File
@@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { OrgRole } from '@grafana/data';
import { evaluateAccess } from './unified/utils/access-control'; import { evaluateAccess } from './unified/utils/access-control';
const commonRoutes: RouteDescriptor[] = [ const commonRoutes: RouteDescriptor[] = [
@@ -90,6 +91,10 @@ const unifiedRoutes: RouteDescriptor[] = [
...commonRoutes, ...commonRoutes,
{ {
path: '/alerting/list', path: '/alerting/list',
roles: evaluateAccess(
[AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead],
[OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList') () => import(/* webpackChunkName: "AlertRuleListIndex" */ 'app/features/alerting/unified/RuleList')
), ),
@@ -214,6 +219,10 @@ const unifiedRoutes: RouteDescriptor[] = [
}, },
{ {
path: '/alerting/groups/', path: '/alerting/groups/',
roles: evaluateAccess(
[AccessControlAction.AlertingInstanceRead, AccessControlAction.AlertingInstancesExternalRead],
[OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups') () => import(/* webpackChunkName: "AlertGroups" */ 'app/features/alerting/unified/AlertGroups')
), ),
@@ -221,6 +230,10 @@ const unifiedRoutes: RouteDescriptor[] = [
{ {
path: '/alerting/new', path: '/alerting/new',
pageClass: 'page-alerting', pageClass: 'page-alerting',
roles: evaluateAccess(
[AccessControlAction.AlertingRuleCreate, AccessControlAction.AlertingRuleExternalWrite],
[OrgRole.Editor, OrgRole.Admin]
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
), ),
@@ -228,6 +241,10 @@ const unifiedRoutes: RouteDescriptor[] = [
{ {
path: '/alerting/:id/edit', path: '/alerting/:id/edit',
pageClass: 'page-alerting', pageClass: 'page-alerting',
roles: evaluateAccess(
[AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleExternalWrite],
[OrgRole.Editor, OrgRole.Admin]
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor') () => import(/* webpackChunkName: "AlertingRuleForm"*/ 'app/features/alerting/unified/RuleEditor')
), ),
@@ -235,6 +252,10 @@ const unifiedRoutes: RouteDescriptor[] = [
{ {
path: '/alerting/:sourceName/:id/view', path: '/alerting/:sourceName/:id/view',
pageClass: 'page-alerting', pageClass: 'page-alerting',
roles: evaluateAccess(
[AccessControlAction.AlertingRuleRead, AccessControlAction.AlertingRuleExternalRead],
[OrgRole.Viewer, OrgRole.Editor, OrgRole.Admin]
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer') () => import(/* webpackChunkName: "AlertingRule"*/ 'app/features/alerting/unified/RuleViewer')
), ),
@@ -16,6 +16,7 @@ jest.mock('app/core/services/context_srv', () => ({
contextSrv: { contextSrv: {
isEditor: true, isEditor: true,
hasAccess: () => true, hasAccess: () => true,
hasPermission: () => true,
}, },
})); }));
const mocks = { const mocks = {
@@ -3,11 +3,13 @@ import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { getInstancesPermissions } from '../../utils/access-control';
import { isGrafanaRulesSource } from '../../utils/datasource';
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc'; import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
import { AnnotationDetailsField } from '../AnnotationDetailsField'; import { AnnotationDetailsField } from '../AnnotationDetailsField';
import { Authorize } from '../Authorize'; import { Authorize } from '../Authorize';
import { getInstancesPermissions } from '../../utils/access-control';
interface AmNotificationsAlertDetailsProps { interface AmNotificationsAlertDetailsProps {
alertManagerSourceName: string; alertManagerSourceName: string;
@@ -16,11 +18,19 @@ interface AmNotificationsAlertDetailsProps {
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => { export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const 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 ( return (
<> <>
<div className={styles.actionsRow}> <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 && ( {alert.status.state === AlertState.Suppressed && (
<LinkButton <LinkButton
href={`${makeAMLink( href={`${makeAMLink(
@@ -45,13 +55,11 @@ export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, aler
</LinkButton> </LinkButton>
)} )}
</Authorize> </Authorize>
<Authorize actions={[permissions.viewSource]}> {isSeeSourceButtonEnabled && alert.generatorURL && (
{alert.generatorURL && ( <LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}> See source
See source </LinkButton>
</LinkButton> )}
)}
</Authorize>
</div> </div>
{Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => ( {Object.entries(alert.annotations).map(([annotationKey, annotationValue]) => (
<AnnotationDetailsField key={annotationKey} annotationKey={annotationKey} value={annotationValue} /> <AnnotationDetailsField key={annotationKey} annotationKey={annotationKey} value={annotationValue} />
@@ -9,6 +9,8 @@ import { AlertQuery } from 'app/types/unified-alerting-dto';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup'; import { PanelPluginsButtonGroup, SupportedPanelPlugins } from '../PanelPluginsButtonGroup';
import { TABLE, TIMESERIES } from '../../utils/constants'; import { TABLE, TIMESERIES } from '../../utils/constants';
import { Authorize } from '../Authorize';
import { AccessControlAction } from 'app/types';
type RuleViewerVisualizationProps = { type RuleViewerVisualizationProps = {
data?: PanelData; data?: PanelData;
@@ -91,20 +93,22 @@ export function RuleViewerVisualization(props: RuleViewerVisualizationProps): JS
/> />
) : null} ) : null}
<PanelPluginsButtonGroup onChange={setPanel} value={panel} size="md" /> <PanelPluginsButtonGroup onChange={setPanel} value={panel} size="md" />
{!isExpressionQuery(query.model) && ( <Authorize actions={[AccessControlAction.DataSourcesExplore]}>
<> {!isExpressionQuery(query.model) && (
<div className={styles.spacing} /> <>
<LinkButton <div className={styles.spacing} />
size="md" <LinkButton
variant="secondary" size="md"
icon="compass" variant="secondary"
target="_blank" icon="compass"
href={createExploreLink(dsSettings, query)} target="_blank"
> href={createExploreLink(dsSettings, query)}
View in Explore >
</LinkButton> View in Explore
</> </LinkButton>
)} </>
)}
</Authorize>
</div> </div>
</div> </div>
<PanelRenderer <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 { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; 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 { AlertLabels } from '../AlertLabels';
import { DetailsField } from '../DetailsField'; import { DetailsField } from '../DetailsField';
import { RuleDetailsActionButtons } from './RuleDetailsActionButtons'; import { RuleDetailsActionButtons } from './RuleDetailsActionButtons';
import { RuleDetailsDataSources } from './RuleDetailsDataSources';
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
import { RuleDetailsExpression } from './RuleDetailsExpression';
import { RuleDetailsAnnotations } from './RuleDetailsAnnotations'; import { RuleDetailsAnnotations } from './RuleDetailsAnnotations';
import { RuleDetailsDataSources } from './RuleDetailsDataSources';
import { RuleDetailsExpression } from './RuleDetailsExpression';
import { RuleDetailsMatchingInstances } from './RuleDetailsMatchingInstances';
interface Props { interface Props {
rule: CombinedRule; 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 { css } from '@emotion/css';
import { GrafanaTheme2, urlUtil } from '@grafana/data'; import { GrafanaTheme2, urlUtil } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { Button, ConfirmModal, ClipboardButton, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui'; import { Button, ClipboardButton, ConfirmModal, HorizontalGroup, LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { useAppNotification } from 'app/core/copy/appNotification'; 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 { 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 { Annotation } from '../../utils/constants';
import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource'; import { getRulesSourceName, isCloudRulesSource, isGrafanaRulesSource } from '../../utils/datasource';
import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc'; import { createExploreLink, createViewLink, makeRuleBasedSilenceLink } from '../../utils/misc';
import * as ruleId from '../../utils/rule-id'; 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 { isFederatedRuleGroup } from '../../utils/rules';
import { AccessControlAction } from 'app/types';
interface Props { interface Props {
rule: CombinedRule; rule: CombinedRule;
@@ -38,12 +39,18 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
const alertmanagerSourceName = isGrafanaRulesSource(rulesSource) const alertmanagerSourceName = isGrafanaRulesSource(rulesSource)
? rulesSource ? rulesSource
: getAlertmanagerByUid(rulesSource.jsonData.alertmanagerUid)?.name; : 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 leftButtons: JSX.Element[] = [];
const rightButtons: JSX.Element[] = []; const rightButtons: JSX.Element[] = [];
const isFederated = isFederatedRuleGroup(group); const isFederated = isFederatedRuleGroup(group);
const { isEditable } = useIsRuleEditable(getRulesSourceName(rulesSource), rulerRule); const { isEditable } = useIsRuleEditable(rulesSourceName, rulerRule);
const returnTo = location.pathname + location.search; const returnTo = location.pathname + location.search;
const isViewMode = inViewMode(location.pathname); const isViewMode = inViewMode(location.pathname);
@@ -74,7 +81,7 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
// explore does not support grafana rule queries atm // explore does not support grafana rule queries atm
// neither do "federated rules" // neither do "federated rules"
if (isCloudRulesSource(rulesSource) && contextSrv.isEditor && !isFederated) { if (isCloudRulesSource(rulesSource) && hasExplorePermission && !isFederated) {
leftButtons.push( leftButtons.push(
<LinkButton <LinkButton
className={style.button} 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) { if (isEditable && rulerRule && !isFederated) {
const sourceName = getRulesSourceName(rulesSource); const sourceName = getRulesSourceName(rulesSource);
const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule); const identifier = ruleId.fromRulerRule(sourceName, namespace.name, group.name, rulerRule);
@@ -210,22 +218,28 @@ export const RuleDetailsActionButtons: FC<Props> = ({ rule, rulesSource }) => {
); );
} }
rightButtons.push( if (hasEditPermission) {
<LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}> rightButtons.push(
Edit <LinkButton className={style.button} size="xs" key="edit" variant="secondary" icon="pen" href={editURL}>
</LinkButton>, Edit
<Button </LinkButton>
className={style.button} );
size="xs" }
type="button" if (hasDeletePermission) {
key="delete" rightButtons.push(
variant="secondary" <Button
icon="trash-alt" className={style.button}
onClick={() => setRuleToDelete(rule)} size="xs"
> type="button"
Delete key="delete"
</Button> variant="secondary"
); icon="trash-alt"
onClick={() => setRuleToDelete(rule)}
>
Delete
</Button>
);
}
} }
if (leftButtons.length || rightButtons.length) { if (leftButtons.length || rightButtons.length) {
return ( return (
@@ -1,7 +1,9 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Badge, ConfirmModal, HorizontalGroup, Icon, Spinner, Tooltip, useStyles2 } from '@grafana/ui'; 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 kbn from 'app/core/utils/kbn';
import { AccessControlAction } from 'app/types';
import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting'; import { CombinedRuleGroup, CombinedRuleNamespace } from 'app/types/unified-alerting';
import pluralize from 'pluralize'; import pluralize from 'pluralize';
import React, { FC, useEffect, useState } from 'react'; 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 [isDeletingGroup, setIsDeletingGroup] = useState(false);
const [isCollapsed, setIsCollapsed] = useState(!expandAll); const [isCollapsed, setIsCollapsed] = useState(!expandAll);
const canEditCloudRules = contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalWrite);
useEffect(() => { useEffect(() => {
setIsCollapsed(!expandAll); setIsCollapsed(!expandAll);
}, [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) { if (!isFederated) {
actionIcons.push( actionIcons.push(
<ActionIcon <ActionIcon
@@ -2,12 +2,14 @@ import { AccessControlAction } from 'app/types';
import { isGrafanaRulesSource } from './datasource'; import { isGrafanaRulesSource } from './datasource';
import { contextSrv } from 'app/core/services/context_srv'; 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'; return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
} }
export function getInstancesPermissions(alertManagerSourceName: string) { export function getInstancesPermissions(rulesSourceName: string) {
const amVersion = getAMversion(alertManagerSourceName); const sourceType = getRulesSourceType(rulesSourceName);
const permissions = { const permissions = {
read: { read: {
@@ -26,23 +28,18 @@ export function getInstancesPermissions(alertManagerSourceName: string) {
grafana: AccessControlAction.AlertingInstanceUpdate, grafana: AccessControlAction.AlertingInstanceUpdate,
external: AccessControlAction.AlertingInstancesExternalWrite, external: AccessControlAction.AlertingInstancesExternalWrite,
}, },
viewSource: {
grafana: AccessControlAction.AlertingInstanceRead,
external: AccessControlAction.DataSourcesExplore,
},
}; };
return { return {
read: permissions.read[amVersion], read: permissions.read[sourceType],
create: permissions.create[amVersion], create: permissions.create[sourceType],
update: permissions.update[amVersion], update: permissions.update[sourceType],
delete: permissions.delete[amVersion], delete: permissions.delete[sourceType],
viewSource: permissions.viewSource[amVersion],
}; };
} }
export function getNotificationsPermissions(alertManagerSourceName: string) { export function getNotificationsPermissions(rulesSourceName: string) {
const amVersion = getAMversion(alertManagerSourceName); const sourceType = getRulesSourceType(rulesSourceName);
const permissions = { const permissions = {
read: { read: {
@@ -64,10 +61,40 @@ export function getNotificationsPermissions(alertManagerSourceName: string) {
}; };
return { return {
read: permissions.read[amVersion], read: permissions.read[sourceType],
create: permissions.create[amVersion], create: permissions.create[sourceType],
update: permissions.update[amVersion], update: permissions.update[sourceType],
delete: permissions.delete[amVersion], 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],
}; };
} }