Phase 4 — P4-03/04: RBAC + user management #14
Reference in New Issue
Block a user
Delete Branch "p4-03-04-rbac-user-mgmt"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes P4-03 (RBAC enforcement at API layer) and P4-04 (user management UI). 30 commits, branched from main and rebased clean.
What's in
Three-role hierarchy (admin > operator > viewer) enforced via chi route-group middleware (`requireRole`). Admin is the fail-closed default — any unbanded route lands there. Agent endpoints (`/ws/agent`, `/api/agents/*`) stay on the bearer-token chain, untouched. Sessions re-validate `disabled_at` per request so admin-driven changes (disable, force-logout) land immediately.
Setup-token flow replaces temp passwords. Admin clicks `+ Add user`, picks username + email + role, server returns a one-time setup link valid for 1 hour (sha256-hashed at rest, raw shown to admin once). User clicks the link → sets a password (≥12 chars) → drops a session → lands on `/`. `Regenerate setup link` issues a new token, replacing the old via INSERT OR REPLACE. Expired tokens swept on the alert engine's 60s tick.
Disable-only lifecycle — soft delete via `disabled_at`. Last-admin guard rejects "disable last admin" and "demote last admin to non-admin" both server-side and UI-hinted. Disabled-username collision on Add User redirects to the existing user's edit page with an amber "Username already exists (disabled) — Re-enable / Pick different name" banner so the operator gets the right action in two clicks.
Self-service password change at `/settings/account` available to any role. Skips current-password check when `must_change_password` is set.
Sortable users list — Username / Email / Role / Last login are clickable headers with the audit-style ↑/↓ glyph; NULL emails / never-logged-in users sort to the bottom regardless of direction. Status column stays non-sortable (compound state). `Last login` populates on both `/api/auth/login` and `/setup` POST.
Schema
Two column-level migrations (CLAUDE.md preference):
Audit actions
`user.created`, `user.updated`, `user.disabled`, `user.enabled`, `user.password_changed`, `user.setup_completed`, `user.setup_token.regenerated`, `user.force_logout`. All target_kind=`user`.
Sweep verified (smoke env)
Out of scope (deferred — see spec)
OIDC (P4-05), email-the-link via SMTP, hard-delete, password complexity / rotation policy, lockout on failed login.
Test plan
Refs