Summary
The last-admin protection has a time-of-check/time-of-use race. checkLastAdminConstraint (used by UpdateUser) and DeleteUser both run CountGroupMembers in the service layer, then perform the UpdateUser/DeleteUser write in a separate statement with no surrounding transaction or row lock. There is no DB-level "≥1 Administrators member" constraint (only the per-user cardinality(group_ids) >= 1 CHECK).
Two concurrent privileged requests, each removing a different one of the last two admins, can both observe count == 2, both pass the guard, and leave the system with zero admins.
Severity is LOW (narrow window, requires two simultaneous privileged actors), but the outcome (no admins) is unrecoverable without DB surgery.
Surfaced by the post-merge security review of #912 (2026-06-02), finding LOW-1.
Fix options
- Perform the count + mutation inside a single transaction with
SELECT ... FOR UPDATE on the Administrators-group members, OR
- Add a deferred DB constraint / trigger enforcing at least one Administrators-group member.
Add a regression test exercising concurrent removal of the last two admins.
Location
internal/auth/service_user.go (checkLastAdminConstraint, DeleteUser).
Summary
The last-admin protection has a time-of-check/time-of-use race.
checkLastAdminConstraint(used byUpdateUser) andDeleteUserboth runCountGroupMembersin the service layer, then perform theUpdateUser/DeleteUserwrite in a separate statement with no surrounding transaction or row lock. There is no DB-level "≥1 Administrators member" constraint (only the per-usercardinality(group_ids) >= 1CHECK).Two concurrent privileged requests, each removing a different one of the last two admins, can both observe
count == 2, both pass the guard, and leave the system with zero admins.Severity is LOW (narrow window, requires two simultaneous privileged actors), but the outcome (no admins) is unrecoverable without DB surgery.
Surfaced by the post-merge security review of #912 (2026-06-02), finding LOW-1.
Fix options
SELECT ... FOR UPDATEon the Administrators-group members, ORAdd a regression test exercising concurrent removal of the last two admins.
Location
internal/auth/service_user.go(checkLastAdminConstraint,DeleteUser).