mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 08:05:43 -06:00
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:
parent
460b8e85d7
commit
49505b9a3b
@ -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())),
|
||||
)
|
||||
|
||||
|
@ -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')
|
||||
),
|
||||
|
@ -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,
|
||||
|
@ -122,6 +122,7 @@ const AmRoutes: FC = () => {
|
||||
/>
|
||||
<div className={styles.break} />
|
||||
<AmSpecificRouting
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
onChange={handleSave}
|
||||
readOnly={readOnly}
|
||||
onRootRouteEdit={enterRootRouteEditMode}
|
||||
|
@ -25,10 +25,12 @@ import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { selectOptionInTest } from '@grafana/ui';
|
||||
import { AlertManagerDataSourceJsonData, AlertManagerImplementation } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { interceptLinkClicks } from 'app/core/navigation/patch/interceptLinkClicks';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
jest.mock('./api/alertmanager');
|
||||
jest.mock('./api/grafana');
|
||||
jest.mock('./utils/config');
|
||||
jest.mock('app/core/services/context_srv');
|
||||
|
||||
const mocks = {
|
||||
getAllDataSources: jest.mocked(getAllDataSources),
|
||||
@ -40,6 +42,7 @@ const mocks = {
|
||||
fetchNotifiers: jest.mocked(fetchNotifiers),
|
||||
testReceivers: jest.mocked(testReceivers),
|
||||
},
|
||||
contextSrv: jest.mocked(contextSrv),
|
||||
};
|
||||
|
||||
const renderReceivers = (alertManagerSourceName?: string) => {
|
||||
@ -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();
|
||||
|
@ -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]);
|
||||
|
@ -13,6 +13,7 @@ export interface EmptyAreaWithCTAProps {
|
||||
buttonIcon?: IconName;
|
||||
buttonSize?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
buttonVariant?: ButtonVariant;
|
||||
showButton?: boolean;
|
||||
}
|
||||
|
||||
export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
||||
@ -23,6 +24,7 @@ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
||||
onButtonClick,
|
||||
text,
|
||||
href,
|
||||
showButton = true,
|
||||
}) => {
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
@ -37,15 +39,16 @@ export const EmptyAreaWithCTA: FC<EmptyAreaWithCTAProps> = ({
|
||||
<EmptyArea>
|
||||
<>
|
||||
<p className={styles.text}>{text}</p>
|
||||
{href ? (
|
||||
<LinkButton href={href} type="button" {...commonProps}>
|
||||
{buttonLabel}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button onClick={onButtonClick} type="button" {...commonProps}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
)}
|
||||
{showButton &&
|
||||
(href ? (
|
||||
<LinkButton href={href} type="button" {...commonProps}>
|
||||
{buttonLabel}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Button onClick={onButtonClick} type="button" {...commonProps}>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
))}
|
||||
</>
|
||||
</EmptyArea>
|
||||
);
|
||||
|
@ -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<AmNotificationsAlertDetailsProps> = ({ alert, alertManagerSourceName }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const isExternalAM = !isGrafanaRulesSource(alertManagerSourceName);
|
||||
const permissions = getInstancesPermissions(alertManagerSourceName);
|
||||
return (
|
||||
<>
|
||||
<div className={styles.actionsRow}>
|
||||
<Authorize
|
||||
actions={
|
||||
isExternalAM
|
||||
? [AccessControlAction.AlertingInstancesExternalWrite]
|
||||
: [AccessControlAction.AlertingInstanceCreate, AccessControlAction.AlertingInstanceUpdate]
|
||||
}
|
||||
fallback={contextSrv.isEditor}
|
||||
>
|
||||
<Authorize actions={[permissions.update, permissions.create]} fallback={contextSrv.isEditor}>
|
||||
{alert.status.state === AlertState.Suppressed && (
|
||||
<LinkButton
|
||||
href={`${makeAMLink(
|
||||
@ -53,9 +45,7 @@ export const AlertDetails: FC<AmNotificationsAlertDetailsProps> = ({ alert, aler
|
||||
</LinkButton>
|
||||
)}
|
||||
</Authorize>
|
||||
<Authorize
|
||||
actions={isExternalAM ? [AccessControlAction.DataSourcesExplore] : [AccessControlAction.AlertingInstanceRead]}
|
||||
>
|
||||
<Authorize actions={[permissions.viewSource]}>
|
||||
{alert.generatorURL && (
|
||||
<LinkButton className={styles.button} href={alert.generatorURL} icon={'chart-line'} size={'sm'}>
|
||||
See source
|
||||
|
@ -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<AmRootRouteProps> = ({
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
const isReadOnly = isVanillaPrometheusAlertManagerDataSource(alertManagerSourceName);
|
||||
|
||||
return (
|
||||
@ -37,9 +39,11 @@ export const AmRootRoute: FC<AmRootRouteProps> = ({
|
||||
Root policy - <i>default for all alerts</i>
|
||||
</h5>
|
||||
{!isEditMode && !isReadOnly && (
|
||||
<Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary">
|
||||
Edit
|
||||
</Button>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Button icon="pen" onClick={onEnterEditMode} size="sm" type="button" variant="secondary">
|
||||
Edit
|
||||
</Button>
|
||||
</Authorize>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
|
@ -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<AmRoutesExpandedReadProps> = ({
|
||||
@ -21,10 +23,11 @@ export const AmRoutesExpandedRead: FC<AmRoutesExpandedReadProps> = ({
|
||||
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<AmRoutesExpandedReadProps> = ({
|
||||
}}
|
||||
receivers={receivers}
|
||||
routes={subroutes}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
/>
|
||||
) : (
|
||||
<p>No nested policies configured.</p>
|
||||
)}
|
||||
{!isAddMode && !readOnly && (
|
||||
<Button
|
||||
className={styles.addNestedRoutingBtn}
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
setSubroutes((subroutes) => [...subroutes, emptyRoute]);
|
||||
setIsAddMode(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
>
|
||||
Add nested policy
|
||||
</Button>
|
||||
<Authorize actions={[permissions.create]}>
|
||||
<Button
|
||||
className={styles.addNestedRoutingBtn}
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
setSubroutes((subroutes) => [...subroutes, emptyRoute]);
|
||||
setIsAddMode(true);
|
||||
}}
|
||||
variant="secondary"
|
||||
type="button"
|
||||
>
|
||||
Add nested policy
|
||||
</Button>
|
||||
</Authorize>
|
||||
)}
|
||||
</div>
|
||||
<div className={gridStyles.titleCell}>Mute timings</div>
|
||||
|
@ -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<FormAmRoute>;
|
||||
@ -69,9 +72,15 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
||||
routes,
|
||||
filters,
|
||||
readOnly = false,
|
||||
alertManagerSourceName,
|
||||
}) => {
|
||||
const [editMode, setEditMode] = useState(false);
|
||||
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 collapseItem = useCallback(() => setExpandedId(undefined), []);
|
||||
@ -102,7 +111,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
||||
renderCell: (item) => item.data.muteTimeIntervals.join(', ') || '-',
|
||||
size: 5,
|
||||
},
|
||||
...(readOnly
|
||||
...(!showActions
|
||||
? []
|
||||
: [
|
||||
{
|
||||
@ -212,6 +221,7 @@ export const AmRoutesTable: FC<AmRoutesTableProps> = ({
|
||||
receivers={receivers}
|
||||
routes={item.data}
|
||||
readOnly={readOnly}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
@ -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<AmSpecificRoutingProps> = ({
|
||||
alertManagerSourceName,
|
||||
onChange,
|
||||
onRootRouteEdit,
|
||||
receivers,
|
||||
@ -34,6 +39,8 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
||||
}) => {
|
||||
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<AmSpecificRoutingProps> = ({
|
||||
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<AmSpecificRoutingProps> = ({
|
||||
)}
|
||||
|
||||
{!isAddMode && !readOnly && (
|
||||
<div className={styles.addMatcherBtnRow}>
|
||||
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
|
||||
New policy
|
||||
</Button>
|
||||
</div>
|
||||
<Authorize actions={[permissions.create]}>
|
||||
<div className={styles.addMatcherBtnRow}>
|
||||
<Button className={styles.addMatcherBtn} icon="plus" onClick={addNewRoute} type="button">
|
||||
New policy
|
||||
</Button>
|
||||
</div>
|
||||
</Authorize>
|
||||
)}
|
||||
</div>
|
||||
<AmRoutesTable
|
||||
@ -147,6 +157,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
||||
receivers={receivers}
|
||||
routes={actualRoutes}
|
||||
filters={{ queryString, contactPoint }}
|
||||
alertManagerSourceName={alertManagerSourceName}
|
||||
/>
|
||||
</>
|
||||
) : readOnly ? (
|
||||
@ -159,6 +170,7 @@ export const AmSpecificRouting: FC<AmSpecificRoutingProps> = ({
|
||||
buttonLabel="New specific policy"
|
||||
onButtonClick={addNewRoute}
|
||||
text="You haven't created any specific policies yet."
|
||||
showButton={canCreateNotifications}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@ -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<Props> = ({ alertManagerSourceName, muteTimingNames, hideActions }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
const amConfigs = useUnifiedAlertingSelector((state) => state.amConfigs);
|
||||
const [muteTimingName, setMuteTimingName] = useState<string>('');
|
||||
const { result }: AsyncRequestState<AlertManagerCortexConfig> =
|
||||
@ -56,14 +60,16 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
|
||||
</p>
|
||||
)}
|
||||
{!hideActions && items.length > 0 && (
|
||||
<LinkButton
|
||||
className={styles.addMuteButton}
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||
>
|
||||
New mute timing
|
||||
</LinkButton>
|
||||
<Authorize actions={[permissions.create]}>
|
||||
<LinkButton
|
||||
className={styles.addMuteButton}
|
||||
icon="plus"
|
||||
variant="primary"
|
||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||
>
|
||||
New mute timing
|
||||
</LinkButton>
|
||||
</Authorize>
|
||||
)}
|
||||
{items.length > 0 ? (
|
||||
<DynamicTable items={items} cols={columns} />
|
||||
@ -74,6 +80,7 @@ export const MuteTimingsTable: FC<Props> = ({ alertManagerSourceName, muteTiming
|
||||
buttonIcon="plus"
|
||||
buttonSize="lg"
|
||||
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
|
||||
showButton={contextSrv.hasPermission(permissions.create)}
|
||||
/>
|
||||
) : (
|
||||
<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) {
|
||||
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>> => {
|
||||
const columns: Array<DynamicTableColumnProps<MuteTimeInterval>> = [
|
||||
{
|
||||
@ -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 (
|
||||
<div>
|
||||
<Link
|
||||
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, { muteName: data.name })}
|
||||
>
|
||||
<IconButton name="edit" title="Edit mute timing" />
|
||||
</Link>
|
||||
<IconButton name={'trash-alt'} title="Delete mute timing" onClick={() => setMuteTimingName(data.name)} />
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<Link
|
||||
href={makeAMLink(`/alerting/routes/mute-timing/edit`, alertManagerSourceName, {
|
||||
muteName: 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>
|
||||
);
|
||||
},
|
||||
@ -129,7 +151,7 @@ function useColumns(alertManagerSourceName: string, hideActions = false, setMute
|
||||
});
|
||||
}
|
||||
return columns;
|
||||
}, [alertManagerSourceName, hideActions, setMuteTimingName]);
|
||||
}, [alertManagerSourceName, setMuteTimingName, showActions, permissions]);
|
||||
}
|
||||
|
||||
function renderTimeIntervals(timeIntervals: TimeInterval[]) {
|
||||
|
@ -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<Props> = ({ config, alertManagerName
|
||||
{!isVanillaAM && <TemplatesTable config={config} alertManagerName={alertManagerName} />}
|
||||
<ReceiversTable config={config} alertManagerName={alertManagerName} />
|
||||
{isCloud && (
|
||||
<Alert className={styles.section} severity="info" title="Global config for contact points">
|
||||
<p>
|
||||
For each external Alertmanager you can define global settings, like server addresses, usernames and
|
||||
password, for all the supported contact points.
|
||||
</p>
|
||||
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
|
||||
{isVanillaAM ? 'View global config' : 'Edit global config'}
|
||||
</LinkButton>
|
||||
</Alert>
|
||||
<Authorize actions={[AccessControlAction.AlertingNotificationsExternalWrite]}>
|
||||
<Alert className={styles.section} severity="info" title="Global config for contact points">
|
||||
<p>
|
||||
For each external Alertmanager you can define global settings, like server addresses, usernames and
|
||||
password, for all the supported contact points.
|
||||
</p>
|
||||
<LinkButton href={makeAMLink('alerting/notifications/global-config', alertManagerName)} variant="secondary">
|
||||
{isVanillaAM ? 'View global config' : 'Edit global config'}
|
||||
</LinkButton>
|
||||
</Alert>
|
||||
</Authorize>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -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<Props> = ({ 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<Props> = ({ 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<Props> = ({ config, alertManagerName }) => {
|
||||
<colgroup>
|
||||
<col />
|
||||
<col />
|
||||
<col />
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<col />
|
||||
</Authorize>
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Contact point name</th>
|
||||
<th>Type</th>
|
||||
<th>Actions</th>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<th>Actions</th>
|
||||
</Authorize>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -93,38 +101,46 @@ export const ReceiversTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
<tr key={receiver.name} className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
<td>{receiver.name}</td>
|
||||
<td>{receiver.types.join(', ')}</td>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
{!isVanillaAM && (
|
||||
<>
|
||||
<ActionIcon
|
||||
aria-label="Edit"
|
||||
data-testid="edit"
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="Edit contact point"
|
||||
icon="pen"
|
||||
/>
|
||||
<ActionIcon
|
||||
onClick={() => onClickDeleteReceiver(receiver.name)}
|
||||
tooltip="Delete contact point"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isVanillaAM && (
|
||||
<ActionIcon
|
||||
data-testid="view"
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="View contact point"
|
||||
icon="file-alt"
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
{!isVanillaAM && (
|
||||
<>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
aria-label="Edit"
|
||||
data-testid="edit"
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="Edit contact point"
|
||||
icon="pen"
|
||||
/>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<ActionIcon
|
||||
onClick={() => onClickDeleteReceiver(receiver.name)}
|
||||
tooltip="Delete contact point"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
</>
|
||||
)}
|
||||
{isVanillaAM && (
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
data-testid="view"
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/receivers/${encodeURIComponent(receiver.name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="View contact point"
|
||||
icon="file-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
)}
|
||||
</td>
|
||||
</Authorize>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
@ -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<Props> = ({ config, alertManagerName }) => {
|
||||
const dispatch = useDispatch();
|
||||
const [expandedTemplates, setExpandedTemplates] = useState<Record<string, boolean>>({});
|
||||
const tableStyles = useStyles2(getAlertTableStyles);
|
||||
const permissions = getNotificationsPermissions(alertManagerName);
|
||||
|
||||
const templateRows = useMemo(() => Object.entries(config.template_files), [config]);
|
||||
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."
|
||||
addButtonLabel="New template"
|
||||
addButtonTo={makeAMLink('/alerting/notifications/templates/new', alertManagerName)}
|
||||
showButton={contextSrv.hasPermission(permissions.create)}
|
||||
>
|
||||
<table className={tableStyles.table} data-testid="templates-table">
|
||||
<colgroup>
|
||||
@ -47,7 +52,9 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Template</th>
|
||||
<th>Actions</th>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<th>Actions</th>
|
||||
</Authorize>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -68,17 +75,27 @@ export const TemplatesTable: FC<Props> = ({ config, alertManagerName }) => {
|
||||
/>
|
||||
</td>
|
||||
<td>{name}</td>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="edit template"
|
||||
icon="pen"
|
||||
/>
|
||||
<ActionIcon onClick={() => setTemplateToDelete(name)} tooltip="delete template" icon="trash-alt" />
|
||||
</td>
|
||||
<Authorize actions={[permissions.update, permissions.delete]}>
|
||||
<td className={tableStyles.actionsCell}>
|
||||
<Authorize actions={[permissions.update]}>
|
||||
<ActionIcon
|
||||
to={makeAMLink(
|
||||
`/alerting/notifications/templates/${encodeURIComponent(name)}/edit`,
|
||||
alertManagerName
|
||||
)}
|
||||
tooltip="edit template"
|
||||
icon="pen"
|
||||
/>
|
||||
</Authorize>
|
||||
<Authorize actions={[permissions.delete]}>
|
||||
<ActionIcon
|
||||
onClick={() => setTemplateToDelete(name)}
|
||||
tooltip="delete template"
|
||||
icon="trash-alt"
|
||||
/>
|
||||
</Authorize>
|
||||
</td>
|
||||
</Authorize>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr className={idx % 2 === 0 ? tableStyles.evenRow : undefined}>
|
||||
|
@ -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<Props> = ({ 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<Props> = ({ silences, alertManagerAlerts, alertManagerSo
|
||||
{!!silences.length && (
|
||||
<>
|
||||
<SilencesFilter />
|
||||
<Authorize
|
||||
actions={
|
||||
isExternalAM
|
||||
? [AccessControlAction.AlertingInstancesExternalWrite]
|
||||
: [AccessControlAction.AlertingInstanceCreate]
|
||||
}
|
||||
fallback={contextSrv.isEditor}
|
||||
>
|
||||
<Authorize actions={[permissions.create]} fallback={contextSrv.isEditor}>
|
||||
<div className={styles.topButtonContainer}>
|
||||
<Link href={makeAMLink('/alerting/silence/new', alertManagerSourceName)}>
|
||||
<Button className={styles.addNewSilence} icon="plus">
|
||||
@ -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;
|
||||
|
78
public/app/features/alerting/unified/utils/access-control.ts
Normal file
78
public/app/features/alerting/unified/utils/access-control.ts
Normal 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);
|
||||
};
|
||||
}
|
Loading…
Reference in New Issue
Block a user