Alerting: fgac for notification policies and contact points (#46939)

* add FGAC actions for silences table

* redirect users without permissions

* add permissions checks to routes

* add fgac to notifications and contact points

* fgac for notification policies

* fix mute timing authorization

* use consistent naming for checking grafana alertmanager

* tests for fgac in contact points and notification policies

* bump up timeout on rule editor test

* use new permissions util

* break out route evaluation into util

* Remove test timeout

* Change permissions for the alert-notifiers endpoint

* Use signed in handler for alert-notifiers when unified alerting enabled

Co-authored-by: Konrad Lalik <konrad.lalik@grafana.com>
This commit is contained in:
Nathan Rodman
2022-04-06 09:24:33 -07:00
committed by GitHub
parent 460b8e85d7
commit 49505b9a3b
18 changed files with 399 additions and 151 deletions

View File

@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/web"
) )
var plog = log.New("api") var plog = log.New("api")
@@ -417,7 +418,14 @@ func (hs *HTTPServer) registerRoutes() {
alertsRoute.Get("/states-for-dashboard", routing.Wrap(hs.GetAlertStatesForDashboard)) 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())), hs.GetAlertNotifiers(hs.Cfg.UnifiedAlerting.IsEnabled())),
) )

View File

@@ -6,6 +6,7 @@ import { RouteDescriptor } from 'app/core/navigation/types';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import { contextSrv } from 'app/core/core'; import { contextSrv } from 'app/core/core';
import { AccessControlAction } from 'app/types'; import { AccessControlAction } from 'app/types';
import { evaluateAccess } from './unified/utils/access-control';
const commonRoutes: RouteDescriptor[] = [ const commonRoutes: RouteDescriptor[] = [
{ {
@@ -95,84 +96,118 @@ const unifiedRoutes: RouteDescriptor[] = [
}, },
{ {
path: '/alerting/routes', path: '/alerting/routes',
roles: () => ['Admin', 'Editor'], roles: () =>
contextSrv.evaluatePermission(config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : () => [], [
AccessControlAction.AlertingNotificationsRead,
AccessControlAction.AlertingNotificationsExternalRead,
]),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes') () => import(/* webpackChunkName: "AlertAmRoutes" */ 'app/features/alerting/unified/AmRoutes')
), ),
}, },
{ {
path: '/alerting/routes/mute-timing/new', path: '/alerting/routes/mute-timing/new',
roles: () => ['Admin', 'Editor'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
), ),
}, },
{ {
path: '/alerting/routes/mute-timing/edit', path: '/alerting/routes/mute-timing/edit',
roles: () => ['Admin', 'Editor'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings') () => import(/* webpackChunkName: "MuteTimings" */ 'app/features/alerting/unified/MuteTimings')
), ),
}, },
{ {
path: '/alerting/silences', path: '/alerting/silences',
roles: () => contextSrv.evaluatePermission(() => [], [AccessControlAction.AlertingInstanceRead]), roles: evaluateAccess([AccessControlAction.AlertingInstanceRead], ['Editor', 'Admin']),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
), ),
}, },
{ {
path: '/alerting/silence/new', path: '/alerting/silence/new',
roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceCreate]), roles: evaluateAccess(
[AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstancesExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
), ),
}, },
{ {
path: '/alerting/silence/:id/edit', path: '/alerting/silence/:id/edit',
roles: () => contextSrv.evaluatePermission(() => ['Editor', 'Admin'], [AccessControlAction.AlertingInstanceUpdate]), roles: evaluateAccess(
[AccessControlAction.AlertingInstanceUpdate, AccessControlAction.AlertingInstancesExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences') () => import(/* webpackChunkName: "AlertSilences" */ 'app/features/alerting/unified/Silences')
), ),
}, },
{ {
path: '/alerting/notifications', path: '/alerting/notifications',
roles: config.unifiedAlertingEnabled ? () => ['Editor', 'Admin'] : undefined, roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsExternalRead],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
), ),
}, },
{ {
path: '/alerting/notifications/templates/new', path: '/alerting/notifications/templates/new',
roles: () => ['Editor', 'Admin'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
), ),
}, },
{ {
path: '/alerting/notifications/templates/:id/edit', path: '/alerting/notifications/templates/:id/edit',
roles: () => ['Editor', 'Admin'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
), ),
}, },
{ {
path: '/alerting/notifications/receivers/new', path: '/alerting/notifications/receivers/new',
roles: () => ['Editor', 'Admin'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsCreate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
), ),
}, },
{ {
path: '/alerting/notifications/receivers/:id/edit', path: '/alerting/notifications/receivers/:id/edit',
roles: () => ['Editor', 'Admin'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
), ),
}, },
{ {
path: '/alerting/notifications/global-config', path: '/alerting/notifications/global-config',
roles: () => ['Admin', 'Editor'], roles: evaluateAccess(
[AccessControlAction.AlertingNotificationsUpdate, AccessControlAction.AlertingNotificationsExternalWrite],
['Editor', 'Admin']
),
component: SafeDynamicImport( component: SafeDynamicImport(
() => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers') () => import(/* webpackChunkName: "NotificationsListPage" */ 'app/features/alerting/unified/Receivers')
), ),

View File

@@ -20,9 +20,12 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui'; import { selectOptionInTest } from '@grafana/ui';
import { ALERTMANAGER_NAME_QUERY_KEY } from './utils/constants'; 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('./api/alertmanager');
jest.mock('./utils/config'); jest.mock('./utils/config');
jest.mock('app/core/services/context_srv');
const mocks = { const mocks = {
getAllDataSourcesMock: jest.mocked(getAllDataSources), getAllDataSourcesMock: jest.mocked(getAllDataSources),
@@ -32,6 +35,7 @@ const mocks = {
updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig), updateAlertManagerConfig: jest.mocked(updateAlertManagerConfig),
fetchStatus: jest.mocked(fetchStatus), fetchStatus: jest.mocked(fetchStatus),
}, },
contextSrv: jest.mocked(contextSrv),
}; };
const renderAmRoutes = (alertManagerSourceName?: string) => { const renderAmRoutes = (alertManagerSourceName?: string) => {
@@ -177,6 +181,9 @@ describe('AmRoutes', () => {
beforeEach(() => { beforeEach(() => {
mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources)); mocks.getAllDataSourcesMock.mockReturnValue(Object.values(dataSources));
mocks.contextSrv.hasAccess.mockImplementation(() => true);
mocks.contextSrv.hasPermission.mockImplementation(() => true);
mocks.contextSrv.evaluatePermission.mockImplementation(() => []);
setDataSourceSrv(new MockDataSourceSrv(dataSources)); 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 () => { it('Show error message if loading Alertmanager config fails', async () => {
mocks.api.fetchAlertManagerConfig.mockRejectedValue({ mocks.api.fetchAlertManagerConfig.mockRejectedValue({
status: 500, status: 500,

View File

@@ -122,6 +122,7 @@ const AmRoutes: FC = () => {
/> />
<div className={styles.break} /> <div className={styles.break} />
<AmSpecificRouting <AmSpecificRouting
alertManagerSourceName={alertManagerSourceName}
onChange={handleSave} onChange={handleSave}
readOnly={readOnly} readOnly={readOnly}
onRootRouteEdit={enterRootRouteEditMode} onRootRouteEdit={enterRootRouteEditMode}

View File

@@ -25,10 +25,12 @@ import { contextSrv } from 'app/core/services/context_srv';
import { selectOptionInTest } from '@grafana/ui'; import { selectOptionInTest } from '@grafana/ui';
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types'; import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks'; import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
import { AccessControlAction } from 'app/types';
jest.mock('./api/alertmanager'); jest.mock('./api/alertmanager');
jest.mock('./api/grafana'); jest.mock('./api/grafana');
jest.mock('./utils/config'); jest.mock('./utils/config');
jest.mock('app/core/services/context_srv');
const mocks = { const mocks = {
getAllDataSources: jest.mocked(getAllDataSources), getAllDataSources: jest.mocked(getAllDataSources),
@@ -40,6 +42,7 @@ const mocks = {
fetchNotifiers: jest.mocked(fetchNotifiers), fetchNotifiers: jest.mocked(fetchNotifiers),
testReceivers: jest.mocked(testReceivers), testReceivers: jest.mocked(testReceivers),
}, },
contextSrv: jest.mocked(contextSrv),
}; };
const renderReceivers = (alertManagerSourceName?: string) => { const renderReceivers = (alertManagerSourceName?: string) => {
@@ -125,8 +128,23 @@ describe('Receivers', () => {
mocks.getAllDataSources.mockReturnValue(Object.values(dataSources)); mocks.getAllDataSources.mockReturnValue(Object.values(dataSources));
mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock); mocks.api.fetchNotifiers.mockResolvedValue(grafanaNotifiersMock);
setDataSourceSrv(new MockDataSourceSrv(dataSources)); setDataSourceSrv(new MockDataSourceSrv(dataSources));
contextSrv.isEditor = true; mocks.contextSrv.isEditor = true;
store.delete(ALERTMANAGER_NAME_LOCAL_STORAGE_KEY); 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 () => { 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 () => { it('Cloud alertmanager receiver can be edited', async () => {
mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig); mocks.api.fetchConfig.mockResolvedValue(someCloudAlertManagerConfig);
mocks.api.updateConfig.mockResolvedValue(); mocks.api.updateConfig.mockResolvedValue();

View File

@@ -41,7 +41,10 @@ const Receivers: FC = () => {
}, [alertManagerSourceName, dispatch, shouldLoadConfig]); }, [alertManagerSourceName, dispatch, shouldLoadConfig]);
useEffect(() => { 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()); dispatch(fetchGrafanaNotifiersAction());
} }
}, [alertManagerSourceName, dispatch, receiverTypes]); }, [alertManagerSourceName, dispatch, receiverTypes]);

View File

@@ -13,6 +13,7 @@ export interface EmptyAreaWithCTAProps {
buttonIcon?: IconName; buttonIcon?: IconName;
buttonSize?: 'xs' | 'sm' | 'md' | 'lg'; buttonSize?: 'xs' | 'sm' | 'md' | 'lg';
buttonVariant?: ButtonVariant; buttonVariant?: ButtonVariant;
showButton?: boolean;
} }
export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
@@ -23,6 +24,7 @@ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
onButtonClick, onButtonClick,
text, text,
href, href,
showButton = true,
}) => { }) => {
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
@@ -37,15 +39,16 @@ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
<EmptyArea> <EmptyArea>
<> <>
<p className={styles.text}>{text}</p> <p className={styles.text}>{text}</p>
{href ? ( {showButton &&
<LinkButton href={href} type="button" {...commonProps}> (href ? (
{buttonLabel} <LinkButton href={href} type="button" {...commonProps}>
</LinkButton> {buttonLabel}
) : ( </LinkButton>
<Button onClick={onButtonClick} type="button" {...commonProps}> ) : (
{buttonLabel} <Button onClick={onButtonClick} type="button" {...commonProps}>
</Button> {buttonLabel}
)} </Button>
))}
</> </>
</EmptyArea> </EmptyArea>
); );

View File

@@ -3,12 +3,11 @@ import { GrafanaTheme2 } from '@grafana/data';
import { LinkButton, useStyles2 } from '@grafana/ui'; import { LinkButton, useStyles2 } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types'; import { AlertmanagerAlert, AlertState } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { isGrafanaRulesSource } from '../../utils/datasource';
import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc'; import { makeAMLink, makeLabelBasedSilenceLink } from '../../utils/misc';
import { AnnotationDetailsField } from '../AnnotationDetailsField'; import { AnnotationDetailsField } from '../AnnotationDetailsField';
import { Authorize } from '../Authorize'; import { Authorize } from '../Authorize';
import { getInstancesPermissions } from '../../utils/access-control';
interface AmNotificationsAlertDetailsProps { interface AmNotificationsAlertDetailsProps {
alertManagerSourceName: string; alertManagerSourceName: string;
@@ -17,18 +16,11 @@ interface AmNotificationsAlertDetailsProps {
export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => { export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName); const permissions = getInstancesPermissions(alertManagerSourceName);
return ( return (
<> <>
<div className={styles.actionsRow}> <div className={styles.actionsRow}>
<Authorize <Authorize actions={[permissions.update, permissions.create]} fallback={contextSrv.isEditor}>
actions={
isExternalAM
? [AccessControlAction.AlertingInstancesExternalWrite]
: [AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstanceUpdate]
}
fallback={contextSrv.isEditor}
>
{alert.status.state === AlertState.Suppressed && ( {alert.status.state === AlertState.Suppressed && (
<LinkButton <LinkButton
href={`${makeAMLink( href={`${makeAMLink(
@@ -53,9 +45,7 @@ export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, aler
</LinkButton> </LinkButton>
)} )}
</Authorize> </Authorize>
<Authorize <Authorize actions={[permissions.viewSource]}>
actions={isExternalAM ? [AccessControlAction.DataSourcesExplore] : [AccessControlAction.AlertingInstanceRead]}
>
{alert.generatorURL && ( {alert.generatorURL && (
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}> <LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
See source See source

View File

@@ -6,7 +6,8 @@ import { AmRouteReceiver, FormAmRoute } from '../../types/amroutes';
import { AmRootRouteForm } from './AmRootRouteForm'; import { AmRootRouteForm } from './AmRootRouteForm';
import { AmRootRouteRead } from './AmRootRouteRead'; import { AmRootRouteRead } from './AmRootRouteRead';
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { Authorize } from '../../components/Authorize';
import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmRootRouteProps { export interface AmRootRouteProps {
isEditMode: boolean; isEditMode: boolean;
onEnterEditMode: () => void; onEnterEditMode: () => void;
@@ -28,6 +29,7 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
}) => { }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const permissions = getNotificationsPermissions(alertManagerSourceName);
const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName); const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
return ( return (
@@ -37,9 +39,11 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
Root policy - <i>default for all alerts</i> Root policy - <i>default for all alerts</i>
</h5> </h5>
{!isEditMode && !isReadOnly && ( {!isEditMode && !isReadOnly && (
<Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary"> <Authorize actions={[permissions.update]}>
Edit <Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary">
</Button> Edit
</Button>
</Authorize>
)} )}
</div> </div>
<p> <p>

View File

@@ -7,13 +7,15 @@ import { emptyRoute } from '../../utils/amroutes';
import { AmRoutesTable } from './AmRoutesTable'; import { AmRoutesTable } from './AmRoutesTable';
import { getGridStyles } from './gridStyles'; import { getGridStyles } from './gridStyles';
import { MuteTimingsTable } from './MuteTimingsTable'; import { MuteTimingsTable } from './MuteTimingsTable';
import { useAlertManagerSourceName } from '../../hooks/useAlertManagerSourceName'; import { Authorize } from '../Authorize';
import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmRoutesExpandedReadProps { export interface AmRoutesExpandedReadProps {
onChange: (routes: FormAmRoute) => void; onChange: (routes: FormAmRoute) => void;
receivers: AmRouteReceiver[]; receivers: AmRouteReceiver[];
routes: FormAmRoute; routes: FormAmRoute;
readOnly?: boolean; readOnly?: boolean;
alertManagerSourceName: string;
} }
export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
@@ -21,10 +23,11 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
receivers, receivers,
routes, routes,
readOnly = false, readOnly = false,
alertManagerSourceName,
}) => { }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const gridStyles = useStyles2(getGridStyles); const gridStyles = useStyles2(getGridStyles);
const [alertManagerSourceName] = useAlertManagerSourceName(); const permissions = getNotificationsPermissions(alertManagerSourceName);
const groupWait = routes.groupWaitValue ? `${routes.groupWaitValue}${routes.groupWaitValueType}` : '-'; const groupWait = routes.groupWaitValue ? `${routes.groupWaitValue}${routes.groupWaitValueType}` : '-';
const groupInterval = routes.groupIntervalValue const groupInterval = routes.groupIntervalValue
@@ -71,23 +74,26 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
}} }}
receivers={receivers} receivers={receivers}
routes={subroutes} routes={subroutes}
alertManagerSourceName={alertManagerSourceName}
/> />
) : ( ) : (
<p>No nested policies configured.</p> <p>No nested policies configured.</p>
)} )}
{!isAddMode && !readOnly && ( {!isAddMode && !readOnly && (
<Button <Authorize actions={[permissions.create]}>
className={styles.addNestedRoutingBtn} <Button
icon="plus" className={styles.addNestedRoutingBtn}
onClick={() => { icon="plus"
setSubroutes((subroutes) => [...subroutes, emptyRoute]); onClick={() => {
setIsAddMode(true); setSubroutes((subroutes) => [...subroutes, emptyRoute]);
}} setIsAddMode(true);
variant="secondary" }}
type="button" variant="secondary"
> type="button"
Add nested policy >
</Button> Add nested policy
</Button>
</Authorize>
)} )}
</div> </div>
<div className={gridStyles.titleCell}>Mute timings</div> <div className={gridStyles.titleCell}>Mute timings</div>

View File

@@ -9,6 +9,8 @@ import { Matchers } from '../silences/Matchers';
import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager'; import { matcherFieldToMatcher, parseMatchers } from '../../utils/alertmanager';
import { intersectionWith, isEqual } from 'lodash'; import { intersectionWith, isEqual } from 'lodash';
import { EmptyArea } from '../EmptyArea'; import { EmptyArea } from '../EmptyArea';
import { contextSrv } from 'app/core/services/context_srv';
import { getNotificationsPermissions } from '../../utils/access-control';
export interface AmRoutesTableProps { export interface AmRoutesTableProps {
isAddMode: boolean; isAddMode: boolean;
@@ -18,6 +20,7 @@ export interface AmRoutesTableProps {
routes: FormAmRoute[]; routes: FormAmRoute[];
filters?: { queryString?: string; contactPoint?: string }; filters?: { queryString?: string; contactPoint?: string };
readOnly?: boolean; readOnly?: boolean;
alertManagerSourceName: string;
} }
type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>; type RouteTableColumnProps = DynamicTableColumnProps<FormAmRoute>;
@@ -69,9 +72,15 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
routes, routes,
filters, filters,
readOnly = false, readOnly = false,
alertManagerSourceName,
}) => { }) => {
const [editMode, setEditMode] = useState(false); const [editMode, setEditMode] = useState(false);
const [expandedId, setExpandedId] = useState<string | number>(); const [expandedId, setExpandedId] = useState<string | number>();
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 expandItem = useCallback((item: RouteTableItemProps) => setExpandedId(item.id), []);
const collapseItem = useCallback(() => setExpandedId(undefined), []); const collapseItem = useCallback(() => setExpandedId(undefined), []);
@@ -102,7 +111,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-', renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
size: 5, size: 5,
}, },
...(readOnly ...(!showActions
? [] ? []
: [ : [
{ {
@@ -212,6 +221,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
receivers={receivers} receivers={receivers}
routes={item.data} routes={item.data}
readOnly={readOnly} readOnly={readOnly}
alertManagerSourceName={alertManagerSourceName}
/> />
) )
} }

View File

@@ -11,8 +11,12 @@ import { MatcherFilter } from '../alert-groups/MatcherFilter';
import { EmptyArea } from '../EmptyArea'; import { EmptyArea } from '../EmptyArea';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA'; import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA';
import { AmRoutesTable } from './AmRoutesTable'; 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 { export interface AmSpecificRoutingProps {
alertManagerSourceName: string;
onChange: (routes: FormAmRoute) => void; onChange: (routes: FormAmRoute) => void;
onRootRouteEdit: () => void; onRootRouteEdit: () => void;
receivers: AmRouteReceiver[]; receivers: AmRouteReceiver[];
@@ -26,6 +30,7 @@ interface Filters {
} }
export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
alertManagerSourceName,
onChange, onChange,
onRootRouteEdit, onRootRouteEdit,
receivers, receivers,
@@ -34,6 +39,8 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
}) => { }) => {
const [actualRoutes, setActualRoutes] = useState([...routes.routes]); const [actualRoutes, setActualRoutes] = useState([...routes.routes]);
const [isAddMode, setIsAddMode] = useState(false); const [isAddMode, setIsAddMode] = useState(false);
const permissions = getNotificationsPermissions(alertManagerSourceName);
const canCreateNotifications = contextSrv.hasPermission(permissions.create);
const [searchParams, setSearchParams] = useURLSearchParams(); const [searchParams, setSearchParams] = useURLSearchParams();
const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams); const { queryString, contactPoint } = getNotificationPoliciesFilters(searchParams);
@@ -97,6 +104,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
buttonLabel="Set a default contact point" buttonLabel="Set a default contact point"
onButtonClick={onRootRouteEdit} onButtonClick={onRootRouteEdit}
text="You haven't set a default contact point for the root route yet." text="You haven't set a default contact point for the root route yet."
showButton={canCreateNotifications}
/> />
) )
) : actualRoutes.length > 0 ? ( ) : actualRoutes.length > 0 ? (
@@ -132,11 +140,13 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
)} )}
{!isAddMode && !readOnly && ( {!isAddMode && !readOnly && (
<div className={styles.addMatcherBtnRow}> <Authorize actions={[permissions.create]}>
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button"> <div className={styles.addMatcherBtnRow}>
New policy <Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
</Button> New policy
</div> </Button>
</div>
</Authorize>
)} )}
</div> </div>
<AmRoutesTable <AmRoutesTable
@@ -147,6 +157,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
receivers={receivers} receivers={receivers}
routes={actualRoutes} routes={actualRoutes}
filters={{ queryString, contactPoint }} filters={{ queryString, contactPoint }}
alertManagerSourceName={alertManagerSourceName}
/> />
</> </>
) : readOnly ? ( ) : readOnly ? (
@@ -159,6 +170,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
buttonLabel="New specific policy" buttonLabel="New specific policy"
onButtonClick={addNewRoute} onButtonClick={addNewRoute}
text="You haven't created any specific policies yet." text="You haven't created any specific policies yet."
showButton={canCreateNotifications}
/> />
)} )}
</div> </div>

