Alerting: Fix contact points scoped permission check (#108851)

* Fix scoped contact point permission check

* Use annotations permissions for k8s API and globals permissions for external AMs

* Fix scoped permission check for GrafanaReceiverForm
This commit is contained in:
Konrad Lalik
2025-07-31 09:17:42 +02:00
committed by GitHub
parent f1821988d9
commit ef80129bd9
4 changed files with 55 additions and 18 deletions
@@ -68,14 +68,14 @@ export const ContactPointHeader = ({ contactPoint, onDelete }: ContactPointHeade
*/
const isReferencedByAnything = usingK8sApi ? Boolean(numberOfPolicies || numberOfRules) : policies.length > 0;
/** Does the current user have permissions to edit the contact point? */
const hasAbilityToEdit = canEditEntity(contactPoint) || editAllowed;
const hasAbilityToEdit = usingK8sApi ? canEditEntity(contactPoint) : editAllowed;
/** Can the contact point actually be edited via the UI? */
const contactPointIsEditable = !provisioned;
/** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be edited? */
const canEdit = editSupported && hasAbilityToEdit && contactPointIsEditable;
/** Does the current user have permissions to delete the contact point? */
const hasAbilityToDelete = canDeleteEntity(contactPoint) || deleteAllowed;
const hasAbilityToDelete = usingK8sApi ? canDeleteEntity(contactPoint) : deleteAllowed;
/** Can the contact point actually be deleted, regardless of permissions? i.e. ensuring it isn't provisioned and isn't referenced elsewhere */
const contactPointIsDeleteable = !provisioned && !numberOfPoliciesPreventingDeletion && !numberOfRules;
/** Given the alertmanager, the user's permissions, and the state of the contact point - can it actually be deleted? */
@@ -198,12 +198,37 @@ describe('contact points', () => {
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(4);
const viewProvisioned = screen.getByTestId('view-action');
expect(viewProvisioned).toBeInTheDocument();
expect(viewProvisioned).toBeEnabled();
// Two contact points should have view buttons: grafana-default-email (cannot be edited) and provisioned-contact-point (provisioned)
const viewButtons = screen.getAllByRole('link', { name: /^view$/i });
expect(viewButtons).toHaveLength(2);
// Check view buttons by their href to verify which contact points they belong to
// The url is the same but the form should be readonly
expect(viewButtons[0]).toHaveAttribute('href', '/alerting/notifications/receivers/grafana-default-email/edit');
expect(viewButtons[1]).toHaveAttribute(
'href',
'/alerting/notifications/receivers/provisioned-contact-point/edit'
);
viewButtons.forEach((button) => {
expect(button).toBeEnabled();
});
// Three contact points should have edit buttons: lotsa-emails, Slack with multiple channels, OnCall Contact point
const editButtons = screen.getAllByRole('link', { name: /^edit$/i });
expect(editButtons).toHaveLength(3);
// Check edit buttons by their href to verify which contact points they belong to
expect(editButtons[0]).toHaveAttribute('href', '/alerting/notifications/receivers/lotsa-emails/edit');
expect(editButtons[1]).toHaveAttribute(
'href',
'/alerting/notifications/receivers/OnCall%20Conctact%20point/edit'
);
expect(editButtons[2]).toHaveAttribute(
'href',
'/alerting/notifications/receivers/Slack%20with%20multiple%20channels/edit'
);
const editButtons = screen.getAllByTestId('edit-action');
expect(editButtons).toHaveLength(4);
editButtons.forEach((button) => {
expect(button).toBeEnabled();
});
@@ -227,11 +252,11 @@ describe('contact points', () => {
expect(screen.getByRole('link', { name: 'add contact point' })).toHaveAttribute('aria-disabled', 'true');
// edit permission is based on API response - we should have 3 buttons
const editButtons = await screen.findAllByTestId('edit-action');
const editButtons = await screen.findAllByRole('link', { name: /^edit$/i });
expect(editButtons).toHaveLength(3);
// there should be view buttons though - one for provisioned, and one for the un-editable contact point
const viewButtons = screen.getAllByTestId('view-action');
const viewButtons = screen.getAllByRole('link', { name: /^view$/i });
expect(viewButtons).toHaveLength(2);
// check buttons in Notification Templates
@@ -329,7 +354,18 @@ describe('contact points', () => {
},
];
const { user } = renderWithProvider(<ContactPoint contactPoint={{ ...basicContactPoint, policies }} />);
// Add the necessary K8s annotations to allow deletion
const contactPointWithDeletePermission: ContactPointWithMetadata = {
...basicContactPoint,
metadata: {
annotations: {
[K8sAnnotations.AccessDelete]: 'true',
},
},
policies,
};
const { user } = renderWithProvider(<ContactPoint contactPoint={contactPointWithDeletePermission} />);
const moreActions = screen.getByRole('button', { name: /More/ });
await user.click(moreActions);
@@ -387,7 +423,7 @@ describe('contact points', () => {
const unusedBadge = screen.getAllByLabelText('unused');
expect(unusedBadge).toHaveLength(1);
const editButtons = screen.getAllByTestId('edit-action');
const editButtons = screen.getAllByRole('link', { name: /^edit$/i });
expect(editButtons).toHaveLength(2);
editButtons.forEach((button) => {
expect(button).toBeEnabled();
@@ -431,9 +467,9 @@ describe('contact points', () => {
expect(screen.queryByRole('link', { name: 'add contact point' })).not.toBeInTheDocument();
const viewProvisioned = screen.getByTestId('view-action');
expect(viewProvisioned).toBeInTheDocument();
expect(viewProvisioned).toBeEnabled();
const viewButton = screen.getByRole('link', { name: /^view$/i });
expect(viewButton).toBeInTheDocument();
expect(viewButton).toBeEnabled();
// check buttons in Notification Templates
const notificationTemplatesTab = screen.getByRole('tab', { name: 'Notification Templates' });
@@ -44,7 +44,8 @@ beforeEach(() => {
grantUserPermissions([AccessControlAction.AlertingNotificationsRead, AccessControlAction.AlertingNotificationsWrite]);
});
const getTemplatePreviewContent = async () => within(screen.getByTestId('template-preview')).findByTestId('mockeditor');
const getTemplatePreviewContent = async () =>
within(await screen.findByTestId('template-preview')).findByTestId('mockeditor');
const templatesSelectorTestId = 'existing-templates-selector';
@@ -135,9 +135,9 @@ export const GrafanaReceiverForm = ({ contactPoint, readOnly = false, editMode }
}
};
const isEditable = Boolean(
(!readOnly || (contactPoint && canEditEntity(contactPoint))) && !contactPoint?.provisioned
);
// If there is no contact point it means we're creating a new one, so scoped permissions doesn't exist yet
const hasScopedEditPermissions = contactPoint ? canEditEntity(contactPoint) : true;
const isEditable = !readOnly && hasScopedEditPermissions && !contactPoint?.provisioned;
const isTestable = !readOnly;
if (isLoadingNotifiers || isLoadingOnCallIntegration) {