Skip to content

[Bug] Concurrent admin deletes can leave a deploy with zero admins (non-atomic last-admin check) #3303

Description

@gitnocknock

Description

The DELETE /auth/users/{user_id} route in omnigent/server/routes/accounts_auth.py enforces a "never leave zero admins" invariant with a check-then-act pattern split across two unlocked database transactions in omnigent/server/accounts_store.py:

  1. Read the current admin list (account_store.list_users()).
  2. If another admin exists, delete (account_store.delete_user(...)).

Nothing ties the read and delete together atomically, so the invariant is not actually enforced the way it is meant to be. If two delete requests target two different admins at the same time, each request's read sees the other as "the one remaining admin," both checks pass, and both deletes commit, leaving zero admins with no in-app recovery path. This succeeds silently (204 on both requests) and is exploitable on any deployment running more than one server process against the same database.

The invariant should make it impossible for concurrent requests to jointly leave zero admins. What actually happens is that both requests' "is there another admin" checks run before either delete commits, so each sees the other admin as still present and passes, both deletes apply, and list_users() ends up returning zero admins.

Steps to reproduce

  1. Seed a deploy with two admins, admin1 and admin2.
  2. Run more than one server process against the same database (e.g. two omnigent server instances behind a load balancer, or a deploy scaled past one replica). A single process won't reliably reproduce this over HTTP, since the route handler has no await between its read and its delete, so one process can't be preempted mid-check by another request.
  3. Send DELETE /auth/users/admin1 and DELETE /auth/users/admin2 to two different processes at the same time, so both requests' list_users() checks run before either delete commits.
  4. Each request's "is there another admin" check sees the other as still present, so both checks pass and both deletes apply via delete_user(...).
  5. GET /auth/users now shows zero admins, with no way to promote anyone back through the API.

Version

0.7.0.dev0

OS

macOs 15.5

Metadata

Metadata

Assignees

Labels

BugSomething isn't workingP0-criticalPriority: service down, data loss, security vulnerabilitycomp:serverComponent: server, API, session managementtriagedIssue has been triaged by the bot

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions