fix(webapp): the last administrator can't be demoted or deleted (#835) #837

Merged
claude-bot merged 1 commit from fix/last-admin-guard into main 2026-09-26 14:39:15 +00:00
Collaborator

Closes #835.

The bug: PATCH /admin/users/{id}/admin had no checks. So the last administrator, whether yourself or someone else, could be demoted, leaving a server nobody can administer. Deleting the last admin's account went unchecked too.

The fix:

  • user_service.count_admins, which leaves system users out; deleted users are hard-deleted.
  • Demoting the only admin returns 409: "This is the only administrator. Make someone else an administrator first."
  • Deleting the only admin, through DELETE /me or DELETE /admin/users/{id}, returns 409. Both paths go through account_deletion_service.delete_user_account, which now raises LastAdminError before the ownership gate.
  • Deliberately not changed: demoting yourself while another admin exists. Whether an admin may step down is part of the pending D68 owner work (lane 9, A3).

Tests: tests/test_last_admin_guard.py has 6 tests. The 3 refusal tests failed before the fix. With the admin, users and account-deletion modules, 107 pass. ruff is clean.

🤖 Generated with Claude Code

Closes #835. **The bug:** `PATCH /admin/users/{id}/admin` had no checks. So the last administrator, whether yourself or someone else, could be demoted, leaving a server nobody can administer. Deleting the last admin's account went unchecked too. **The fix:** - `user_service.count_admins`, which leaves system users out; deleted users are hard-deleted. - **Demoting** the only admin returns 409: "This is the only administrator. Make someone else an administrator first." - **Deleting** the only admin, through `DELETE /me` or `DELETE /admin/users/{id}`, returns 409. Both paths go through `account_deletion_service.delete_user_account`, which now raises `LastAdminError` before the ownership gate. - **Deliberately not changed:** demoting yourself while another admin exists. Whether an admin may step down is part of the pending D68 owner work (lane 9, A3). **Tests:** `tests/test_last_admin_guard.py` has 6 tests. The 3 refusal tests failed before the fix. With the admin, users and account-deletion modules, 107 pass. ruff is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(webapp): refuse to demote or delete the last administrator (#835)
All checks were successful
CI / Docker image build (pull_request) Successful in 25s
CI / Bot/backend version sync (pull_request) Successful in 31s
CI / Backend lint (ruff) (pull_request) Successful in 33s
CI / Feedback site tests (pull_request) Successful in 47s
CI / Does this change touch the recording path? (pull_request) Successful in 27s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m25s
CI / Bot tests and audit (pull_request) Successful in 1m53s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 16m9s
CI / Backend migration, tests, and audit (pull_request) Successful in 18m3s
CI / Frontend tests, audit, and build (pull_request) Successful in 23m23s
f78e626129
PATCH /admin/users/{id}/admin had no checks: revoking admin from the
sole remaining administrator left a server nobody could administer.
Both this endpoint and the account-deletion service (self-service and
admin-initiated) now count administrators (excluding the system/
tombstone user) and refuse with 409 when the change would leave zero,
whether the target is the caller or someone else.

Tests in webapp/backend/tests/test_last_admin_guard.py written first
and confirmed failing against the pre-fix code.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.