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}
-
- ) : (
-
- {buttonLabel}
-
- )}
+ {showButton &&
+ (href ? (
+
+ {buttonLabel}
+
+ ) : (
+
+ {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 && (
-
- Edit
-
+
+
+ Edit
+
+
)}
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 && (
- {
- setSubroutes((subroutes) => [...subroutes, emptyRoute]);
- setIsAddMode(true);
- }}
- variant="secondary"
- type="button"
- >
- Add nested policy
-
+
+ {
+ setSubroutes((subroutes) => [...subroutes, emptyRoute]);
+ setIsAddMode(true);
+ }}
+ variant="secondary"
+ type="button"
+ >
+ Add nested policy
+
+
)}
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 && (
-
-
- New policy
-
-
+
+
+
+ New policy
+
+
+
)}
= ({
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 }) => {
Template
- Actions
+
+ Actions
+
@@ -68,17 +75,27 @@ export const TemplatesTable: FC = ({ config, alertManagerName }) => {
/>
{name}
-
-
- setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" />
-
+
+
+
+
+
+
+ setTemplateToDelete(name)}
+ tooltip="delete template"
+ icon="trash-alt"
+ />
+
+
+
{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 && (
<>
-
+
@@ -178,15 +170,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
function useColumns(alertManagerSourceName: string) {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
- const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
+ const permissions = getInstancesPermissions(alertManagerSourceName);
return useMemo((): SilenceTableColumnProps[] => {
const handleExpireSilenceClick = (id: string) => {
dispatch(expireSilenceAction(alertManagerSourceName, id));
};
- const showActions = contextSrv.hasAccess(
- isExternalAM ? AccessControlAction.AlertingInstancesExternalWrite : AccessControlAction.AlertingInstanceUpdate,
- contextSrv.isEditor
- );
+ const showActions = contextSrv.hasAccess(permissions.update, contextSrv.isEditor);
const columns: SilenceTableColumnProps[] = [
{
id: 'state',
@@ -262,7 +251,7 @@ function useColumns(alertManagerSourceName: string) {
});
}
return columns;
- }, [alertManagerSourceName, dispatch, styles, isExternalAM]);
+ }, [alertManagerSourceName, dispatch, styles, permissions]);
}
export default SilencesTable;
diff --git a/public/app/features/alerting/unified/utils/access-control.ts b/public/app/features/alerting/unified/utils/access-control.ts
new file mode 100644
index 00000000000..7d18ec3c7fb
--- /dev/null
+++ b/public/app/features/alerting/unified/utils/access-control.ts
@@ -0,0 +1,78 @@
+import { AccessControlAction } from 'app/types';
+import { isGrafanaRulesSource } from './datasource';
+import { contextSrv } from 'app/core/services/context_srv';
+
+function getAMversion(alertManagerSourceName: string) {
+ return isGrafanaRulesSource(alertManagerSourceName) ? 'grafana' : 'external';
+}
+
+export function getInstancesPermissions(alertManagerSourceName: string) {
+ const amVersion = getAMversion(alertManagerSourceName);
+
+ 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,
+ },
+ viewSource: {
+ grafana: AccessControlAction.AlertingInstanceRead,
+ external: AccessControlAction.DataSourcesExplore,
+ },
+ };
+
+ return {
+ read: permissions.read[amVersion],
+ create: permissions.create[amVersion],
+ update: permissions.update[amVersion],
+ delete: permissions.delete[amVersion],
+ viewSource: permissions.viewSource[amVersion],
+ };
+}
+
+export function getNotificationsPermissions(alertManagerSourceName: string) {
+ const amVersion = getAMversion(alertManagerSourceName);
+
+ 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[amVersion],
+ create: permissions.create[amVersion],
+ update: permissions.update[amVersion],
+ delete: permissions.delete[amVersion],
+ };
+}
+
+export function evaluateAccess(actions: AccessControlAction[], fallBackUserRoles: string[]) {
+ return () => {
+ return contextSrv.evaluatePermission(() => fallBackUserRoles, actions);
+ };
+}