mirror of
https://github.com/grafana/grafana.git
synced 2026-08-18 09:05:04 -05: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:
@@ -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} />
|
||||||
|
|||||||
+18
-14
@@ -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;
|
||||||
|
|||||||
+43
-29
@@ -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],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user