diff --git a/public/app/features/alerting/unified/RuleList.test.tsx b/public/app/features/alerting/unified/RuleList.test.tsx index e7898e55547..b13b738d74b 100644 --- a/public/app/features/alerting/unified/RuleList.test.tsx +++ b/public/app/features/alerting/unified/RuleList.test.tsx @@ -8,20 +8,23 @@ import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector' import { locationService, setDataSourceSrv } from '@grafana/runtime'; import { configureStore } from 'app/store/configureStore'; +import { AccessControlAction } from 'app/types'; import { PromAlertingRuleState, PromApplication } from 'app/types/unified-alerting-dto'; import RuleList from './RuleList'; import { fetchBuildInfo } from './api/buildInfo'; import { fetchRules } from './api/prometheus'; -import { fetchRulerRules, deleteRulerRulesGroup, deleteNamespace, setRulerRuleGroup } from './api/ruler'; +import { deleteNamespace, deleteRulerRulesGroup, fetchRulerRules, setRulerRuleGroup } from './api/ruler'; import { + enableRBAC, + grantUserPermissions, mockDataSource, + MockDataSourceSrv, mockPromAlert, mockPromAlertingRule, mockPromRecordingRule, mockPromRuleGroup, mockPromRuleNamespace, - MockDataSourceSrv, somePromRules, someRulerRules, } from './mocks'; @@ -101,6 +104,8 @@ const ui = { moreErrorsButton: byRole('button', { name: /more errors/ }), editCloudGroupIcon: byTestId('edit-group'), + newRuleButton: byRole('link', { name: 'New alert rule' }), + editGroupModal: { namespaceInput: byLabelText('Namespace'), ruleGroupInput: byLabelText('Rule group'), @@ -628,4 +633,105 @@ describe('RuleList', () => { ); }); }); + + describe('RBAC Enabled', () => { + describe('Grafana Managed Alerts', () => { + it('New alert button should be visible when the user has alert rule create and folder read permissions and no rules exists', async () => { + enableRBAC(); + + grantUserPermissions([ + AccessControlAction.FoldersRead, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + ]); + + mocks.getAllDataSourcesMock.mockReturnValue([]); + setDataSourceSrv(new MockDataSourceSrv({})); + mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({}); + + renderRuleList(); + + await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + expect(ui.newRuleButton.get()).toBeInTheDocument(); + }); + + it('New alert button should be visible when the user has alert rule create and folder read permissions and rules already exists', async () => { + enableRBAC(); + + grantUserPermissions([ + AccessControlAction.FoldersRead, + AccessControlAction.AlertingRuleCreate, + AccessControlAction.AlertingRuleRead, + ]); + + mocks.getAllDataSourcesMock.mockReturnValue([]); + setDataSourceSrv(new MockDataSourceSrv({})); + mocks.api.fetchRules.mockResolvedValue(somePromRules('grafana')); + mocks.api.fetchRulerRules.mockResolvedValue(someRulerRules); + + renderRuleList(); + + await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + expect(ui.newRuleButton.get()).toBeInTheDocument(); + }); + }); + + describe('Cloud Alerts', () => { + it('New alert button should be visible when the user has the alert rule external write and datasource read permissions and no rules exists', async () => { + enableRBAC(); + + grantUserPermissions([ + // AccessControlAction.AlertingRuleRead, + AccessControlAction.DataSourcesRead, + AccessControlAction.AlertingRuleExternalRead, + AccessControlAction.AlertingRuleExternalWrite, + ]); + + mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]); + setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom })); + mocks.api.fetchBuildInfo.mockResolvedValue({ + application: PromApplication.Cortex, + features: { + rulerApiEnabled: true, + }, + }); + + mocks.api.fetchRules.mockResolvedValue([]); + mocks.api.fetchRulerRules.mockResolvedValue({}); + + renderRuleList(); + + await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + expect(ui.newRuleButton.get()).toBeInTheDocument(); + }); + + it('New alert button should be visible when the user has the alert rule external write and data source read permissions and rules already exists', async () => { + enableRBAC(); + + grantUserPermissions([ + AccessControlAction.DataSourcesRead, + AccessControlAction.AlertingRuleExternalRead, + AccessControlAction.AlertingRuleExternalWrite, + ]); + + mocks.getAllDataSourcesMock.mockReturnValue([dataSources.prom]); + setDataSourceSrv(new MockDataSourceSrv({ prom: dataSources.prom })); + mocks.api.fetchBuildInfo.mockResolvedValue({ + application: PromApplication.Cortex, + features: { + rulerApiEnabled: true, + }, + }); + + mocks.api.fetchRules.mockResolvedValue(somePromRules('Cortex')); + mocks.api.fetchRulerRules.mockResolvedValue(someRulerRules); + + renderRuleList(); + + await waitFor(() => expect(mocks.api.fetchRules).toHaveBeenCalledTimes(1)); + expect(ui.newRuleButton.get()).toBeInTheDocument(); + }); + }); + }); }); diff --git a/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx b/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx index 1e65372a238..518348877aa 100644 --- a/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/RuleFolderPicker.tsx @@ -1,6 +1,7 @@ import React, { FC } from 'react'; import { FolderPicker, Props as FolderPickerProps } from 'app/core/components/Select/FolderPicker'; +import { PermissionLevelString } from 'app/types'; export interface Folder { title: string; @@ -12,5 +13,12 @@ export interface Props extends Omit = ({ value, ...props }) => ( - + ); diff --git a/public/app/features/alerting/unified/mocks.ts b/public/app/features/alerting/unified/mocks.ts index 0dcb67cca28..70c9070c8fd 100644 --- a/public/app/features/alerting/unified/mocks.ts +++ b/public/app/features/alerting/unified/mocks.ts @@ -6,7 +6,8 @@ import { DataSourceRef, ScopedVars, } from '@grafana/data'; -import { DataSourceSrv, GetDataSourceListFilters, config } from '@grafana/runtime'; +import { config, DataSourceSrv, GetDataSourceListFilters } from '@grafana/runtime'; +import { contextSrv } from 'app/core/services/context_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AlertmanagerAlert, @@ -18,8 +19,8 @@ import { Silence, SilenceState, } from 'app/plugins/datasource/alertmanager/types'; -import { FolderDTO } from 'app/types'; -import { AlertingRule, Alert, RecordingRule, RuleGroup, RuleNamespace, CombinedRule } from 'app/types/unified-alerting'; +import { AccessControlAction, FolderDTO } from 'app/types'; +import { Alert, AlertingRule, CombinedRule, RecordingRule, RuleGroup, RuleNamespace } from 'app/types/unified-alerting'; import { GrafanaAlertStateDecision, GrafanaRuleDefinition, @@ -465,3 +466,13 @@ export const mockFolder = (partial?: Partial): FolderDTO => { ...partial, }; }; + +export const enableRBAC = () => { + jest.spyOn(contextSrv, 'accessControlEnabled').mockReturnValue(true); +}; + +export const grantUserPermissions = (permissions: AccessControlAction[]) => { + jest + .spyOn(contextSrv, 'hasPermission') + .mockImplementation((action) => permissions.includes(action as AccessControlAction)); +}; diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts index 0f03e12af10..9b736179118 100644 --- a/public/app/features/alerting/unified/utils/access-control.ts +++ b/public/app/features/alerting/unified/utils/access-control.ts @@ -108,9 +108,11 @@ export function evaluateAccess(actions: AccessControlAction[], fallBackUserRoles export function getRulesAccess() { return { canCreateGrafanaRules: - contextSrv.hasEditPermissionInFolders && + contextSrv.hasAccess(AccessControlAction.FoldersRead, contextSrv.isEditor) && contextSrv.hasAccess(rulesPermissions.create.grafana, contextSrv.isEditor), - canCreateCloudRules: contextSrv.hasAccess(rulesPermissions.create.external, contextSrv.isEditor), + canCreateCloudRules: + contextSrv.hasAccess(AccessControlAction.DataSourcesRead, contextSrv.isEditor) && + contextSrv.hasAccess(rulesPermissions.create.external, contextSrv.isEditor), canEditRules: (rulesSourceName: string) => contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, contextSrv.isEditor), };