feat(manager-core): admin auth gate (Phase 3a)
Closes the regression risk of the admin API and dashboard being open
to anyone reaching the bound port. Required foundation before v1.1
data-plane services land.
Per-user accounts (admin_users), Argon2id passwords, env-var bootstrap
of the first admin that becomes inert once any admin exists, opaque
32-byte session token doubling as bearer credential, 24h sliding TTL
configurable via PICLOUD_SESSION_TTL_HOURS. is_active column lets
admins be deactivated without losing audit history; last-active-admin
guard on DELETE and on PATCH that flips is_active to false (sessions
also wiped on deactivation).
require_admin middleware fronts every /api/v1/admin/* route. The data
plane (/api/v1/execute/{id}), /healthz, /version, and user routes
stay open. picloud admin reset-password <username> subcommand handles
recovery without going through HTTP.
Dashboard gains /admin/login and /admin/admins surfaces, a top-bar
user menu, and a token store with a localStorage echo so refreshes
don't sign you out. Cookie-based auth works in parallel for non-SPA
clients.
Forward compatibility: future RBAC tables (admin_roles,
admin_user_roles) join on admin_users.id; the auth middleware is the
seam where role checks slot in. Email, 2FA, passkeys, and personal
API tokens are all additive without touching admin_users.
Blueprint §11.4 updated to reflect what actually shipped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
33
crates/manager-core/migrations/0004_admin_auth.sql
Normal file
33
crates/manager-core/migrations/0004_admin_auth.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Phase 3a admin auth — see blueprint §11.4.
|
||||
--
|
||||
-- Per-user platform-operator accounts (distinct from the v1.1+ `users`
|
||||
-- table, which is for script-end users). Every authenticated admin is a
|
||||
-- full admin in this cut; role/permission tables will be added later
|
||||
-- without touching this schema.
|
||||
--
|
||||
-- `admin_sessions.token_hash` stores SHA-256 of the raw token; the raw
|
||||
-- value only ever exists in the login response, the HttpOnly cookie, and
|
||||
-- bearer-token requests. Cascade on user delete kills the user's sessions
|
||||
-- automatically — which is also why deactivating a user can simply wipe
|
||||
-- their rows instead of marking each session expired.
|
||||
|
||||
CREATE TABLE admin_users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_login_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE TABLE admin_sessions (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
user_id UUID NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
last_used_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX admin_sessions_user_idx ON admin_sessions (user_id);
|
||||
CREATE INDEX admin_sessions_expiry_idx ON admin_sessions (expires_at);
|
||||
Reference in New Issue
Block a user