Add username+password login for Owner/Operator staff accounts on op.console, the primary web login when Zero Trust is not in front of the API. Three handlers form the whole surface: login mints a server-side session cookie, logout revokes it idempotently, and change-password re-verifies the current password before rotating the hash and clearing must_change_password. - Session cookies are HttpOnly+Secure+SameSite=Lax, host-only, stored server-side as a SHA-256 hash with a 12h TTL. - Login is anti-enumeration: every failure runs a uniform bcrypt compare against a dummy hash and returns the same vague error. - Credential-bearing writes require Content-Type: application/json, returning 415 otherwise, to close the cross-site form-POST forgery vector as a belt to the SameSite cookie. - Local auth fails closed: login is rejected unless local_auth_enabled is set, so a Zero-Trust-only deployment never accepts a local password. - Extend the users table with a nullable password_hash and must_change_password; staff are role=admin rows with a hash, players are role=user rows with hash NULL. - /me now reports must_change_password so the panel can force a first-login change. Covered by Go unit tests (handlers, content-type guard, anti-enumeration, forced-change lockdown) and the OpenAPI route-parity gate.
34 lines
1.8 KiB
SQL
34 lines
1.8 KiB
SQL
-- Phase B local-password auth + sessions + runtime settings.
|
|
-- The web (op.console) authenticates Owner/Operator via username+password;
|
|
-- `felis breakGlass` (the sudo-only emergency TUI) mints/resets these directly
|
|
-- against Postgres so recovery works even when the API is down. Players keep
|
|
-- password_hash NULL (account-link identity only — see account_links).
|
|
|
|
-- Owner/Operator credentials live on the existing users row, not a separate
|
|
-- table: role=admin WITH a hash is staff; role=user with NULL hash is a player.
|
|
ALTER TABLE users
|
|
ADD COLUMN password_hash text, -- bcrypt; NULL for link-only players
|
|
ADD COLUMN must_change_password boolean NOT NULL DEFAULT false; -- force change-on-first-login
|
|
|
|
-- Server-set httpOnly session cookies. The remote path authenticates with a
|
|
-- stateless Cloudflare-Access JWT (no cookie); local-password auth needs its
|
|
-- own session. Store only the hash of the opaque cookie value, mirroring tokens.
|
|
CREATE TABLE sessions (
|
|
token_hash text PRIMARY KEY, -- sha-256(cookie value)
|
|
user_id text NOT NULL REFERENCES users(id),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
expires_at timestamptz NOT NULL,
|
|
revoked_at timestamptz -- non-NULL once invalidated
|
|
);
|
|
CREATE INDEX sessions_user_id_idx ON sessions (user_id);
|
|
|
|
-- Runtime security/platform settings as jsonb. The live API reads these from
|
|
-- Postgres per-request (NOT the read-only felis-config Secret), so the
|
|
-- break-glass TUI can flip toggles (e.g. local_auth_enabled) direct-to-DB
|
|
-- without patching the Secret and rolling the pod.
|
|
CREATE TABLE platform_settings (
|
|
key text PRIMARY KEY, -- e.g. 'local_auth_enabled'
|
|
value jsonb NOT NULL,
|
|
updated_at timestamptz NOT NULL DEFAULT now()
|
|
);
|