diff --git a/pkg/api/api.go b/pkg/api/api.go index 16af1f809c6..12c7dbd4a8c 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/web" ) var plog = log.New("api") @@ -417,7 +418,14 @@ func (hs *HTTPServer) registerRoutes() { alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard)) }) - apiRoute.Get("/alert-notifiers", reqEditorRole, routing.Wrap( + var notifiersAuthHandler web.Handler + if hs.Cfg.UnifiedAlerting.IsEnabled() { + notifiersAuthHandler = reqSignedIn + } else { + notifiersAuthHandler = reqEditorRole + } + + apiRoute.Get("/alert-notifiers", notifiersAuthHandler, routing.Wrap( hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())), ) diff --git a/public/app/features/alerting/routes.tsx b/public/app/features/alerting/routes.tsx index b7a90c80d4a..1e64912cc57 100644 --- a/public/app/features/alerting/routes.tsx +++ b/public/app/features/alerting/routes.tsx @@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types'; import { uniq } from 'lodash'; import { contextSrv } from 'app/core/core'; import { AccessControlAction } from 'app/types'; +import { evaluateAccess } from './unified/utils/access-control'; const commonRoutes: RouteDescriptor[] = [ { @@ -95,84 +96,118 @@ const unifiedRoutes: RouteDescriptor[] = [ }, { path: '/alerting/routes', - roles: () => ['Admin', 'Editor'], + roles: () => + contextSrv.evaluatePermission(config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : () => [], [ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsExternalRead, + ]), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes') ), }, { path: '/alerting/routes/mute-timing/new', - roles: () => ['Admin', 'Editor'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') ), }, { path: '/alerting/routes/mute-timing/edit', - roles: () => ['Admin', 'Editor'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') ), }, { path: '/alerting/silences', - roles: () => contextSrv.evaluatePermission(() => [], [AccessControlAction.AlertingInstanceRead]), + roles: evaluateAccess([AccessControlAction.AlertingInstanceRead], ['Editor', 'Admin']), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') ), }, { path: '/alerting/silence/new', - roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceCreate]), + roles: evaluateAccess( + [AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstancesExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') ), }, { path: '/alerting/silence/:id/edit', - roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceUpdate]), + roles: evaluateAccess( + [AccessControlAction.AlertingInstanceUpdate, AccessControlAction.AlertingInstancesExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') ), }, { path: '/alerting/notifications', - roles: config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : undefined, + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), }, { path: '/alerting/notifications/templates/new', - roles: () => ['Editor', 'Admin'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), }, { path: '/alerting/notifications/templates/:id/edit', - roles: () => ['Editor', 'Admin'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), }, { path: '/alerting/notifications/receivers/new', - roles: () => ['Editor', 'Admin'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), }, { path: '/alerting/notifications/receivers/:id/edit', - roles: () => ['Editor', 'Admin'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), }, { path: '/alerting/notifications/global-config', - roles: () => ['Admin', 'Editor'], + roles: evaluateAccess( + [AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite], + ['Editor', 'Admin'] + ), component: SafeDynamicImport( () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') ), diff --git a/public/app/features/alerting/unified/AmRoutes.test.tsx b/public/app/features/alerting/unified/AmRoutes.test.tsx index 01f339bbf66..85a561165ba 100644 --- a/public/app/features/alerting/unified/AmRoutes.test.tsx +++ b/public/app/features/alerting/unified/AmRoutes.test.tsx @@ -20,9 +20,12 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource'; import userEvent from '@testing-library/user-event'; import { selectOptionInTest } from '@grafana/ui'; import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; +import { contextSrv } from 'app/core/services/context_srv'; +import { AccessControlAction } from 'app/types'; jest.mock('./api/alertmanager'); jest.mock('./utils/config'); +jest.mock('app/core/services/context_srv'); const mocks = { getAllDataSourcesMock: jest.mocked(getAllDataSources), @@ -32,6 +35,7 @@ const mocks = { updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig), fetchStatus: jest.mocked(fetchStatus), }, + contextSrv: jest.mocked(contextSrv), }; const renderAmRoutes = (alertManagerSourceName?: string) => { @@ -177,6 +181,9 @@ describe('AmRoutes', () => { beforeEach(() => { mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources)); + mocks.contextSrv.hasAccess.mockImplementation(() => true); + mocks.contextSrv.hasPermission.mockImplementation(() => true); + mocks.contextSrv.evaluatePermission.mockImplementation(() => []); setDataSourceSrv(new MockDataSourceSrv(dataSources)); }); @@ -359,6 +366,18 @@ describe('AmRoutes', () => { }); }); + it('hides create and edit button if user does not have permission', () => { + mocks.contextSrv.hasAccess.mockImplementation((action) => + [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsRead].includes( + action as AccessControlAction + ) + ); + + renderAmRoutes(); + expect(ui.newPolicyButton.query()).not.toBeInTheDocument(); + expect(ui.editButton.query()).not.toBeInTheDocument(); + }); + it('Show error message if loading Alertmanager config fails', async () => { mocks.api.fetchAlertManagerConfig.mockRejectedValue({ status: 500, diff --git a/public/app/features/alerting/unified/AmRoutes.tsx b/public/app/features/alerting/unified/AmRoutes.tsx index 43c09b1d4e2..8bcdaa7e184 100644 --- a/public/app/features/alerting/unified/AmRoutes.tsx +++ b/public/app/features/alerting/unified/AmRoutes.tsx @@ -122,6 +122,7 @@ const AmRoutes: FC = () => { />
{ @@ -125,8 +128,23 @@ describe('Receivers', () => { mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); setDataSourceSrv(new MockDataSourceSrv(dataSources)); - contextSrv.isEditor = true; + mocks.contextSrv.isEditor = true; store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); + + mocks.contextSrv.evaluatePermission.mockImplementation(() => []); + mocks.contextSrv.hasPermission.mockImplementation((action) => { + const permissions = [ + AccessControlAction.AlertingNotificationsRead, + AccessControlAction.AlertingNotificationsCreate, + AccessControlAction.AlertingNotificationsUpdate, + AccessControlAction.AlertingNotificationsDelete, + AccessControlAction.AlertingNotificationsExternalRead, + AccessControlAction.AlertingNotificationsExternalWrite, + ]; + return permissions.includes(action as AccessControlAction); + }); + + mocks.contextSrv.hasAccess.mockImplementation(() => true); }); it('Template and receiver tables are rendered, alertmanager can be selected', async () => { @@ -295,6 +313,19 @@ describe('Receivers', () => { }); }); + it('Hides create contact point button for users without permission', () => { + mocks.api.fetchConfig.mockResolvedValue(someGrafanaAlertManagerConfig); + mocks.api.updateConfig.mockResolvedValue(); + mocks.contextSrv.hasAccess.mockImplementation((action) => + [AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead].some( + (a) => a === action + ) + ); + renderReceivers(); + + expect(ui.newContactPointButton.query()).not.toBeInTheDocument(); + }); + it('Cloud alertmanager receiver can be edited', async () => { mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig); mocks.api.updateConfig.mockResolvedValue(); diff --git a/public/app/features/alerting/unified/Receivers.tsx b/public/app/features/alerting/unified/Receivers.tsx index 02bee4161d8..cf503117041 100644 --- a/public/app/features/alerting/unified/Receivers.tsx +++ b/public/app/features/alerting/unified/Receivers.tsx @@ -41,7 +41,10 @@ const Receivers: FC = () => { }, [alertManagerSourceName, dispatch, shouldLoadConfig]); useEffect(() => { - if (alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && !(receiverTypes.result || receiverTypes.loading)) { + if ( + alertManagerSourceName === GRAFANA_RULES_SOURCE_NAME && + !(receiverTypes.result || receiverTypes.loading || receiverTypes.error) + ) { dispatch(fetchGrafanaNotifiersAction()); } }, [alertManagerSourceName, dispatch, receiverTypes]); diff --git a/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx b/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx index c507512ab07..a1c8d834104 100644 --- a/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx +++ b/public/app/features/alerting/unified/components/EmptyAreaWithCTA.tsx @@ -13,6 +13,7 @@ export interface EmptyAreaWithCTAProps { buttonIcon?: IconName; buttonSize?: 'xs' | 'sm' | 'md' | 'lg'; buttonVariant?: ButtonVariant; + showButton?: boolean; } export const EmptyAreaWithCTA: FC = ({ @@ -23,6 +24,7 @@ export const EmptyAreaWithCTA: FC = ({ onButtonClick, text, href, + showButton = true, }) => { const styles = useStyles(getStyles); @@ -37,15 +39,16 @@ export const EmptyAreaWithCTA: FC = ({ <>

{text}

- {href ? ( - - {buttonLabel} - - ) : ( - - )} + {showButton && + (href ? ( + + {buttonLabel} + + ) : ( + + ))}
); diff --git a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx index e7867add2ba..53818045141 100644 --- a/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx +++ b/public/app/features/alerting/unified/components/alert-groups/AlertDetails.tsx @@ -3,12 +3,11 @@ import { GrafanaTheme2 } from '@grafana/data'; import { LinkButton, useStyles2 } from '@grafana/ui'; import { contextSrv } from 'app/core/services/context_srv'; import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types'; -import { AccessControlAction } from 'app/types'; import React, { FC } from 'react'; -import { isGrafanaRulesSource } from '../../utils/datasource'; import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc'; import { AnnotationDetailsField } from '../AnnotationDetailsField'; import { Authorize } from '../Authorize'; +import { getInstancesPermissions } from '../../utils/access-control'; interface AmNotificationsAlertDetailsProps { alertManagerSourceName: string; @@ -17,18 +16,11 @@ interface AmNotificationsAlertDetailsProps { export const AlertDetails: FC = ({ alert, alertManagerSourceName }) => { const styles = useStyles2(getStyles); - const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName); + const permissions = getInstancesPermissions(alertManagerSourceName); return ( <>
- + {alert.status.state === AlertState.Suppressed && ( = ({ alert, aler )} - + {alert.generatorURL && ( See source diff --git a/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx b/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx index e248d4e2609..e3c1a7d7299 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRootRoute.tsx @@ -6,7 +6,8 @@ import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes'; import { AmRootRouteForm } from './AmRootRouteForm'; import { AmRootRouteRead } from './AmRootRouteRead'; import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; - +import { Authorize } from '../../components/Authorize'; +import { getNotificationsPermissions } from '../../utils/access-control'; export interface AmRootRouteProps { isEditMode: boolean; onEnterEditMode: () => void; @@ -28,6 +29,7 @@ export const AmRootRoute: FC = ({ }) => { const styles = useStyles2(getStyles); + const permissions = getNotificationsPermissions(alertManagerSourceName); const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName); return ( @@ -37,9 +39,11 @@ export const AmRootRoute: FC = ({ Root policy - default for all alerts {!isEditMode && !isReadOnly && ( - + + + )}

diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx index 04d3431e5b2..1ea268ae584 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesExpandedRead.tsx @@ -7,13 +7,15 @@ import { emptyRoute } from '../../utils/amroutes'; import { AmRoutesTable } from './AmRoutesTable'; import { getGridStyles } from './gridStyles'; import { MuteTimingsTable } from './MuteTimingsTable'; -import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName'; +import { Authorize } from '../Authorize'; +import { getNotificationsPermissions } from '../../utils/access-control'; export interface AmRoutesExpandedReadProps { onChange: (routes: FormAmRoute) => void; receivers: AmRouteReceiver[]; routes: FormAmRoute; readOnly?: boolean; + alertManagerSourceName: string; } export const AmRoutesExpandedRead: FC = ({ @@ -21,10 +23,11 @@ export const AmRoutesExpandedRead: FC = ({ receivers, routes, readOnly = false, + alertManagerSourceName, }) => { const styles = useStyles2(getStyles); const gridStyles = useStyles2(getGridStyles); - const [alertManagerSourceName] = useAlertManagerSourceName(); + const permissions = getNotificationsPermissions(alertManagerSourceName); const groupWait = routes.groupWaitValue ? `${routes.groupWaitValue}${routes.groupWaitValueType}` : '-'; const groupInterval = routes.groupIntervalValue @@ -71,23 +74,26 @@ export const AmRoutesExpandedRead: FC = ({ }} receivers={receivers} routes={subroutes} + alertManagerSourceName={alertManagerSourceName} /> ) : (

No nested policies configured.

)} {!isAddMode && !readOnly && ( - + + + )}
Mute timings
diff --git a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx index e5cb1df5d46..c25f812d57a 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmRoutesTable.tsx @@ -9,6 +9,8 @@ import { Matchers } from '../silences/Matchers'; import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager'; import { intersectionWith, isEqual } from 'lodash'; import { EmptyArea } from '../EmptyArea'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getNotificationsPermissions } from '../../utils/access-control'; export interface AmRoutesTableProps { isAddMode: boolean; @@ -18,6 +20,7 @@ export interface AmRoutesTableProps { routes: FormAmRoute[]; filters?: { queryString?: string; contactPoint?: string }; readOnly?: boolean; + alertManagerSourceName: string; } type RouteTableColumnProps = DynamicTableColumnProps; @@ -69,9 +72,15 @@ export const AmRoutesTable: FC = ({ routes, filters, readOnly = false, + alertManagerSourceName, }) => { const [editMode, setEditMode] = useState(false); const [expandedId, setExpandedId] = useState(); + const permissions = getNotificationsPermissions(alertManagerSourceName); + const canEditRoutes = contextSrv.hasPermission(permissions.update); + const canDeleteRoutes = contextSrv.hasPermission(permissions.delete); + + const showActions = !readOnly && (canEditRoutes || canDeleteRoutes); const expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []); const collapseItem = useCallback(() => setExpandedId(undefined), []); @@ -102,7 +111,7 @@ export const AmRoutesTable: FC = ({ renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-', size: 5, }, - ...(readOnly + ...(!showActions ? [] : [ { @@ -212,6 +221,7 @@ export const AmRoutesTable: FC = ({ receivers={receivers} routes={item.data} readOnly={readOnly} + alertManagerSourceName={alertManagerSourceName} /> ) } diff --git a/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx b/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx index c2acff52421..7fba03bfc12 100644 --- a/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx +++ b/public/app/features/alerting/unified/components/amroutes/AmSpecificRouting.tsx @@ -11,8 +11,12 @@ import { MatcherFilter } from '../alert-groups/MatcherFilter'; import { EmptyArea } from '../EmptyArea'; import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA'; import { AmRoutesTable } from './AmRoutesTable'; +import { Authorize } from '../../components/Authorize'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getNotificationsPermissions } from '../../utils/access-control'; export interface AmSpecificRoutingProps { + alertManagerSourceName: string; onChange: (routes: FormAmRoute) => void; onRootRouteEdit: () => void; receivers: AmRouteReceiver[]; @@ -26,6 +30,7 @@ interface Filters { } export const AmSpecificRouting: FC = ({ + alertManagerSourceName, onChange, onRootRouteEdit, receivers, @@ -34,6 +39,8 @@ export const AmSpecificRouting: FC = ({ }) => { const [actualRoutes, setActualRoutes] = useState([...routes.routes]); const [isAddMode, setIsAddMode] = useState(false); + const permissions = getNotificationsPermissions(alertManagerSourceName); + const canCreateNotifications = contextSrv.hasPermission(permissions.create); const [searchParams, setSearchParams] = useURLSearchParams(); const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams); @@ -97,6 +104,7 @@ export const AmSpecificRouting: FC = ({ buttonLabel="Set a default contact point" onButtonClick={onRootRouteEdit} text="You haven't set a default contact point for the root route yet." + showButton={canCreateNotifications} /> ) ) : actualRoutes.length > 0 ? ( @@ -132,11 +140,13 @@ export const AmSpecificRouting: FC = ({ )} {!isAddMode && !readOnly && ( -
- -
+ +
+ +
+
)} = ({ receivers={receivers} routes={actualRoutes} filters={{ queryString, contactPoint }} + alertManagerSourceName={alertManagerSourceName} /> ) : readOnly ? ( @@ -159,6 +170,7 @@ export const AmSpecificRouting: FC = ({ buttonLabel="New specific policy" onButtonClick={addNewRoute} text="You haven't created any specific policies yet." + showButton={canCreateNotifications} /> )} diff --git a/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx b/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx index 7ee43ea5b43..04497c62d28 100644 --- a/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx +++ b/public/app/features/alerting/unified/components/amroutes/MuteTimingsTable.tsx @@ -17,6 +17,9 @@ import { getYearsString, } from '../../utils/alertmanager'; import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA'; +import { Authorize } from '../../components/Authorize'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getNotificationsPermissions } from '../../utils/access-control'; interface Props { alertManagerSourceName: string; @@ -27,6 +30,7 @@ interface Props { export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTimingNames, hideActions }) => { const styles = useStyles2(getStyles); const dispatch = useDispatch(); + const permissions = getNotificationsPermissions(alertManagerSourceName); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); const [muteTimingName, setMuteTimingName] = useState(''); const { result }: AsyncRequestState = @@ -56,14 +60,16 @@ export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTiming

)} {!hideActions && items.length > 0 && ( - - New mute timing - + + + New mute timing + + )} {items.length > 0 ? ( @@ -74,6 +80,7 @@ export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTiming buttonIcon="plus" buttonSize="lg" href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)} + showButton={contextSrv.hasPermission(permissions.create)} /> ) : (

No mute timings configured

@@ -93,6 +100,11 @@ export const MuteTimingsTable: FC = ({ alertManagerSourceName, muteTiming }; function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) { + const permissions = getNotificationsPermissions(alertManagerSourceName); + + const userHasEditPermissions = contextSrv.hasPermission(permissions.update); + const userHasDeletePermissions = contextSrv.hasPermission(permissions.delete); + const showActions = !hideActions && (userHasEditPermissions || userHasDeletePermissions); return useMemo((): Array> => { const columns: Array> = [ { @@ -109,19 +121,29 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute renderCell: ({ data }) => renderTimeIntervals(data.time_intervals), }, ]; - if (!hideActions) { + if (showActions) { columns.push({ id: 'actions', label: 'Actions', renderCell: function renderActions({ data }) { return (
- - - - setMuteTimingName(data.name)} /> + + + + + + + setMuteTimingName(data.name)} + /> +
); }, @@ -129,7 +151,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute }); } return columns; - }, [alertManagerSourceName, hideActions, setMuteTimingName]); + }, [alertManagerSourceName, setMuteTimingName, showActions, permissions]); } function renderTimeIntervals(timeIntervals: TimeInterval[]) { diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx index 7a03daa5322..439b851212d 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversAndTemplatesView.tsx @@ -2,9 +2,11 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; +import { AccessControlAction } from 'app/types'; import React, { FC } from 'react'; import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; import { makeAMLink } from '../../utils/misc'; +import { Authorize } from '../Authorize'; import { ReceiversTable } from './ReceiversTable'; import { TemplatesTable } from './TemplatesTable'; @@ -22,15 +24,17 @@ export const ReceiversAndTemplatesView: FC = ({ config, alertManagerName {!isVanillaAM && } {isCloud && ( - -

- For each external Alertmanager you can define global settings, like server addresses, usernames and - password, for all the supported contact points. -

- - {isVanillaAM ? 'View global config' : 'Edit global config'} - -
+ + +

+ For each external Alertmanager you can define global settings, like server addresses, usernames and + password, for all the supported contact points. +

+ + {isVanillaAM ? 'View global config' : 'Edit global config'} + +
+
)} ); diff --git a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx index ba453192eb8..6a3b6761efa 100644 --- a/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/ReceiversTable.tsx @@ -13,6 +13,9 @@ import { isReceiverUsed } from '../../utils/alertmanager'; import { useDispatch } from 'react-redux'; import { deleteReceiverAction } from '../../state/actions'; import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; +import { Authorize } from '../../components/Authorize'; +import { contextSrv } from 'app/core/services/context_srv'; +import { getNotificationsPermissions } from '../../utils/access-control'; interface Props { config: AlertManagerCortexConfig; @@ -24,6 +27,7 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => { const tableStyles = useStyles2(getAlertTableStyles); const styles = useStyles2(getStyles); const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName); + const permissions = getNotificationsPermissions(alertManagerName); const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); // receiver name slated for deletion. If this is set, a confirmation modal is shown. If user approves, this receiver is deleted @@ -66,7 +70,7 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => { className={styles.section} title="Contact points" description="Define where the notifications will be sent to, for example email or Slack." - showButton={!isVanillaAM} + showButton={!isVanillaAM && contextSrv.hasPermission(permissions.create)} addButtonLabel="New contact point" addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)} > @@ -74,13 +78,17 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => { - + + + Contact point name Type - Actions + + Actions + @@ -93,38 +101,46 @@ export const ReceiversTable: FC = ({ config, alertManagerName }) => { {receiver.name} {receiver.types.join(', ')} - - {!isVanillaAM && ( - <> - - onClickDeleteReceiver(receiver.name)} - tooltip="Delete contact point" - icon="trash-alt" - /> - - )} - {isVanillaAM && ( - - )} - + + + {!isVanillaAM && ( + <> + + + + + onClickDeleteReceiver(receiver.name)} + tooltip="Delete contact point" + icon="trash-alt" + /> + + + )} + {isVanillaAM && ( + + + + )} + + ))} diff --git a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx index 48f9d83b7a3..b661c350a98 100644 --- a/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx +++ b/public/app/features/alerting/unified/components/receivers/TemplatesTable.tsx @@ -9,6 +9,9 @@ import { ReceiversSection } from './ReceiversSection'; import { makeAMLink } from '../../utils/misc'; import { useDispatch } from 'react-redux'; import { deleteTemplateAction } from '../../state/actions'; +import { contextSrv } from 'app/core/services/context_srv'; +import { Authorize } from '../../components/Authorize'; +import { getNotificationsPermissions } from '../../utils/access-control'; interface Props { config: AlertManagerCortexConfig; @@ -19,6 +22,7 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => { const dispatch = useDispatch(); const [expandedTemplates, setExpandedTemplates] = useState>({}); const tableStyles = useStyles2(getAlertTableStyles); + const permissions = getNotificationsPermissions(alertManagerName); const templateRows = useMemo(() => Object.entries(config.template_files), [config]); const [templateToDelete, setTemplateToDelete] = useState(); @@ -36,6 +40,7 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => { description="Templates construct the messages that get sent to the contact points." addButtonLabel="New template" addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)} + showButton={contextSrv.hasPermission(permissions.create)} > @@ -47,7 +52,9 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => { - + + + @@ -68,17 +75,27 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => { /> - + + + {isExpanded && ( diff --git a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx index 86ef755f6a7..b70fd472166 100644 --- a/public/app/features/alerting/unified/components/silences/SilencesTable.tsx +++ b/public/app/features/alerting/unified/components/silences/SilencesTable.tsx @@ -18,9 +18,8 @@ import { useDispatch } from 'react-redux'; import { expireSilenceAction } from '../../state/actions'; import { SilenceDetails } from './SilenceDetails'; import { Stack } from '@grafana/experimental'; -import { AccessControlAction } from '../../../../../types'; import { Authorize } from '../Authorize'; -import { isGrafanaRulesSource } from '../../utils/datasource'; +import { getInstancesPermissions } from '../../utils/access-control'; export interface SilenceTableItem extends Silence { silencedAlerts: AlertmanagerAlert[]; @@ -38,7 +37,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo const styles = useStyles2(getStyles); const [queryParams] = useQueryParams(); const filteredSilences = useFilteredSilences(silences); - const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName); + const permissions = getInstancesPermissions(alertManagerSourceName); const { silenceState } = getSilenceFiltersFromUrlParams(queryParams); @@ -65,14 +64,7 @@ const SilencesTable: FC = ({ silences, alertManagerAlerts, alertManagerSo {!!silences.length && ( <> - +
TemplateActionsActions
{name} - - setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" /> - + + + + + setTemplateToDelete(name)} + tooltip="delete template" + icon="trash-alt" + /> + +