mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Export contact points to check access control action instead legacy role (#71990)
* introduce a new action "alert.provisioning.secrets:read" and role "fixed:alerting.provisioning.secrets:reader" * update alerting API authorization layer to let the user read provisioning with the new action * let new action use decrypt flag * add action and role to docs
This commit is contained in:
@@ -696,6 +696,21 @@ describe('RuleList', () => {
|
||||
await userEvent.click(ui.moreButton.get());
|
||||
expect(ui.exportButton.get()).toBeInTheDocument();
|
||||
});
|
||||
it('Export button should be visible when the user has alert provisioning read secrets permissions', async () => {
|
||||
enableRBAC();
|
||||
|
||||
grantUserPermissions([AccessControlAction.AlertingProvisioningReadSecrets]);
|
||||
|
||||
mocks.getAllDataSourcesMock.mockReturnValue([]);
|
||||
setDataSourceSrv(new MockDataSourceSrv({}));
|
||||
mocks.api.fetchRules.mockResolvedValue([]);
|
||||
mocks.api.fetchRulerRules.mockResolvedValue({});
|
||||
|
||||
renderRuleList();
|
||||
|
||||
await userEvent.click(ui.moreButton.get());
|
||||
expect(ui.exportButton.get()).toBeInTheDocument();
|
||||
});
|
||||
it('Export button should not be visible when the user has no alert provisioning read permissions', async () => {
|
||||
enableRBAC();
|
||||
|
||||
|
||||
@@ -74,7 +74,9 @@ const Policy: FC<PolicyComponentProps> = ({
|
||||
const permissions = getNotificationsPermissions(alertManagerSourceName);
|
||||
const canEditRoutes = contextSrv.hasPermission(permissions.update);
|
||||
const canDeleteRoutes = contextSrv.hasPermission(permissions.delete);
|
||||
const canReadProvisioning = contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin());
|
||||
const canReadProvisioning =
|
||||
contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin()) ||
|
||||
contextSrv.hasPermission(permissions.provisioning.readSecrets);
|
||||
|
||||
const contactPoint = currentRoute.receiver;
|
||||
const continueMatching = currentRoute.continue ?? false;
|
||||
|
||||
@@ -8,16 +8,23 @@ import {
|
||||
Receiver,
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { ContactPointsState, NotifierDTO, NotifierType } from 'app/types';
|
||||
import { AccessControlAction, ContactPointsState, NotifierDTO, NotifierType } from 'app/types';
|
||||
|
||||
import * as onCallApi from '../../api/onCallApi';
|
||||
import * as receiversApi from '../../api/receiversApi';
|
||||
import { enableRBAC, grantUserPermissions } from '../../mocks';
|
||||
import { fetchGrafanaNotifiersAction } from '../../state/actions';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
|
||||
import { createUrl } from '../../utils/url';
|
||||
|
||||
import { ReceiversTable } from './ReceiversTable';
|
||||
import * as grafanaApp from './grafanaAppReceivers/grafanaApp';
|
||||
|
||||
const renderReceieversTable = async (receivers: Receiver[], notifiers: NotifierDTO[]) => {
|
||||
const renderReceieversTable = async (
|
||||
receivers: Receiver[],
|
||||
notifiers: NotifierDTO[],
|
||||
alertmanagerName = 'alertmanager-1'
|
||||
) => {
|
||||
const config: AlertManagerCortexConfig = {
|
||||
template_files: {},
|
||||
alertmanager_config: {
|
||||
@@ -30,7 +37,7 @@ const renderReceieversTable = async (receivers: Receiver[], notifiers: NotifierD
|
||||
|
||||
return render(
|
||||
<TestProvider store={store}>
|
||||
<ReceiversTable config={config} alertManagerName="alertmanager-1" />
|
||||
<ReceiversTable config={config} alertManagerName={alertmanagerName} />
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
@@ -127,4 +134,88 @@ describe('ReceiversTable', () => {
|
||||
expect(rows[1]).toHaveTextContent('without receivers');
|
||||
expect(rows[1].querySelector('[data-column="Type"]')).toHaveTextContent('');
|
||||
});
|
||||
|
||||
describe('RBAC Enabled', () => {
|
||||
describe('Export button', () => {
|
||||
const receivers: Receiver[] = [
|
||||
{
|
||||
name: 'with receivers',
|
||||
grafana_managed_receiver_configs: [mockGrafanaReceiver('googlechat'), mockGrafanaReceiver('sensugo')],
|
||||
},
|
||||
{
|
||||
name: 'no receivers',
|
||||
},
|
||||
];
|
||||
|
||||
const notifiers: NotifierDTO[] = [mockNotifier('googlechat', 'Google Chat'), mockNotifier('sensugo', 'Sensu Go')];
|
||||
|
||||
it('should be visible when user has permissions to read provisioning', async () => {
|
||||
enableRBAC();
|
||||
grantUserPermissions([AccessControlAction.AlertingProvisioningRead]);
|
||||
|
||||
await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||
expect(buttons).toHaveLength(2);
|
||||
expect(buttons).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: 'false',
|
||||
name: 'with receivers',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: 'false',
|
||||
name: 'no receivers',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
it('should be visible when user has permissions to read provisioning with secrets', async () => {
|
||||
enableRBAC();
|
||||
grantUserPermissions([AccessControlAction.AlertingProvisioningReadSecrets]);
|
||||
|
||||
await renderReceieversTable(receivers, notifiers, GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
const buttons = within(screen.getByTestId('dynamic-table')).getAllByTestId('export');
|
||||
expect(buttons).toHaveLength(2);
|
||||
expect(buttons).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: 'true',
|
||||
name: 'with receivers',
|
||||
}),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
href: createUrl(`http://localhost/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: 'true',
|
||||
name: 'no receivers',
|
||||
}),
|
||||
}),
|
||||
])
|
||||
);
|
||||
});
|
||||
it('should not be visible when user has no provisioning permissions', async () => {
|
||||
enableRBAC();
|
||||
grantUserPermissions([AccessControlAction.AlertingNotificationsRead]);
|
||||
|
||||
await renderReceieversTable(receivers, [], GRAFANA_RULES_SOURCE_NAME);
|
||||
|
||||
const buttons = within(screen.getByTestId('dynamic-table')).queryAllByTestId('export');
|
||||
expect(buttons).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,6 +69,7 @@ interface ActionProps {
|
||||
delete: AccessControlAction;
|
||||
provisioning: {
|
||||
read: AccessControlAction;
|
||||
readSecrets: AccessControlAction;
|
||||
};
|
||||
};
|
||||
alertManagerName: string;
|
||||
@@ -89,17 +90,20 @@ function ViewAction({ permissions, alertManagerName, receiverName }: ActionProps
|
||||
}
|
||||
|
||||
function ExportAction({ permissions, receiverName }: ActionProps) {
|
||||
const canReadSecrets = contextSrv.hasAccess(permissions.provisioning.readSecrets, isOrgAdmin());
|
||||
return (
|
||||
<Authorize actions={[permissions.provisioning.read]} fallback={isOrgAdmin()}>
|
||||
<Authorize actions={[permissions.provisioning.read, permissions.provisioning.readSecrets]}>
|
||||
<ActionIcon
|
||||
data-testid="export"
|
||||
to={createUrl(`/api/v1/provisioning/contact-points/export/`, {
|
||||
download: 'true',
|
||||
format: 'yaml',
|
||||
decrypt: isOrgAdmin().toString(),
|
||||
decrypt: canReadSecrets.toString(),
|
||||
name: receiverName,
|
||||
})}
|
||||
tooltip={isOrgAdmin() ? 'Export contact point' : 'Export redacted contact point'}
|
||||
tooltip={
|
||||
canReadSecrets ? 'Export contact point with decrypted secrets' : 'Export contact point with redacted secrets'
|
||||
}
|
||||
icon="download-alt"
|
||||
target="_blank"
|
||||
/>
|
||||
@@ -301,7 +305,10 @@ export const ReceiversTable = ({ config, alertManagerName }: Props) => {
|
||||
const [showCannotDeleteReceiverModal, setShowCannotDeleteReceiverModal] = useState(false);
|
||||
|
||||
const isGrafanaAM = alertManagerName === GRAFANA_RULES_SOURCE_NAME;
|
||||
const showExport = isGrafanaAM && contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin());
|
||||
const showExport =
|
||||
isGrafanaAM &&
|
||||
(contextSrv.hasAccess(permissions.provisioning.read, isOrgAdmin()) ||
|
||||
contextSrv.hasAccess(permissions.provisioning.readSecrets, isOrgAdmin()));
|
||||
|
||||
const onClickDeleteReceiver = (receiverName: string): void => {
|
||||
if (isReceiverUsed(receiverName, config)) {
|
||||
@@ -436,6 +443,7 @@ function useGetColumns(
|
||||
delete: AccessControlAction;
|
||||
provisioning: {
|
||||
read: AccessControlAction;
|
||||
readSecrets: AccessControlAction;
|
||||
};
|
||||
},
|
||||
isVanillaAM: boolean
|
||||
@@ -498,7 +506,12 @@ function useGetColumns(
|
||||
label: 'Actions',
|
||||
renderCell: ({ data: { provisioned, name } }) => (
|
||||
<Authorize
|
||||
actions={[permissions.update, permissions.delete, permissions.provisioning.read]}
|
||||
actions={[
|
||||
permissions.update,
|
||||
permissions.delete,
|
||||
permissions.provisioning.read,
|
||||
permissions.provisioning.readSecrets,
|
||||
]}
|
||||
fallback={isOrgAdmin()}
|
||||
>
|
||||
<div className={tableStyles.actionsCell}>
|
||||
|
||||
@@ -50,6 +50,7 @@ export const notificationsPermissions = {
|
||||
|
||||
export const provisioningPermissions = {
|
||||
read: AccessControlAction.AlertingProvisioningRead,
|
||||
readSecrets: AccessControlAction.AlertingProvisioningReadSecrets,
|
||||
write: AccessControlAction.AlertingProvisioningWrite,
|
||||
};
|
||||
|
||||
@@ -125,6 +126,8 @@ export function getRulesAccess() {
|
||||
rulesSourceName === GRAFANA_RULES_SOURCE_NAME ? contextSrv.hasEditPermissionInFolders : contextSrv.isEditor;
|
||||
return contextSrv.hasAccess(getRulesPermissions(rulesSourceName).update, permissionFallback);
|
||||
},
|
||||
canReadProvisioning: contextSrv.hasAccess(provisioningPermissions.read, isOrgAdmin()),
|
||||
canReadProvisioning:
|
||||
contextSrv.hasAccess(provisioningPermissions.read, isOrgAdmin()) ||
|
||||
contextSrv.hasAccess(provisioningPermissions.readSecrets, isOrgAdmin()),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,6 +118,7 @@ export enum AccessControlAction {
|
||||
AlertingNotificationsExternalRead = 'alert.notifications.external:read',
|
||||
|
||||
// Alerting provisioning actions
|
||||
AlertingProvisioningReadSecrets = 'alert.provisioning.secrets:read',
|
||||
AlertingProvisioningRead = 'alert.provisioning:read',
|
||||
AlertingProvisioningWrite = 'alert.provisioning:write',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user