View File

@@ -17,6 +17,9 @@ import {
getYearsString, getYearsString,
} from '../../utils/alertmanager'; } from '../../utils/alertmanager';
import { EmptyAreaWithCTA } from '../EmptyAreaWithCTA'; 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 { interface Props {
alertManagerSourceName: string; alertManagerSourceName: string;
@@ -27,6 +30,7 @@ interface Props {
export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTimingNames, hideActions }) => { export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTimingNames, hideActions }) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const dispatch = useDispatch(); const dispatch = useDispatch();
const permissions = getNotificationsPermissions(alertManagerSourceName);
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs); const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
const [muteTimingName, setMuteTimingName] = useState<string>(''); const [muteTimingName, setMuteTimingName] = useState<string>('');
const { result }: AsyncRequestState<AlertManagerCortexConfig> = const { result }: AsyncRequestState<AlertManagerCortexConfig> =
@@ -56,14 +60,16 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
</p> </p>
)} )}
{!hideActions && items.length > 0 && ( {!hideActions && items.length > 0 && (
<LinkButton <Authorize actions={[permissions.create]}>
className={styles.addMuteButton} <LinkButton
icon="plus" className={styles.addMuteButton}
variant="primary" icon="plus"
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)} variant="primary"
> href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
New mute timing >
</LinkButton> New mute timing
</LinkButton>
</Authorize>
)} )}
{items.length > 0 ? ( {items.length > 0 ? (
<DynamicTable items={items} cols={columns} /> <DynamicTable items={items} cols={columns} />
@@ -74,6 +80,7 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
buttonIcon="plus" buttonIcon="plus"
buttonSize="lg" buttonSize="lg"
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)} href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
showButton={contextSrv.hasPermission(permissions.create)}
/> />
) : ( ) : (
<p>No mute timings configured</p> <p>No mute timings configured</p>
@@ -93,6 +100,11 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
}; };
function useColumns(alertManagerSourceName: string, hideActions = false, setMuteTimingName: (name: string) => void) { 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<DynamicTableColumnProps<MuteTimeInterval>> => { return useMemo((): Array<DynamicTableColumnProps<MuteTimeInterval>> => {
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [ const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
{ {
@@ -109,19 +121,29 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
renderCell: ({ data }) => renderTimeIntervals(data.time_intervals), renderCell: ({ data }) => renderTimeIntervals(data.time_intervals),
}, },
]; ];
if (!hideActions) { if (showActions) {
columns.push({ columns.push({
id: 'actions', id: 'actions',
label: 'Actions', label: 'Actions',
renderCell: function renderActions({ data }) { renderCell: function renderActions({ data }) {
return ( return (
<div> <div>
<Link <Authorize actions={[permissions.update]}>
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, { muteName: data.name })} <Link
> href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, {
<IconButton name="edit" title="Edit mute timing" /> muteName: data.name,
</Link> })}
<IconButton name={'trash-alt'} title="Delete mute timing" onClick={() => setMuteTimingName(data.name)} /> >
<IconButton name="edit" title="Edit mute timing" />
</Link>
</Authorize>
<Authorize actions={[permissions.delete]}>
<IconButton
name={'trash-alt'}
title="Delete mute timing"
onClick={() => setMuteTimingName(data.name)}
/>
</Authorize>
</div> </div>
); );
}, },
@@ -129,7 +151,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
}); });
} }
return columns; return columns;
}, [alertManagerSourceName, hideActions, setMuteTimingName]); }, [alertManagerSourceName, setMuteTimingName, showActions, permissions]);
} }
function renderTimeIntervals(timeIntervals: TimeInterval[]) { function renderTimeIntervals(timeIntervals: TimeInterval[]) {

View File

@@ -2,9 +2,11 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Alert, LinkButton, useStyles2 } from '@grafana/ui'; import { Alert, LinkButton, useStyles2 } from '@grafana/ui';
import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types'; import { AlertManagerCortexConfig } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; import { GRAFANA_RULES_SOURCE_NAME, isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
import { Authorize } from '../Authorize';
import { ReceiversTable } from './ReceiversTable'; import { ReceiversTable } from './ReceiversTable';
import { TemplatesTable } from './TemplatesTable'; import { TemplatesTable } from './TemplatesTable';
@@ -22,15 +24,17 @@ export const ReceiversAndTemplatesView: FC<Props> = ({ config, alertManagerName
{!isVanillaAM && <TemplatesTable config={config} alertManagerName={alertManagerName} />} {!isVanillaAM && <TemplatesTable config={config} alertManagerName={alertManagerName} />}
<ReceiversTable config={config} alertManagerName={alertManagerName} /> <ReceiversTable config={config} alertManagerName={alertManagerName} />
{isCloud && ( {isCloud && (
<Alert className={styles.section} severity="info" title="Global config for contact points"> <Authorize actions={[AccessControlAction.AlertingNotificationsExternalWrite]}>
<p> <Alert className={styles.section} severity="info" title="Global config for contact points">
For each external Alertmanager you can define global settings, like server addresses, usernames and <p>
password, for all the supported contact points. For each external Alertmanager you can define global settings, like server addresses, usernames and
</p> password, for all the supported contact points.
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary"> </p>
{isVanillaAM ? 'View global config' : 'Edit global config'} <LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
</LinkButton> {isVanillaAM ? 'View global config' : 'Edit global config'}
</Alert> </LinkButton>
</Alert>
</Authorize>
)} )}
</> </>
); );

View File

@@ -13,6 +13,9 @@ import { isReceiverUsed } from '../../utils/alertmanager';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { deleteReceiverAction } from '../../state/actions'; import { deleteReceiverAction } from '../../state/actions';
import { isVanillaPrometheusAlertManagerDataSource } from '../../utils/datasource'; 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 { interface Props {
config: AlertManagerCortexConfig; config: AlertManagerCortexConfig;
@@ -24,6 +27,7 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
const tableStyles = useStyles2(getAlertTableStyles); const tableStyles = useStyles2(getAlertTableStyles);
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName); const isVanillaAM = isVanillaPrometheusAlertManagerDataSource(alertManagerName);
const permissions = getNotificationsPermissions(alertManagerName);
const grafanaNotifiers = useUnifiedAlertingSelector((state) => state.grafanaNotifiers); 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 // 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<Props> = ({ config, alertManagerName }) => {
className={styles.section} className={styles.section}
title="Contact points" title="Contact points"
description="Define where the notifications will be sent to, for example email or Slack." 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" addButtonLabel="New contact point"
addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)} addButtonTo={makeAMLink('/alerting/notifications/receivers/new', alertManagerName)}
> >
@@ -74,13 +78,17 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
<colgroup> <colgroup>
<col /> <col />
<col /> <col />
<col /> <Authorize actions={[permissions.update, permissions.delete]}>
<col />
</Authorize>
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th>Contact point name</th> <th>Contact point name</th>
<th>Type</th> <th>Type</th>
<th>Actions</th> <Authorize actions={[permissions.update, permissions.delete]}>
<th>Actions</th>
</Authorize>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -93,38 +101,46 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
<tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}> <tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
<td>{receiver.name}</td> <td>{receiver.name}</td>
<td>{receiver.types.join(', ')}</td> <td>{receiver.types.join(', ')}</td>
<td className={tableStyles.actionsCell}> <Authorize actions={[permissions.update, permissions.delete]}>
{!isVanillaAM && ( <td className={tableStyles.actionsCell}>
<> {!isVanillaAM && (
<ActionIcon <>
aria-label="Edit" <Authorize actions={[permissions.update]}>
data-testid="edit" <ActionIcon
to={makeAMLink( aria-label="Edit"
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`, data-testid="edit"
alertManagerName to={makeAMLink(
)} `/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
tooltip="Edit contact point" alertManagerName
icon="pen" )}
/> tooltip="Edit contact point"
<ActionIcon icon="pen"
onClick={() => onClickDeleteReceiver(receiver.name)} />
tooltip="Delete contact point" </Authorize>
icon="trash-alt" <Authorize actions={[permissions.delete]}>
/> <ActionIcon
</> onClick={() => onClickDeleteReceiver(receiver.name)}
)} tooltip="Delete contact point"
{isVanillaAM && ( icon="trash-alt"
<ActionIcon />
data-testid="view" </Authorize>
to={makeAMLink( </>
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`, )}
alertManagerName {isVanillaAM && (
)} <Authorize actions={[permissions.update]}>
tooltip="View contact point" <ActionIcon
icon="file-alt" data-testid="view"
/> to={makeAMLink(
)} `/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
</td> alertManagerName
)}
tooltip="View contact point"
icon="file-alt"
/>
</Authorize>
)}
</td>
</Authorize>
</tr> </tr>
))} ))}
</tbody> </tbody>

