Alerting: Use folders' RBAC permission to control rules actions (#51434)

Co-authored-by: Gilles De Mey <gilles.de.mey@gmail.com>
This commit is contained in:
Konrad Lalik
2022-06-30 13:00:29 +02:00
committed by GitHub
parent 2a6b32598d
commit 117bac71f5
7 changed files with 90 additions and 60 deletions

View File

@@ -7,19 +7,21 @@ import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
import { DataSourceInstanceSettings } from '@grafana/data';
import { BackendSrv, locationService, setBackendSrv, setDataSourceSrv } from '@grafana/runtime';
import { locationService, setDataSourceSrv } from '@grafana/runtime';
import { contextSrv } from 'app/core/services/context_srv';
import { DashboardSearchHit } from 'app/features/search/types';
import { configureStore } from 'app/store/configureStore';
import { GrafanaAlertStateDecision, PromApplication } from 'app/types/unified-alerting-dto';
import { searchFolders } from '../../../../app/features/manage-dashboards/state/actions';
import { backendSrv } from '../../../core/services/backend_srv';
import { AccessControlAction } from '../../../types';
import RuleEditor from './RuleEditor';
import { discoverFeatures } from './api/buildInfo';
import { fetchRulerRules, fetchRulerRulesGroup, fetchRulerRulesNamespace, setRulerRuleGroup } from './api/ruler';
import { ExpressionEditorProps } from './components/rule-editor/ExpressionEditor';
import { disableRBAC, mockDataSource, MockDataSourceSrv } from './mocks';
import { disableRBAC, mockDataSource, MockDataSourceSrv, mockFolder } from './mocks';
import { getAllDataSources } from './utils/config';
import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import { getDefaultQueries } from './utils/rule-form';
@@ -404,10 +406,7 @@ describe('RuleEditor', () => {
uid: 'abcd',
id: 1,
};
const getFolderByUid = jest.fn().mockResolvedValue({
...folder,
canSave: true,
});
const dataSources = {
default: mockDataSource({
type: 'prometheus',
@@ -416,10 +415,13 @@ describe('RuleEditor', () => {
}),
};
const backendSrv = {
getFolderByUid,
} as any as BackendSrv;
setBackendSrv(backendSrv);
jest.spyOn(backendSrv, 'getFolderByUid').mockResolvedValue({
...mockFolder(),
accessControl: {
[AccessControlAction.AlertingRuleUpdate]: true,
},
});
setDataSourceSrv(new MockDataSourceSrv(dataSources));
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));

View File

@@ -23,14 +23,14 @@ describe('useIsRuleEditable', () => {
describe('RBAC enabled', () => {
beforeEach(enableRBAC);
describe('Grafana rules', () => {
// When RBAC is enabled we require only folder:read permission and apriopriate alerting permissions
// When RBAC is enabled we require appropriate alerting permissions in the folder scope
it('Should allow editing when the user has the alert rule update permission in the folder', () => {
mockUseFolder({
accessControl: {
[AccessControlAction.AlertingRuleUpdate]: true,
},
});
beforeEach(() => {
mockUseFolder({ canSave: false });
});
it('Should allow editing when the user has the alert rule update permission', () => {
mockPermissions([AccessControlAction.AlertingRuleUpdate]);
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper });
@@ -40,7 +40,12 @@ describe('useIsRuleEditable', () => {
});
it('Should allow deleting when the user has the alert rule delete permission', () => {
mockPermissions([AccessControlAction.AlertingRuleDelete]);
mockUseFolder({
accessControl: {
[AccessControlAction.AlertingRuleDelete]: true,
},
});
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper });
@@ -50,7 +55,8 @@ describe('useIsRuleEditable', () => {
});
it('Should forbid editing when the user has no alert rule update permission', () => {
mockPermissions([]);
mockUseFolder({ accessControl: {} });
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper });
@@ -60,7 +66,8 @@ describe('useIsRuleEditable', () => {
});
it('Should forbid deleting when the user has no alert rule delete permission', () => {
mockPermissions([]);
mockUseFolder({ accessControl: {} });
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper });
@@ -69,9 +76,15 @@ describe('useIsRuleEditable', () => {
expect(result.current.isRemovable).toBe(false);
});
it('Should allow editing and deleting when the user has aler rule permissions but does not have folder canSave permission', () => {
mockPermissions([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleDelete]);
mockUseFolder({ canSave: false });
it('Should allow editing and deleting when the user has alert rule permissions but does not have folder canSave permission', () => {
mockUseFolder({
canSave: false,
accessControl: {
[AccessControlAction.AlertingRuleUpdate]: true,
[AccessControlAction.AlertingRuleDelete]: true,
},
});
const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper });

View File

@@ -35,8 +35,18 @@ export function useIsRuleEditable(rulesSourceName: string, rule?: RulerRuleDTO):
);
}
const canEditGrafanaRules = contextSrv.hasAccess(rulePermission.update, folder?.canSave ?? false);
const canRemoveGrafanaRules = contextSrv.hasAccess(rulePermission.delete, folder?.canSave ?? false);
if (!folder) {
// Loading or invalid folder UID
return {
isEditable: false,
isRemovable: false,
loading,
};
}
const rbacDisabledFallback = folder.canSave;
const canEditGrafanaRules = contextSrv.hasAccessInMetadata(rulePermission.update, folder, rbacDisabledFallback);
const canRemoveGrafanaRules = contextSrv.hasAccessInMetadata(rulePermission.delete, folder, rbacDisabledFallback);
return {
isEditable: canEditGrafanaRules,

View File

@@ -1,7 +1,7 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { isEmpty } from 'lodash';
import { getBackendSrv, locationService } from '@grafana/runtime';
import { locationService } from '@grafana/runtime';
import {
AlertmanagerAlert,
AlertManagerCortexConfig,
@@ -27,6 +27,7 @@ import {
} from 'app/types/unified-alerting';
import { PromApplication, RulerRulesConfigDTO } from 'app/types/unified-alerting-dto';
import { backendSrv } from '../../../../core/services/backend_srv';
import {
addAlertManagers,
createOrUpdateSilence,
@@ -568,7 +569,7 @@ export const deleteTemplateAction = (templateName: string, alertManagerSourceNam
export const fetchFolderAction = createAsyncThunk(
'unifiedalerting/fetchFolder',
(uid: string): Promise<FolderDTO> => withSerializedError((getBackendSrv() as any).getFolderByUid(uid))
(uid: string): Promise<FolderDTO> => withSerializedError(backendSrv.getFolderByUid(uid, { withAccessControl: true }))
);
export const fetchFolderIfNotFetchedAction = (uid: string): ThunkResult<void> => {