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' })
+})