View File

@@ -9,6 +9,9 @@ import { ReceiversSection } from './ReceiversSection';
import { makeAMLink } from '../../utils/misc'; import { makeAMLink } from '../../utils/misc';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { deleteTemplateAction } from '../../state/actions'; 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 { interface Props {
config: AlertManagerCortexConfig; config: AlertManagerCortexConfig;
@@ -19,6 +22,7 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({}); const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
const tableStyles = useStyles2(getAlertTableStyles); const tableStyles = useStyles2(getAlertTableStyles);
const permissions = getNotificationsPermissions(alertManagerName);
const templateRows = useMemo(() => Object.entries(config.template_files), [config]); const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
const [templateToDelete, setTemplateToDelete] = useState<string>(); const [templateToDelete, setTemplateToDelete] = useState<string>();
@@ -36,6 +40,7 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
description="Templates construct the messages that get sent to the contact points." description="Templates construct the messages that get sent to the contact points."
addButtonLabel="New template" addButtonLabel="New template"
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)} addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
showButton={contextSrv.hasPermission(permissions.create)}
> >
<table className={tableStyles.table} data-testid="templates-table"> <table className={tableStyles.table} data-testid="templates-table">
<colgroup> <colgroup>
@@ -47,7 +52,9 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
<tr> <tr>
<th></th> <th></th>
<th>Template</th> <th>Template</th>
<th>Actions</th> <Authorize actions={[permissions.update, permissions.delete]}>
<th>Actions</th>
</Authorize>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -68,17 +75,27 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
/> />
</td> </td>
<td>{name}</td> <td>{name}</td>
<td className={tableStyles.actionsCell}> <Authorize actions={[permissions.update, permissions.delete]}>
<ActionIcon <td className={tableStyles.actionsCell}>
to={makeAMLink( <Authorize actions={[permissions.update]}>
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`, <ActionIcon
alertManagerName to={makeAMLink(
)} `/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
tooltip="edit template" alertManagerName
icon="pen" )}
/> tooltip="edit template"
<ActionIcon onClick={() => setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" /> icon="pen"
</td> />
</Authorize>
<Authorize actions={[permissions.delete]}>
<ActionIcon
onClick={() => setTemplateToDelete(name)}
tooltip="delete template"
icon="trash-alt"
/>
</Authorize>
</td>
</Authorize>
</tr> </tr>
{isExpanded && ( {isExpanded && (
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}> <tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>

View File

@@ -18,9 +18,8 @@ import { useDispatch } from 'react-redux';
import { expireSilenceAction } from '../../state/actions'; import { expireSilenceAction } from '../../state/actions';
import { SilenceDetails } from './SilenceDetails'; import { SilenceDetails } from './SilenceDetails';
import { Stack } from '@grafana/experimental'; import { Stack } from '@grafana/experimental';
import { AccessControlAction } from '../../../../../types';
import { Authorize } from '../Authorize'; import { Authorize } from '../Authorize';
import { isGrafanaRulesSource } from '../../utils/datasource'; import { getInstancesPermissions } from '../../utils/access-control';
export interface SilenceTableItem extends Silence { export interface SilenceTableItem extends Silence {
silencedAlerts: AlertmanagerAlert[]; silencedAlerts: AlertmanagerAlert[];
@@ -38,7 +37,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const [queryParams] = useQueryParams(); const [queryParams] = useQueryParams();
const filteredSilences = useFilteredSilences(silences); const filteredSilences = useFilteredSilences(silences);
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName); const permissions = getInstancesPermissions(alertManagerSourceName);
const { silenceState } = getSilenceFiltersFromUrlParams(queryParams); const { silenceState } = getSilenceFiltersFromUrlParams(queryParams);
@@ -65,14 +64,7 @@ const SilencesTable: FC<Props> = ({ silences, alertManagerAlerts, alertManagerSo
{!!silences.length && ( {!!silences.length && (
<> <>
<SilencesFilter /> <SilencesFilter />
<Authorize <Authorize actions={[permissions.create]} fallback={contextSrv.isEditor}>
actions={
isExternalAM
? [AccessControlAction.AlertingInstancesExternalWrite]
: [AccessControlAction.AlertingInstanceCreate]
}
fallback={contextSrv.isEditor}
>
<div className={styles.topButtonContainer}> <div className={styles.topButtonContainer}>
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}> <Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
<Button className={styles.addNewSilence} icon="plus"> <Button className={styles.addNewSilence} icon="plus">
@@ -178,15 +170,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
function useColumns(alertManagerSourceName: string) { function useColumns(alertManagerSourceName: string) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName); const permissions = getInstancesPermissions(alertManagerSourceName);
return useMemo((): SilenceTableColumnProps[] => { return useMemo((): SilenceTableColumnProps[] => {
const handleExpireSilenceClick = (id: string) => { const handleExpireSilenceClick = (id: string) => {
dispatch(expireSilenceAction(alertManagerSourceName, id)); dispatch(expireSilenceAction(alertManagerSourceName, id));
}; };
const showActions = contextSrv.hasAccess( const showActions = contextSrv.hasAccess(permissions.update, contextSrv.isEditor);
isExternalAM ? AccessControlAction.AlertingInstancesExternalWrite : AccessControlAction.AlertingInstanceUpdate,
contextSrv.isEditor
);
const columns: SilenceTableColumnProps[] = [ const columns: SilenceTableColumnProps[] = [
{ {
id: 'state', id: 'state',
@@ -262,7 +251,7 @@ function useColumns(alertManagerSourceName: string) {
}); });
} }
return columns; return columns;
}, [alertManagerSourceName, dispatch, styles, isExternalAM]); }, [alertManagerSourceName, dispatch, styles, permissions]);
} }
export default SilencesTable; export default SilencesTable;

View File

@@ -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);
};
}