Alerting: Improve alert list panel and alert rules toolbar permissions handling (#83954)

* Improve alert list panel and alert rules toolbar permissions handling

* Refactor permission checking, add tests

* Remove unneccessary act wrapper

* Fix test error
pull/84072/head
Konrad Lalik 1 year ago committed by GitHub
parent 1181141b40
commit a4acd9d204
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      public/app/features/alerting/unified/initAlerting.tsx
  2. 31
      public/app/plugins/panel/alertlist/UnifiedAlertList.tsx
  3. 22
      public/app/plugins/panel/alertlist/UnifiedalertList.test.tsx
  4. 4
      public/app/plugins/panel/alertlist/module.tsx

@ -1,14 +1,21 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { addCustomRightAction } from '../../dashboard/components/DashNav/DashNav';
import { getRulesPermissions } from './utils/access-control';
import { GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
const AlertRulesToolbarButton = React.lazy(
() => import(/* webpackChunkName: "alert-rules-toolbar-button" */ './integration/AlertRulesToolbarButton')
);
export function initAlerting() {
const grafanaRulesPermissions = getRulesPermissions(GRAFANA_RULES_SOURCE_NAME);
if (contextSrv.hasPermission(grafanaRulesPermissions.read)) {
addCustomRightAction({
show: () => config.unifiedAlertingEnabled || (config.featureToggles.alertingPreviewUpgrade ?? false),
component: ({ dashboard }) => (
@ -19,3 +26,4 @@ export function initAlerting() {
index: -2,
});
}
}

@ -16,7 +16,6 @@ import {
useStyles2,
} from '@grafana/ui';
import { config } from 'app/core/config';
import { contextSrv } from 'app/core/services/context_srv';
import alertDef from 'app/features/alerting/state/alertDef';
import { alertRuleApi } from 'app/features/alerting/unified/api/alertRuleApi';
import { INSTANCES_DISPLAY_LIMIT } from 'app/features/alerting/unified/components/rules/RuleDetails';
@ -38,9 +37,10 @@ import { flattenCombinedRules, getFirstActiveAt } from 'app/features/alerting/un
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel } from 'app/features/dashboard/state';
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction, ThunkDispatch, useDispatch } from 'app/types';
import { ThunkDispatch, useDispatch } from 'app/types';
import { PromAlertingRuleState } from 'app/types/unified-alerting-dto';
import { AlertingAction, useAlertingAbility } from '../../../features/alerting/unified/hooks/useAbilities';
import { getAlertingRule } from '../../../features/alerting/unified/utils/rules';
import { AlertingRule, CombinedRuleWithLocation } from '../../../types/unified-alerting';
@ -93,9 +93,10 @@ const fetchPromAndRuler = ({
}
};
export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const dispatch = useDispatch();
const [limitInstances, toggleLimit] = useToggle(true);
const [, gmaViewAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule);
const { usePrometheusRulesByNamespaceQuery } = alertRuleApi;
@ -137,7 +138,7 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
// If the datasource is not defined we should NOT skip the query
// Undefined dataSourceName means that there is no datasource filter applied and we should fetch all the rules
const shouldFetchGrafanaRules = !dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME;
const shouldFetchGrafanaRules = (!dataSourceName || dataSourceName === GRAFANA_RULES_SOURCE_NAME) && gmaViewAllowed;
//For grafana managed rules, get the result using RTK Query to avoid the need of using the redux store
//See https://github.com/grafana/grafana/pull/70482
@ -217,15 +218,6 @@ export function UnifiedAlertList(props: PanelProps<UnifiedAlertListOptions>) {
const havePreviousResults = Object.values(promRulesRequests).some((state) => state.result);
if (
!contextSrv.hasPermission(AccessControlAction.AlertingRuleRead) &&
!contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead)
) {
return (
<Alert title="Permission required">Sorry, you do not have the required permissions to read alert rules</Alert>
);
}
return (
<CustomScrollbar autoHeightMin="100%" autoHeightMax="100%">
<div className={styles.container}>
@ -456,3 +448,16 @@ export const getStyles = (theme: GrafanaTheme2) => ({
display: none;
`,
});
export function UnifiedAlertListPanel(props: PanelProps<UnifiedAlertListOptions>) {
const [, gmaReadAllowed] = useAlertingAbility(AlertingAction.ViewAlertRule);
const [, externalReadAllowed] = useAlertingAbility(AlertingAction.ViewExternalAlertRule);
if (!gmaReadAllowed && !externalReadAllowed) {
return (
<Alert title="Permission required">Sorry, you do not have the required permissions to read alert rules</Alert>
);
}
return <UnifiedAlertList {...props} />;
}

@ -2,7 +2,6 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Provider } from 'react-redux';
import { act } from 'react-test-renderer';
import { byRole, byText } from 'testing-library-selector';
import { FieldConfigSource, getDefaultTimeRange, LoadingState, PanelProps, PluginExtensionTypes } from '@grafana/data';
@ -25,7 +24,7 @@ import {
} from '../../../features/alerting/unified/mocks';
import { GRAFANA_RULES_SOURCE_NAME } from '../../../features/alerting/unified/utils/datasource';
import { UnifiedAlertList } from './UnifiedAlertList';
import { UnifiedAlertListPanel } from './UnifiedAlertList';
import { GroupMode, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
import * as utils from './util';
@ -159,20 +158,20 @@ const renderPanel = (options: Partial<UnifiedAlertListOptions> = defaultOptions)
return render(
<Provider store={store}>
<UnifiedAlertList {...props} />
<UnifiedAlertListPanel {...props} />
</Provider>
);
};
describe('UnifiedAlertList', () => {
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
it('subscribes to the dashboard refresh interval', async () => {
jest.spyOn(defaultProps, 'replaceVariables').mockReturnValue('severity=critical');
await act(async () => {
renderPanel();
});
expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1);
await waitFor(() => expect(dashboard.events.subscribe).toHaveBeenCalledTimes(1));
expect(dashboard.events.subscribe.mock.calls[0][0]).toEqual(TimeRangeUpdatedEvent);
});
@ -180,14 +179,12 @@ describe('UnifiedAlertList', () => {
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
});
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
const filterAlertsSpy = jest.spyOn(utils, 'filterAlerts');
const replaceVarsSpy = jest.spyOn(defaultProps, 'replaceVariables').mockReturnValue('severity=critical');
const user = userEvent.setup();
await act(async () => {
renderPanel({
alertInstanceLabelFilter: '$label',
dashboardAlerts: false,
@ -195,7 +192,6 @@ describe('UnifiedAlertList', () => {
datasource: GRAFANA_RULES_SOURCE_NAME,
folder: undefined,
});
});
await waitFor(() => {
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
@ -222,4 +218,12 @@ describe('UnifiedAlertList', () => {
expect.anything()
);
});
it('should render authorization error when user has no permission', async () => {
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(false);
renderPanel();
expect(screen.getByRole('alert', { name: 'Permission required' })).toBeInTheDocument();
});
});

@ -17,7 +17,7 @@ import { GRAFANA_DATASOURCE_NAME } from '../../../features/alerting/unified/util
import { AlertList } from './AlertList';
import { alertListPanelMigrationHandler } from './AlertListMigrationHandler';
import { GroupBy } from './GroupByWithLoading';
import { UnifiedAlertList } from './UnifiedAlertList';
import { UnifiedAlertListPanel } from './UnifiedAlertList';
import { AlertListSuggestionsSupplier } from './suggestions';
import { AlertListOptions, GroupMode, ShowOption, SortOrder, UnifiedAlertListOptions, ViewMode } from './types';
@ -156,7 +156,7 @@ const alertList = new PanelPlugin<AlertListOptions>(AlertList)
.setMigrationHandler(alertListPanelMigrationHandler)
.setSuggestionsSupplier(new AlertListSuggestionsSupplier());
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertList).setPanelOptions((builder) => {
const unifiedAlertList = new PanelPlugin<UnifiedAlertListOptions>(UnifiedAlertListPanel).setPanelOptions((builder) => {
builder
.addRadio({
path: 'viewMode',

Loading…
Cancel
Save