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:
Yuri Tseretyan
2023-08-08 12:29:34 -04:00
committed by GitHub
parent e1d239a86e
commit 6b4a9d73d7
17 changed files with 347 additions and 104 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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',