diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 4441b75d658..2d64d9c6ade 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -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", }) } diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index bc41d7056c9..ef88f26d6c6 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -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') diff --git a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx index 7f89081d217..6366c421d9a 100644 --- a/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/ContactPoints.tsx @@ -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 = () => { - setActiveTab(ActiveTab.ContactPoints)} - /> + {showContactPointsTab && ( + setActiveTab(ActiveTab.ContactPoints)} + /> + )} {showTemplatesTab && ( )} - + > +) => + render( + + {children} + , + { historyOptions } + ); + setupMswServer(); const ui = { diff --git a/public/app/features/alerting/unified/components/templates/permissions.ts b/public/app/features/alerting/unified/components/templates/permissions.ts new file mode 100644 index 00000000000..a92758f1a1d --- /dev/null +++ b/public/app/features/alerting/unified/components/templates/permissions.ts @@ -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, +]; diff --git a/public/app/features/alerting/unified/hooks/useAbilities.ts b/public/app/features/alerting/unified/hooks/useAbilities.ts index dac04eb248a..c60acd5e7f2 100644 --- a/public/app/features/alerting/unified/hooks/useAbilities.ts +++ b/public/app/features/alerting/unified/hooks/useAbilities.ts @@ -231,17 +231,41 @@ export function useAllAlertmanagerAbilities(): Abilities { // 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), diff --git a/public/app/features/alerting/unified/utils/datasource.ts b/public/app/features/alerting/unified/utils/datasource.ts index 764de417bfb..64275f04169 100644 --- a/public/app/features/alerting/unified/utils/datasource.ts +++ b/public/app/features/alerting/unified/utils/datasource.ts @@ -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) => diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index de2c79b0362..01f5b4e1f6b 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -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',