mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: FGAC bug bash fixes (#47873)
* Improve new alert and new silence buttons permission handling * Prevent loading alert rules when no sufficient permissions provided * Improve add and edit rule permissions * Add new rule CTA button for non-editors * Update mock * Fix imports
This commit is contained in:
parent
0c31399e34
commit
785145c045
@ -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<ExistingRuleEditorProps> = ({ identifier }) => {
|
||||
</Page.Contents>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Page.Contents>
|
||||
@ -47,12 +48,15 @@ const ExistingRuleEditor: FC<ExistingRuleEditorProps> = ({ identifier }) => {
|
||||
</Page.Contents>
|
||||
);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return <AlertWarning title="Rule not found">Sorry! This rule does not exist.</AlertWarning>;
|
||||
}
|
||||
|
||||
if (isEditable === false) {
|
||||
return <AlertWarning title="Cannot edit rule">Sorry! You do not have permission to edit this rule.</AlertWarning>;
|
||||
}
|
||||
|
||||
return <AlertRuleForm existing={result} />;
|
||||
};
|
||||
|
||||
@ -67,10 +71,16 @@ const RuleEditor: FC<RuleEditorProps> = ({ match }) => {
|
||||
await dispatch(fetchAllPromBuildInfoAction());
|
||||
}, [dispatch]);
|
||||
|
||||
if (!(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor)) {
|
||||
const { canCreateGrafanaRules, canCreateCloudRules, canEditRules } = useRulesAccess();
|
||||
|
||||
if (!canCreateGrafanaRules && !canCreateCloudRules) {
|
||||
return <AlertWarning title="Cannot create rules">Sorry! You are not allowed to create rules.</AlertWarning>;
|
||||
}
|
||||
|
||||
if (identifier && !canEditRules(identifier.ruleSourceName)) {
|
||||
return <AlertWarning title="Cannot edit rules">Sorry! You are not allowed to edit rules.</AlertWarning>;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Page.Contents>
|
||||
|
@ -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(
|
||||
)}
|
||||
<RuleStats showInactive={true} showRecording={true} namespaces={filteredNamespaces} />
|
||||
</div>
|
||||
{(contextSrv.hasEditPermissionInFolders || contextSrv.isEditor) && (
|
||||
{(canCreateGrafanaRules || canCreateCloudRules) && (
|
||||
<LinkButton
|
||||
href={urlUtil.renderUrl('alerting/new', { returnTo: location.pathname + location.search })}
|
||||
icon="plus"
|
||||
|
@ -8,6 +8,7 @@ const fetch = jest.fn();
|
||||
|
||||
jest.mock('./prometheus');
|
||||
jest.mock('./ruler');
|
||||
jest.mock('app/core/services/context_srv', () => {});
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: () => ({ fetch }),
|
||||
}));
|
||||
|
@ -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 (
|
||||
<EmptyListCTA
|
||||
title="You haven`t created any alert rules yet"
|
||||
|
@ -2,6 +2,7 @@ import { CallToActionCard } from '@grafana/ui';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import React, { FC } from 'react';
|
||||
import { getInstancesPermissions } from '../../utils/access-control';
|
||||
import { makeAMLink } from '../../utils/misc';
|
||||
|
||||
type Props = {
|
||||
@ -9,7 +10,9 @@ type Props = {
|
||||
};
|
||||
|
||||
export const NoSilencesSplash: FC<Props> = ({ alertManagerSourceName }) => {
|
||||
if (contextSrv.isEditor) {
|
||||
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||
|
||||
if (contextSrv.hasAccess(permissions.create, contextSrv.isEditor)) {
|
||||
return (
|
||||
<EmptyListCTA
|
||||
title="You haven't created any silences yet"
|
||||
|
@ -8,93 +8,93 @@ function getRulesSourceType(alertManagerSourceName: string): RulesSourceType {
|
||||
return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
|
||||
}
|
||||
|
||||
const instancesPermissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingInstanceRead,
|
||||
external: AccessControlAction.AlertingInstancesExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingInstanceCreate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
const notificationsPermissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingNotificationsRead,
|
||||
external: AccessControlAction.AlertingNotificationsExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingNotificationsCreate,
|
||||
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingNotificationsUpdate,
|
||||
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingNotificationsDelete,
|
||||
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
const rulesPermissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingRuleRead,
|
||||
external: AccessControlAction.AlertingRuleExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingRuleCreate,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingRuleUpdate,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingRuleDelete,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
export function getInstancesPermissions(rulesSourceName: string) {
|
||||
const sourceType = getRulesSourceType(rulesSourceName);
|
||||
|
||||
const permissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingInstanceRead,
|
||||
external: AccessControlAction.AlertingInstancesExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingInstanceCreate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingInstanceUpdate,
|
||||
external: AccessControlAction.AlertingInstancesExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
read: permissions.read[sourceType],
|
||||
create: permissions.create[sourceType],
|
||||
update: permissions.update[sourceType],
|
||||
delete: permissions.delete[sourceType],
|
||||
read: instancesPermissions.read[sourceType],
|
||||
create: instancesPermissions.create[sourceType],
|
||||
update: instancesPermissions.update[sourceType],
|
||||
delete: instancesPermissions.delete[sourceType],
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotificationsPermissions(rulesSourceName: string) {
|
||||
const sourceType = getRulesSourceType(rulesSourceName);
|
||||
|
||||
const permissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingNotificationsRead,
|
||||
external: AccessControlAction.AlertingNotificationsExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingNotificationsCreate,
|
||||
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingNotificationsUpdate,
|
||||
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingNotificationsDelete,
|
||||
external: AccessControlAction.AlertingNotificationsExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
read: permissions.read[sourceType],
|
||||
create: permissions.create[sourceType],
|
||||
update: permissions.update[sourceType],
|
||||
delete: permissions.delete[sourceType],
|
||||
read: notificationsPermissions.read[sourceType],
|
||||
create: notificationsPermissions.create[sourceType],
|
||||
update: notificationsPermissions.update[sourceType],
|
||||
delete: notificationsPermissions.delete[sourceType],
|
||||
};
|
||||
}
|
||||
|
||||
export function getRulesPermissions(rulesSourceName: string) {
|
||||
const sourceType = getRulesSourceType(rulesSourceName);
|
||||
|
||||
const permissions = {
|
||||
read: {
|
||||
grafana: AccessControlAction.AlertingRuleRead,
|
||||
external: AccessControlAction.AlertingRuleExternalRead,
|
||||
},
|
||||
create: {
|
||||
grafana: AccessControlAction.AlertingRuleCreate,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
update: {
|
||||
grafana: AccessControlAction.AlertingRuleUpdate,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
delete: {
|
||||
grafana: AccessControlAction.AlertingRuleDelete,
|
||||
external: AccessControlAction.AlertingRuleExternalWrite,
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
read: permissions.read[sourceType],
|
||||
create: permissions.create[sourceType],
|
||||
update: permissions.update[sourceType],
|
||||
delete: permissions.delete[sourceType],
|
||||
read: rulesPermissions.read[sourceType],
|
||||
create: rulesPermissions.create[sourceType],
|
||||
update: rulesPermissions.update[sourceType],
|
||||
delete: rulesPermissions.delete[sourceType],
|
||||
};
|
||||
}
|
||||
|
||||
@ -103,3 +103,14 @@ export function evaluateAccess(actions: AccessControlAction[], fallBackUserRoles
|
||||
return contextSrv.evaluatePermission(() => 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),
|
||||
};
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getRulesAccess } from './access-control';
|
||||
|
||||
export function useRulesAccess() {
|
||||
return useMemo(() => getRulesAccess(), []);
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user