diff --git a/public/app/features/alerting/unified/RuleEditor.tsx b/public/app/features/alerting/unified/RuleEditor.tsx index 143c1f597ab..8fbd3315bc3 100644 --- a/public/app/features/alerting/unified/RuleEditor.tsx +++ b/public/app/features/alerting/unified/RuleEditor.tsx @@ -4,7 +4,6 @@ import { Alert, LinkButton, LoadingPlaceholder, useStyles2, withErrorBoundary } import Page from 'app/core/components/Page/Page'; import { useCleanup } from 'app/core/hooks/useCleanup'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; -import { contextSrv } from 'app/core/services/context_srv'; import { RuleIdentifier } from 'app/types/unified-alerting'; import React, { FC, useEffect } from 'react'; import { useDispatch } from 'react-redux'; @@ -13,6 +12,7 @@ import { AlertRuleForm } from './components/rule-editor/AlertRuleForm'; import { useIsRuleEditable } from './hooks/useIsRuleEditable'; import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; import { fetchAllPromBuildInfoAction, fetchEditableRuleAction } from './state/actions'; +import { useRulesAccess } from './utils/accessControlHooks'; import * as ruleId from './utils/rule-id'; interface ExistingRuleEditorProps { @@ -38,6 +38,7 @@ const ExistingRuleEditor: FC = ({ identifier }) => { ); } + if (error) { return ( @@ -47,12 +48,15 @@ const ExistingRuleEditor: FC = ({ identifier }) => { ); } + if (!result) { return Sorry! This rule does not exist.; } + if (isEditable === false) { return Sorry! You do not have permission to edit this rule.; } + return ; }; @@ -67,10 +71,16 @@ const RuleEditor: FC = ({ match }) => { await dispatch(fetchAllPromBuildInfoAction()); }, [dispatch]); - if (!(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor)) { + const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess(); + + if (!canCreateGrafanaRules && !canCreateCloudRules) { return Sorry! You are not allowed to create rules.; } + if (identifier && !canEditRules(identifier.ruleSourceName)) { + return Sorry! You are not allowed to edit rules.; + } + if (loading) { return ( diff --git a/public/app/features/alerting/unified/RuleList.tsx b/public/app/features/alerting/unified/RuleList.tsx index a2ca8e82e2f..3ca8b91ca73 100644 --- a/public/app/features/alerting/unified/RuleList.tsx +++ b/public/app/features/alerting/unified/RuleList.tsx @@ -1,24 +1,24 @@ +import { css } from '@emotion/css'; import { GrafanaTheme2, urlUtil } from '@grafana/data'; -import { useStyles2, LinkButton, withErrorBoundary, Button } from '@grafana/ui'; +import { Button, LinkButton, useStyles2, withErrorBoundary } from '@grafana/ui'; +import { useQueryParams } from 'app/core/hooks/useQueryParams'; import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; +import { useLocation } from 'react-router-dom'; import { AlertingPageWrapper } from './components/AlertingPageWrapper'; import { NoRulesSplash } from './components/rules/NoRulesCTA'; -import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; -import { useFilteredRules } from './hooks/useFilteredRules'; -import { fetchAllPromAndRulerRulesAction } from './state/actions'; -import { getAllRulesSourceNames } from './utils/datasource'; -import { css } from '@emotion/css'; -import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces'; -import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants'; -import RulesFilter from './components/rules/RulesFilter'; +import { RuleListErrors } from './components/rules/RuleListErrors'; import { RuleListGroupView } from './components/rules/RuleListGroupView'; import { RuleListStateView } from './components/rules/RuleListStateView'; -import { useQueryParams } from 'app/core/hooks/useQueryParams'; -import { useLocation } from 'react-router-dom'; -import { contextSrv } from 'app/core/services/context_srv'; +import RulesFilter from './components/rules/RulesFilter'; import { RuleStats } from './components/rules/RuleStats'; -import { RuleListErrors } from './components/rules/RuleListErrors'; +import { useCombinedRuleNamespaces } from './hooks/useCombinedRuleNamespaces'; +import { useFilteredRules } from './hooks/useFilteredRules'; +import { useUnifiedAlertingSelector } from './hooks/useUnifiedAlertingSelector'; +import { fetchAllPromAndRulerRulesAction } from './state/actions'; +import { useRulesAccess } from './utils/accessControlHooks'; +import { RULE_LIST_POLL_INTERVAL_MS } from './utils/constants'; +import { getAllRulesSourceNames } from './utils/datasource'; import { getFiltersFromUrlParams } from './utils/misc'; const VIEWS = { @@ -38,6 +38,8 @@ const RuleList = withErrorBoundary( const filters = getFiltersFromUrlParams(queryParams); const filtersActive = Object.values(filters).some((filter) => filter !== undefined); + const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess(); + const view = VIEWS[queryParams['view'] as keyof typeof VIEWS] ? (queryParams['view'] as keyof typeof VIEWS) : 'groups'; @@ -93,7 +95,7 @@ const RuleList = withErrorBoundary( )} - {(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && ( + {(canCreateGrafanaRules || canCreateCloudRules) && ( {}); jest.mock('@grafana/runtime', () => ({ getBackendSrv: () => ({ fetch }), })); diff --git a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx index 2a74d453395..536f959e6bd 100644 --- a/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx +++ b/public/app/features/alerting/unified/components/rules/NoRulesCTA.tsx @@ -1,10 +1,12 @@ -import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; -import { contextSrv } from 'app/core/services/context_srv'; -import React, { FC } from 'react'; import { CallToActionCard } from '@grafana/ui'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import React, { FC } from 'react'; +import { useRulesAccess } from '../../utils/accessControlHooks'; export const NoRulesSplash: FC = () => { - if (contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) { + const { canCreateGrafanaRules, canCreateCloudRules } = useRulesAccess(); + + if (canCreateGrafanaRules || canCreateCloudRules) { return ( = ({ alertManagerSourceName }) => { - if (contextSrv.isEditor) { + const permissions = getInstancesPermissions(alertManagerSourceName); + + if (contextSrv.hasAccess(permissions.create, contextSrv.isEditor)) { return ( fallBackUserRoles, actions); }; } + +export function getRulesAccess() { + return { + canCreateGrafanaRules: + contextSrv.hasEditPermissionInFolders && + contextSrv.hasAccess(rulesPermissions.create.grafana, contextSrv.isEditor), + canCreateCloudRules: contextSrv.hasAccess(rulesPermissions.create.external, contextSrv.isEditor), + canEditRules: (rulesSourceName: string) => + contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, contextSrv.isEditor), + }; +} diff --git a/public/app/features/alerting/unified/utils/accessControlHooks.ts b/public/app/features/alerting/unified/utils/accessControlHooks.ts new file mode 100644 index 00000000000..1181005f133 --- /dev/null +++ b/public/app/features/alerting/unified/utils/accessControlHooks.ts @@ -0,0 +1,6 @@ +import { useMemo } from 'react'; +import { getRulesAccess } from './access-control'; + +export function useRulesAccess() { + return useMemo(() => getRulesAccess(), []); +} diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index e494c62dda3..3d2385886fd 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -1,5 +1,7 @@ import { DataSourceJsonData, DataSourceInstanceSettings } from '@grafana/data'; +import { contextSrv } from 'app/core/services/context_srv'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; +import { AccessControlAction } from 'app/types'; import { RulesSource } from 'app/types/unified-alerting'; import { getAllDataSources } from './config'; @@ -15,13 +17,17 @@ export enum DataSourceType { export const RulesDataSourceTypes: string[] = [DataSourceType.Loki, DataSourceType.Prometheus]; export function getRulesDataSources() { + if (!contextSrv.hasPermission(AccessControlAction.AlertingRuleExternalRead)) { + return []; + } + return getAllDataSources() .filter((ds) => RulesDataSourceTypes.includes(ds.type) && ds.jsonData.manageAlerts !== false) .sort((a, b) => a.name.localeCompare(b.name)); } export function getRulesDataSource(rulesSourceName: string) { - return getAllDataSources().find((x) => x.name === rulesSourceName); + return getRulesDataSources().find((x) => x.name === rulesSourceName); } export function getAlertManagerDataSources() { @@ -42,11 +48,23 @@ export function getLotexDataSourceByName(dataSourceName: string): DataSourceInst } export function getAllRulesSourceNames(): string[] { - return [...getRulesDataSources().map((r) => r.name), GRAFANA_RULES_SOURCE_NAME]; + const availableRulesSources: string[] = getRulesDataSources().map((r) => r.name); + + if (contextSrv.hasPermission(AccessControlAction.AlertingRuleRead)) { + availableRulesSources.push(GRAFANA_RULES_SOURCE_NAME); + } + + return availableRulesSources; } export function getAllRulesSources(): RulesSource[] { - return [...getRulesDataSources(), GRAFANA_RULES_SOURCE_NAME]; + const availableRulesSources: RulesSource[] = getRulesDataSources(); + + if (contextSrv.hasPermission(AccessControlAction.AlertingRuleRead)) { + availableRulesSources.push(GRAFANA_RULES_SOURCE_NAME); + } + + return availableRulesSources; } export function getRulesSourceName(rulesSource: RulesSource): string { diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index bf9326f252e..c288586f1dd 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -1,39 +1,41 @@ import { DataQuery, + DataSourceRef, + getDefaultRelativeTimeRange, + IntervalValues, rangeUtil, RelativeTimeRange, ScopedVars, - getDefaultRelativeTimeRange, TimeRange, - IntervalValues, - DataSourceRef, } from '@grafana/data'; import { getDataSourceSrv } from '@grafana/runtime'; -import { contextSrv } from 'app/core/services/context_srv'; +import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { getNextRefIdChar } from 'app/core/utils/query'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { ExpressionDatasourceUID } from 'app/features/expressions/ExpressionDatasource'; import { ExpressionQuery, ExpressionQueryType } from 'app/features/expressions/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; import { + AlertQuery, Annotations, GrafanaAlertStateDecision, - AlertQuery, Labels, PostableRuleGrafanaRuleDTO, RulerRuleDTO, } from 'app/types/unified-alerting-dto'; import { EvalFunction } from '../../state/alertDef'; import { RuleFormType, RuleFormValues } from '../types/rule-form'; +import { getRulesAccess } from './access-control'; import { Annotation } from './constants'; import { isGrafanaRulesSource } from './datasource'; import { arrayToRecord, recordToArray } from './misc'; import { isAlertingRulerRule, isGrafanaRulerRule, isRecordingRulerRule } from './rules'; import { parseInterval } from './time'; -import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; -export const getDefaultFormValues = (): RuleFormValues => - Object.freeze({ +export const getDefaultFormValues = (): RuleFormValues => { + const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess(); + + return Object.freeze({ name: '', labels: [{ key: '', value: '' }], annotations: [ @@ -42,7 +44,7 @@ export const getDefaultFormValues = (): RuleFormValues => { key: Annotation.runbookURL, value: '' }, ], dataSourceName: null, - type: !contextSrv.isEditor ? RuleFormType.grafana : undefined, // viewers can't create prom alerts + type: canCreateGrafanaRules ? RuleFormType.grafana : canCreateCloudRules ? RuleFormType.cloudAlerting : undefined, // viewers can't create prom alerts // grafana folder: null, @@ -60,6 +62,7 @@ export const getDefaultFormValues = (): RuleFormValues => forTime: 1, forTimeUnit: 'm', }); +}; export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { const { name, expression, forTime, forTimeUnit, type } = values;