diff --git a/app/forms/silo-access.tsx b/app/forms/silo-access.tsx index 6bc711230..dd1388c99 100644 --- a/app/forms/silo-access.tsx +++ b/app/forms/silo-access.tsx @@ -18,6 +18,9 @@ import { Access16Icon } from '@oxide/design-system/icons/react' import { ListboxField } from '~/components/form/fields/ListboxField' import { SideModalForm } from '~/components/form/SideModalForm' +import { HL } from '~/components/HL' +import { useCurrentUser } from '~/hooks/use-current-user' +import { confirmAction } from '~/stores/confirm-action' import { SideModalFormDocs } from '~/ui/lib/ModalLinks' import { ResourceLabel } from '~/ui/lib/SideModal' import { docLinks } from '~/util/links' @@ -86,6 +89,7 @@ export function SiloAccessEditUserSideModal({ policy, defaultValues, }: EditRoleModalProps) { + const { me } = useCurrentUser() const updatePolicy = useApiMutation(api.policyUpdate, { onSuccess: () => { queryClient.invalidateEndpoint('policyView') @@ -106,9 +110,31 @@ export function SiloAccessEditUserSideModal({ } onSubmit={({ roleName }) => { - updatePolicy.mutate({ - body: updateRole({ identityId, identityType, roleName }, policy), - }) + const body = updateRole({ identityId, identityType, roleName }, policy) + // Only silo admins can edit the policy, so an admin who removes their + // own admin role may not be able to undo the change. "May" because + // they could still be an admin through a group. + if ( + identityId === me.id && + defaultValues.roleName === 'admin' && + roleName !== 'admin' + ) { + confirmAction({ + actionType: 'danger', + doAction: () => updatePolicy.mutateAsync({ body }), + modalTitle: 'Remove your own admin role', + modalContent: ( +

+ You are removing the admin role from your own account. You may lose + the ability to manage access to this silo, including restoring your own + role. Are you sure? +

+ ), + errorTitle: 'Could not update role', + }) + return + } + updatePolicy.mutate({ body }) }} loading={updatePolicy.isPending} submitError={updatePolicy.error} diff --git a/test/e2e/silo-access.e2e.ts b/test/e2e/silo-access.e2e.ts index 720ef8ba1..3713a4082 100644 --- a/test/e2e/silo-access.e2e.ts +++ b/test/e2e/silo-access.e2e.ts @@ -80,3 +80,41 @@ test('Click through silo access page', async ({ page }) => { await page.getByRole('button', { name: 'Confirm' }).click() await expect(user3Row).toBeHidden() }) + +// changing your own role away from admin requires confirmation because you +// may not be able to undo it +test('Removing your own admin role requires confirmation', async ({ page }) => { + await page.goto('/access') + + const table = page.getByRole('table') + + // Hannah Arendt is the mock session user and a silo admin + const openEditForm = async () => { + await page + .getByRole('row', { name: 'Hannah Arendt' }) + .getByRole('button', { name: 'Row actions' }) + .click() + await page.getByRole('menuitem', { name: 'Change role' }).click() + await page.getByRole('radio', { name: /^Viewer / }).click() + await page.getByRole('button', { name: 'Update role' }).click() + } + const editForm = page.getByRole('dialog', { name: 'Edit role' }) + const confirmModal = page.getByRole('dialog', { name: 'Remove your own admin role' }) + + // confirmation modal pops up over the side modal. cancel out of it, then + // close the form and check nothing changed + await openEditForm() + await expect(confirmModal).toBeVisible() + await confirmModal.getByRole('button', { name: 'Cancel' }).click() + await expect(confirmModal).toBeHidden() + await expect(editForm).toBeVisible() + await editForm.getByRole('button', { name: 'Cancel' }).click() + await expectRowVisible(table, { Name: 'Hannah Arendt', Role: 'silo.admin' }) + + // this time, confirm + await openEditForm() + await confirmModal.getByRole('button', { name: 'Confirm' }).click() + + await expect(editForm).toBeHidden() + await expectRowVisible(table, { Name: 'Hannah Arendt', Role: 'silo.viewer' }) +})