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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 90 additions and 60 deletions

View File

@ -3617,24 +3617,24 @@ exports[`better eslint`] = {
[0, 34, 3, "Unexpected any. Specify a different type.", "193409811"], [0, 34, 3, "Unexpected any. Specify a different type.", "193409811"],
[0, 39, 3, "Unexpected any. Specify a different type.", "193409811"] [0, 39, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"public/app/core/services/backend_srv.ts:3750224237": [ "public/app/core/services/backend_srv.ts:360059123": [
[79, 20, 3, "Unexpected any. Specify a different type.", "193409811"], [83, 20, 3, "Unexpected any. Specify a different type.", "193409811"],
[149, 63, 3, "Unexpected any. Specify a different type.", "193409811"], [153, 63, 3, "Unexpected any. Specify a different type.", "193409811"],
[227, 40, 3, "Unexpected any. Specify a different type.", "193409811"], [231, 40, 3, "Unexpected any. Specify a different type.", "193409811"],
[281, 38, 20, "Do not use any type assertions.", "443888156"], [285, 38, 20, "Do not use any type assertions.", "443888156"],
[281, 55, 3, "Unexpected any. Specify a different type.", "193409811"], [285, 55, 3, "Unexpected any. Specify a different type.", "193409811"],
[352, 103, 3, "Unexpected any. Specify a different type.", "193409811"], [356, 103, 3, "Unexpected any. Specify a different type.", "193409811"],
[388, 49, 3, "Unexpected any. Specify a different type.", "193409811"], [392, 49, 3, "Unexpected any. Specify a different type.", "193409811"],
[392, 16, 3, "Unexpected any. Specify a different type.", "193409811"], [396, 16, 3, "Unexpected any. Specify a different type.", "193409811"],
[392, 43, 3, "Unexpected any. Specify a different type.", "193409811"], [396, 43, 3, "Unexpected any. Specify a different type.", "193409811"],
[396, 35, 3, "Unexpected any. Specify a different type.", "193409811"], [400, 35, 3, "Unexpected any. Specify a different type.", "193409811"],
[400, 33, 3, "Unexpected any. Specify a different type.", "193409811"],
[404, 33, 3, "Unexpected any. Specify a different type.", "193409811"], [404, 33, 3, "Unexpected any. Specify a different type.", "193409811"],
[408, 31, 3, "Unexpected any. Specify a different type.", "193409811"], [408, 33, 3, "Unexpected any. Specify a different type.", "193409811"],
[412, 31, 3, "Unexpected any. Specify a different type.", "193409811"], [412, 31, 3, "Unexpected any. Specify a different type.", "193409811"],
[423, 16, 3, "Unexpected any. Specify a different type.", "193409811"] [416, 31, 3, "Unexpected any. Specify a different type.", "193409811"],
[427, 16, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"public/app/core/services/context_srv.ts:1157446284": [ "public/app/core/services/context_srv.ts:698805616": [
[59, 10, 3, "Unexpected any. Specify a different type.", "193409811"], [59, 10, 3, "Unexpected any. Specify a different type.", "193409811"],
[60, 11, 3, "Unexpected any. Specify a different type.", "193409811"], [60, 11, 3, "Unexpected any. Specify a different type.", "193409811"],
[62, 14, 3, "Unexpected any. Specify a different type.", "193409811"], [62, 14, 3, "Unexpected any. Specify a different type.", "193409811"],
@ -4201,14 +4201,11 @@ exports[`better eslint`] = {
[127, 14, 16, "Do not use any type assertions.", "1747412709"], [127, 14, 16, "Do not use any type assertions.", "1747412709"],
[128, 18, 24, "Do not use any type assertions.", "3254438164"] [128, 18, 24, "Do not use any type assertions.", "3254438164"]
], ],
"public/app/features/alerting/unified/RuleEditor.test.tsx:3247797279": [ "public/app/features/alerting/unified/RuleEditor.test.tsx:2974108144": [
[231, 42, 149, "Do not use any type assertions.", "2017267554"], [233, 42, 149, "Do not use any type assertions.", "2017267554"],
[418, 23, 50, "Do not use any type assertions.", "772738025"], [454, 42, 32, "Do not use any type assertions.", "2259972478"],
[418, 23, 36, "Do not use any type assertions.", "4069427080"], [507, 65, 3, "Unexpected any. Specify a different type.", "193409811"],
[420, 9, 3, "Unexpected any. Specify a different type.", "193409811"], [642, 42, 20, "Do not use any type assertions.", "1750699644"]
[452, 42, 32, "Do not use any type assertions.", "2259972478"],
[505, 65, 3, "Unexpected any. Specify a different type.", "193409811"],
[640, 42, 20, "Do not use any type assertions.", "1750699644"]
], ],
"public/app/features/alerting/unified/RuleList.test.tsx:629893124": [ "public/app/features/alerting/unified/RuleList.test.tsx:629893124": [
[125, 21, 16, "Do not use any type assertions.", "2939667099"], [125, 21, 16, "Do not use any type assertions.", "2939667099"],
@ -4386,8 +4383,8 @@ exports[`better eslint`] = {
[43, 13, 12, "Do not use any type assertions.", "96869412"], [43, 13, 12, "Do not use any type assertions.", "96869412"],
[64, 7, 25, "Do not use any type assertions.", "4119455957"] [64, 7, 25, "Do not use any type assertions.", "4119455957"]
], ],
"public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx:4024141754": [ "public/app/features/alerting/unified/hooks/useIsRuleEditable.test.tsx:2319947585": [
[150, 64, 29, "Do not use any type assertions.", "2249960884"] [163, 64, 29, "Do not use any type assertions.", "2249960884"]
], ],
"public/app/features/alerting/unified/mocks.ts:2719319751": [ "public/app/features/alerting/unified/mocks.ts:2719319751": [
[48, 14, 7, "Do not use any type assertions.", "3399135973"], [48, 14, 7, "Do not use any type assertions.", "3399135973"],
@ -4411,11 +4408,9 @@ exports[`better eslint`] = {
[234, 6, 172, "Do not use any type assertions.", "1154786841"], [234, 6, 172, "Do not use any type assertions.", "1154786841"],
[242, 6, 177, "Do not use any type assertions.", "3855113404"] [242, 6, 177, "Do not use any type assertions.", "3855113404"]
], ],
"public/app/features/alerting/unified/state/actions.ts:2828725785": [ "public/app/features/alerting/unified/state/actions.ts:175963059": [
[73, 23, 24, "Do not use any type assertions.", "3178482079"], [74, 23, 24, "Do not use any type assertions.", "3178482079"],
[253, 70, 24, "Do not use any type assertions.", "3178482079"], [254, 70, 24, "Do not use any type assertions.", "3178482079"]
[570, 60, 22, "Do not use any type assertions.", "3527197253"],
[570, 79, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"public/app/features/alerting/unified/types/receiver-form.ts:4067603235": [ "public/app/features/alerting/unified/types/receiver-form.ts:4067603235": [
[10, 27, 3, "Unexpected any. Specify a different type.", "193409811"], [10, 27, 3, "Unexpected any. Specify a different type.", "193409811"],

View File

@ -44,6 +44,10 @@ export interface BackendSrvDependencies {
logout: () => void; logout: () => void;
} }
export interface FolderRequestOptions {
withAccessControl?: boolean;
}
export class BackendSrv implements BackendService { export class BackendSrv implements BackendService {
private inFlightRequests: Subject<string> = new Subject<string>(); private inFlightRequests: Subject<string> = new Subject<string>();
private HTTP_REQUEST_CANCELED = -1; private HTTP_REQUEST_CANCELED = -1;
@ -433,8 +437,13 @@ export class BackendSrv implements BackendService {
return this.get<DashboardDTO>(`/api/public/dashboards/${uid}`); return this.get<DashboardDTO>(`/api/public/dashboards/${uid}`);
} }
getFolderByUid(uid: string) { getFolderByUid(uid: string, options: FolderRequestOptions = {}) {
return this.get<FolderDTO>(`/api/folders/${uid}`); const queryParams = new URLSearchParams();
if (options.withAccessControl) {
queryParams.set('accesscontrol', 'true');
}
return this.get<FolderDTO>(`/api/folders/${uid}?${queryParams.toString()}`);
} }
} }

View File

@ -174,7 +174,7 @@ export class ContextSrv {
} }
hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) { hasAccessInMetadata(action: string, object: WithAccessControlMetadata, fallBack: boolean) {
if (!config.rbacEnabled) { if (!this.accessControlEnabled()) {
return fallBack; return fallBack;
} }
return this.hasPermissionInMetadata(action, object); return this.hasPermissionInMetadata(action, object);

View File

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

View File

@ -23,14 +23,14 @@ describe('useIsRuleEditable', () => {
describe('RBAC enabled', () => { describe('RBAC enabled', () => {
beforeEach(enableRBAC); beforeEach(enableRBAC);
describe('Grafana rules', () => { 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 wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); 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', () => { 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 wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); 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', () => { it('Should forbid editing when the user has no alert rule update permission', () => {
mockPermissions([]); mockUseFolder({ accessControl: {} });
const wrapper = getProviderWrapper(); const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); 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', () => { it('Should forbid deleting when the user has no alert rule delete permission', () => {
mockPermissions([]); mockUseFolder({ accessControl: {} });
const wrapper = getProviderWrapper(); const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper });
@ -69,9 +76,15 @@ describe('useIsRuleEditable', () => {
expect(result.current.isRemovable).toBe(false); 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', () => { it('Should allow editing and deleting when the user has alert rule permissions but does not have folder canSave permission', () => {
mockPermissions([AccessControlAction.AlertingRuleUpdate, AccessControlAction.AlertingRuleDelete]); mockUseFolder({
mockUseFolder({ canSave: false }); canSave: false,
accessControl: {
[AccessControlAction.AlertingRuleUpdate]: true,
[AccessControlAction.AlertingRuleDelete]: true,
},
});
const wrapper = getProviderWrapper(); const wrapper = getProviderWrapper();
const { result } = renderHook(() => useIsRuleEditable('grafana', mockRulerGrafanaRule()), { wrapper }); 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); if (!folder) {
const canRemoveGrafanaRules = contextSrv.hasAccess(rulePermission.delete, folder?.canSave ?? false); // 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 { return {
isEditable: canEditGrafanaRules, isEditable: canEditGrafanaRules,

View File

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