Alerting: Fix some templates RBAC UI action control/checks (#94898)
This commit is contained in:
@@ -435,12 +435,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
ac.EvalPermission(ac.ActionAlertingReceiversRead),
|
||||
ac.EvalPermission(ac.ActionAlertingReceiversReadSecrets),
|
||||
ac.EvalPermission(ac.ActionAlertingReceiversCreate),
|
||||
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesRead),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesWrite),
|
||||
ac.EvalPermission(ac.ActionAlertingNotificationsTemplatesDelete),
|
||||
)
|
||||
}
|
||||
|
||||
if hasAccess(ac.EvalAny(contactPointsPerms...)) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications",
|
||||
Text: "Contact points", SubTitle: "Choose how to notify your contact points when an alert instance fires", Id: "receivers", Url: s.cfg.AppSubURL + "/alerting/notifications",
|
||||
Icon: "comment-alt-share",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { GrafanaRouteComponent, RouteDescriptor } from 'app/core/navigation/type
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { PERMISSIONS_CONTACT_POINTS } from './unified/components/contact-points/permissions';
|
||||
import { PERMISSIONS_TEMPLATES } from './unified/components/templates/permissions';
|
||||
import { evaluateAccess } from './unified/utils/access-control';
|
||||
|
||||
export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
@@ -104,6 +105,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
...PERMISSIONS_CONTACT_POINTS,
|
||||
...PERMISSIONS_TEMPLATES,
|
||||
]),
|
||||
component: importAlertingComponent(
|
||||
() =>
|
||||
@@ -150,6 +152,7 @@ export function getAlertingRoutes(cfg = config): RouteDescriptor[] {
|
||||
roles: evaluateAccess([
|
||||
AccessControlAction.AlertingNotificationsRead,
|
||||
AccessControlAction.AlertingNotificationsExternalRead,
|
||||
...PERMISSIONS_TEMPLATES,
|
||||
]),
|
||||
component: importAlertingComponent(
|
||||
() => import(/* webpackChunkName: "Templates" */ 'app/features/alerting/unified/Templates')
|
||||
|
||||
@@ -164,31 +164,39 @@ const NotificationTemplatesTab = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const useTabQueryParam = () => {
|
||||
const useTabQueryParam = (defaultTab: ActiveTab) => {
|
||||
const [queryParams, setQueryParams] = useURLSearchParams();
|
||||
const param = useMemo(() => {
|
||||
const queryParam = queryParams.get('tab');
|
||||
|
||||
if (!queryParam || !Object.values(ActiveTab).map(String).includes(queryParam)) {
|
||||
return ActiveTab.ContactPoints;
|
||||
return defaultTab;
|
||||
}
|
||||
|
||||
return queryParam || ActiveTab.ContactPoints;
|
||||
}, [queryParams]);
|
||||
return queryParam || defaultTab;
|
||||
}, [defaultTab, queryParams]);
|
||||
|
||||
const setParam = (tab: ActiveTab) => setQueryParams({ tab });
|
||||
|
||||
return [param, setParam] as const;
|
||||
};
|
||||
|
||||
export const ContactPointsPageContents = () => {
|
||||
const { selectedAlertmanager } = useAlertmanager();
|
||||
const [activeTab, setActiveTab] = useTabQueryParam();
|
||||
const [, showContactPointsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewContactPoint);
|
||||
const [, showTemplatesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationTemplate);
|
||||
|
||||
// Depending on permissions, user may not have access to all tabs,
|
||||
// but we can default to picking the first one that they definitely _do_ have access to
|
||||
const defaultTab = [
|
||||
showContactPointsTab && ActiveTab.ContactPoints,
|
||||
showTemplatesTab && ActiveTab.NotificationTemplates,
|
||||
].filter((tab) => !!tab)[0];
|
||||
|
||||
const [activeTab, setActiveTab] = useTabQueryParam(defaultTab);
|
||||
|
||||
const { contactPoints } = useContactPointsWithStatus({
|
||||
alertmanager: selectedAlertmanager!,
|
||||
});
|
||||
const [_, showTemplatesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationTemplate);
|
||||
|
||||
const showingContactPoints = activeTab === ActiveTab.ContactPoints;
|
||||
const showNotificationTemplates = activeTab === ActiveTab.NotificationTemplates;
|
||||
@@ -198,12 +206,14 @@ export const ContactPointsPageContents = () => {
|
||||
<GrafanaAlertmanagerDeliveryWarning currentAlertmanager={selectedAlertmanager!} />
|
||||
<Stack direction="column">
|
||||
<TabsBar>
|
||||
<Tab
|
||||
label="Contact Points"
|
||||
active={showingContactPoints}
|
||||
counter={contactPoints.length}
|
||||
onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)}
|
||||
/>
|
||||
{showContactPointsTab && (
|
||||
<Tab
|
||||
label="Contact Points"
|
||||
active={showingContactPoints}
|
||||
counter={contactPoints.length}
|
||||
onChangeTab={() => setActiveTab(ActiveTab.ContactPoints)}
|
||||
/>
|
||||
)}
|
||||
{showTemplatesTab && (
|
||||
<Tab
|
||||
label="Notification Templates"
|
||||
|
||||
@@ -17,9 +17,5 @@ export const PERMISSIONS_CONTACT_POINTS_MODIFY = [
|
||||
|
||||
/**
|
||||
* List of all permissions that allow contact points read/write functionality
|
||||
*
|
||||
* Any permission in this list will also be checked for whether the built-in Grafana Alertmanager is shown
|
||||
* (as the implication is that if they have one of these permissions, then they should be able to see Grafana AM in the AM selector)
|
||||
*/
|
||||
|
||||
export const PERMISSIONS_CONTACT_POINTS = [...PERMISSIONS_CONTACT_POINTS_READ, ...PERMISSIONS_CONTACT_POINTS_MODIFY];
|
||||
|
||||
@@ -166,7 +166,7 @@ function TemplateRow({ notificationTemplate, idx, alertManagerName, onDeleteClic
|
||||
/>
|
||||
</Authorize>
|
||||
)}
|
||||
<Authorize actions={[AlertmanagerAction.CreateContactPoint]}>
|
||||
<Authorize actions={[AlertmanagerAction.CreateNotificationTemplate]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(uid)}/duplicate`,
|
||||
|
||||
+17
-2
@@ -1,5 +1,8 @@
|
||||
import 'core-js/stable/structured-clone';
|
||||
import { MemoryHistoryBuildOptions } from 'history';
|
||||
import { ComponentProps, ReactNode } from 'react';
|
||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { screen, waitFor } from 'test/test-utils';
|
||||
import { render, screen, waitFor } from 'test/test-utils';
|
||||
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
@@ -8,6 +11,7 @@ import {
|
||||
setOnCallFeatures,
|
||||
setOnCallIntegrations,
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/plugins/configure-plugins';
|
||||
import { AlertmanagerProvider } from 'app/features/alerting/unified/state/AlertmanagerContext';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@@ -15,10 +19,21 @@ import { AccessControlAction } from 'app/types';
|
||||
import { AlertmanagerConfigBuilder, setupMswServer } from '../../../mockApi';
|
||||
import { grantUserPermissions } from '../../../mocks';
|
||||
import { captureRequests } from '../../../mocks/server/events';
|
||||
import { renderWithProvider } from '../../contact-points/ContactPoints.test';
|
||||
|
||||
import { GrafanaReceiverForm } from './GrafanaReceiverForm';
|
||||
|
||||
const renderWithProvider = (
|
||||
children: ReactNode,
|
||||
historyOptions?: MemoryHistoryBuildOptions,
|
||||
providerProps?: Partial<ComponentProps<typeof AlertmanagerProvider>>
|
||||
) =>
|
||||
render(
|
||||
<AlertmanagerProvider accessType="notification" {...providerProps}>
|
||||
{children}
|
||||
</AlertmanagerProvider>,
|
||||
{ historyOptions }
|
||||
);
|
||||
|
||||
setupMswServer();
|
||||
|
||||
const ui = {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
/**
|
||||
* List of all permissions that allow templates read/write functionality
|
||||
*/
|
||||
|
||||
export const PERMISSIONS_TEMPLATES = [
|
||||
AccessControlAction.AlertingTemplatesRead,
|
||||
AccessControlAction.AlertingTemplatesWrite,
|
||||
AccessControlAction.AlertingTemplatesDelete,
|
||||
];
|
||||
@@ -231,17 +231,41 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
|
||||
// TODO: Move this into the permissions config and generalise that code to allow for an array of permissions
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversCreate : null
|
||||
),
|
||||
[AlertmanagerAction.ViewContactPoint]: toAbility(AlwaysSupported, notificationsPermissions.read),
|
||||
[AlertmanagerAction.UpdateContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
|
||||
[AlertmanagerAction.DeleteContactPoint]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
|
||||
[AlertmanagerAction.ViewContactPoint]: toAbility(
|
||||
AlwaysSupported,
|
||||
notificationsPermissions.read,
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversRead : null
|
||||
),
|
||||
[AlertmanagerAction.UpdateContactPoint]: toAbility(
|
||||
hasConfigurationAPI,
|
||||
notificationsPermissions.update,
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
|
||||
),
|
||||
[AlertmanagerAction.DeleteContactPoint]: toAbility(
|
||||
hasConfigurationAPI,
|
||||
notificationsPermissions.delete,
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingReceiversWrite : null
|
||||
),
|
||||
// At the time of writing, only Grafana flavored alertmanager supports exporting,
|
||||
// and if a user can view the contact point, then they can also export it
|
||||
// So the only check we make is if the alertmanager is Grafana flavored
|
||||
[AlertmanagerAction.ExportContactPoint]: [isGrafanaFlavoredAlertmanager, isGrafanaFlavoredAlertmanager],
|
||||
// -- notification templates --
|
||||
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
|
||||
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(AlwaysSupported, notificationsPermissions.read),
|
||||
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.update),
|
||||
[AlertmanagerAction.CreateNotificationTemplate]: toAbility(
|
||||
hasConfigurationAPI,
|
||||
notificationsPermissions.create,
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
|
||||
),
|
||||
[AlertmanagerAction.ViewNotificationTemplate]: toAbility(
|
||||
AlwaysSupported,
|
||||
notificationsPermissions.read,
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesRead : null
|
||||
),
|
||||
[AlertmanagerAction.UpdateNotificationTemplate]: toAbility(
|
||||
hasConfigurationAPI,
|
||||
notificationsPermissions.update,
|
||||
isGrafanaFlavoredAlertmanager ? AccessControlAction.AlertingTemplatesWrite : null
|
||||
),
|
||||
[AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
|
||||
// -- notification policies --
|
||||
[AlertmanagerAction.CreateNotificationPolicy]: toAbility(hasConfigurationAPI, notificationsPermissions.create),
|
||||
|
||||
@@ -12,6 +12,7 @@ import { PromApplication, RulesSourceApplication } from 'app/types/unified-alert
|
||||
|
||||
import { alertmanagerApi } from '../api/alertmanagerApi';
|
||||
import { PERMISSIONS_CONTACT_POINTS } from '../components/contact-points/permissions';
|
||||
import { PERMISSIONS_TEMPLATES } from '../components/templates/permissions';
|
||||
import { useAlertManagersByPermission } from '../hooks/useAlertManagerSources';
|
||||
import { isAlertManagerWithConfigAPI } from '../state/AlertmanagerContext';
|
||||
|
||||
@@ -151,6 +152,7 @@ export function getAlertManagerDataSourcesByPermission(permission: 'instance' |
|
||||
const builtinAlertmanagerPermissions = [
|
||||
...Object.values(permissions).flatMap((permissions) => permissions.grafana),
|
||||
...PERMISSIONS_CONTACT_POINTS,
|
||||
...PERMISSIONS_TEMPLATES,
|
||||
];
|
||||
|
||||
const hasPermissionsForInternalAlertmanager = builtinAlertmanagerPermissions.some((permission) =>
|
||||
|
||||
@@ -134,6 +134,11 @@ export enum AccessControlAction {
|
||||
AlertingReceiversWrite = 'alert.notifications.receivers:write',
|
||||
AlertingReceiversRead = 'alert.notifications.receivers:read',
|
||||
|
||||
// Alerting templates actions
|
||||
AlertingTemplatesRead = 'alert.notifications.templates:read',
|
||||
AlertingTemplatesWrite = 'alert.notifications.templates:write',
|
||||
AlertingTemplatesDelete = 'alert.notifications.templates:delete',
|
||||
|
||||
ActionAPIKeysRead = 'apikeys:read',
|
||||
ActionAPIKeysCreate = 'apikeys:create',
|
||||
ActionAPIKeysDelete = 'apikeys:delete',
|
||||
|
||||
Reference in New Issue
Block a user