Skip to content

fix(server): user system review — transactional last-admin guard and role disclosure#157

Merged
ZingerLittleBee merged 7 commits into
mainfrom
worktree/brave-forest-38d8
Jul 7, 2026
Merged

fix(server): user system review — transactional last-admin guard and role disclosure#157
ZingerLittleBee merged 7 commits into
mainfrom
worktree/brave-forest-38d8

Conversation

@ZingerLittleBee

Copy link
Copy Markdown
Owner

Summary

Outcome of a design review of the two-tier RBAC user system (admin/member). Six decisions were settled and recorded in docs/superpowers/specs/2026-07-07-user-system-design-review.md; this PR ships the one code fix plus the disclosure follow-ups.

Fix

  • Transactional last-admin guard (crates/server/src/service/user.rs): the count(role='admin') check and the subsequent mutation ran outside a shared transaction, so two concurrent demotions/deletions could both pass the guard and leave the system with zero admins — unrecoverable in-product, since init_admin only re-bootstraps an empty users table. update_user and delete_user now run guard + mutation in one transaction (and delete_user's multi-table cleanup becomes atomic).

Role disclosure (web + iOS + docs)

Member visibility is fleet-wide by design (no per-server scoping; external exposure belongs to status pages). That boundary is now stated where it matters:

  • Web: create-user dialog shows a role hint under the selector (EN/ZH)
  • iOS: create-user sheet Role section footer mirrors the same copy (String Catalog, en + zh-Hans), plus DEBUG verification hooks (SB_UITEST_ADMIN=users, SB_UITEST_PRESENT=users-create)
  • Docs: admin guide documents the member boundary and a lost-admin recovery procedure (wipe users table → restart re-triggers the bootstrap banner); security guide + ENV.md warn that oauth.allow_registration with a public provider grants fleet-wide read access to every provider user

Review decisions (recorded in the spec)

# Topic Outcome
1 Member scoping Keep global read-only; external exposure → status pages; disclose in UI/docs
2 Forced password change for created users Keep as-is (admin fully trusted)
3 Live WS revalidation after role change Known boundary, no code change
4 OAuth auto-registration blast radius Docs warning only, no allowlist
5 Last-admin guard race Fixed (transactional)
6 Admin lockout recovery Documented, no CLI

Verification

  • cargo test --workspace green (~295 tests; the 5 spa_* failures seen initially were a missing local apps/web/dist, resolved by building the bundle)
  • cargo clippy clean; web vitest 93 files / 663 tests green; bun run typecheck + ultracite clean
  • Web dialog visually verified in the browser (member/admin hints)
  • iOS build succeeded (SwiftLint clean); simulator screenshots verified EN member/admin hints and zh-Hans rendering

…elete

The count(role='admin') check and the subsequent mutation ran outside a
shared transaction, so two concurrent demotions/deletions could both pass
the guard and leave the system with zero admins — a state init_admin
cannot recover because it only re-bootstraps an empty users table.
Wrapping each path in one transaction lets SQLite serialize the writers
so the second request re-reads the count and is correctly rejected; the
multi-table cleanup in delete_user becomes atomic as a bonus.
Spell out in the dialog that a member sees all servers' monitoring data
(security events and public IPs included) with no write access, and
point at status pages for exposing selected servers to outsiders, so
member accounts aren't mistaken for scoped/customer-facing access.
…nd admin recovery

- Admin guide: state that member visibility is fleet-wide with no
  per-server scoping, and direct external exposure to status pages
- Admin guide: add a lost-admin recovery procedure (wipe users table,
  restart to re-trigger the bootstrap banner) with its side effects
- Security guide + ENV.md: warn that oauth allow_registration with a
  public provider grants fleet-wide read access to every provider user
Six decisions from a design grilling of the two-tier RBAC user system:
member positioning, no forced password change, WS revalidation declined,
oauth registration handled via docs, transactional last-admin guard
(fixed), and documented admin lockout recovery.
Mirror the web create-user dialog: the Role section footer now states
that a member sees all servers' monitoring data (security events and
public IPs included) with no write access and points at status pages
for exposing selected servers to outsiders, while the admin option
spells out full-control scope. Three new String Catalog keys with
zh-Hans translations.

Also adds DEBUG verification hooks following the existing pattern:
SB_UITEST_ADMIN=users pushes the Users screen and
SB_UITEST_PRESENT=users-create auto-opens the create sheet, since the
'+' navbar button is unreachable for the headless cliclick harness.
@ZingerLittleBee
ZingerLittleBee merged commit 988d8b7 into main Jul 7, 2026
4 checks passed
@ZingerLittleBee
ZingerLittleBee deleted the worktree/brave-forest-38d8 branch July 7, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant