Implements the account and user-management system behind the new web UI against development repositories only. Nothing here connects to, reads, or modifies the production umbracontrol database, and no account is created in any approved environment. Account service and API: - Local temporary-password and OIDC enrollment, separated by type. - Argon2id credentials, must-change enforcement, server-side sessions with hashed tokens, idle and absolute deadlines, rotation and revocation. - Administrator account management: list, get, create, suspend, reactivate, deactivate, revoke sessions, org-scoped audit view. - Hidden-prompt operator command for the first administrator, refusing production mode and refusing to run without a TTY. Temporary passwords are never accepted from a client. The request schema has no password field and refuses unknown fields; the administrator path generates the one-time password in process, hashes it immediately, and returns it once in the creation response. The account can reach no route, including /api/auth/me, until it replaces that password, after which the one-time value no longer authenticates. Security posture: fail-closed configuration, Secure cookies in production, double-submit CSRF, constant-time token comparison, generic authentication errors, audit detail allow-listing, and logger-tree secret redaction. Reviewed but unapplied: migrations/0001_phase1_identity.sql adds users.username, a separate user_credentials table, and additive session columns, with a rollback and open review questions. The PostgreSQL adapter is deliberately absent, so the application refuses to start in production rather than run against an unreviewed store. Verification: 169 tests pass (148 new, 21 pre-existing); ruff check, ruff format, compileall, and make web-check are clean.
5.9 KiB
UmbraTrans web account management — phase-1 identity slice
First backend slice for docs/umbratrans-backend-design.md: accounts, both
enrollment modes, Argon2id credentials, server-side sessions, administrator
lifecycle actions, and sanitized audit events, plus a FastAPI application
factory.
This slice does not connect to PostgreSQL. The reviewed migration in
migrations/ has not been applied, and the production repository adapter is
deliberately absent, so the application fails closed instead of starting
against an unreviewed store.
Layout
| Module | Responsibility |
|---|---|
models.py |
Frozen dataclasses and enums. Hash fields are repr=False; AccountView is the only account shape that may cross the API. |
security.py |
Argon2id hashing, password policy, session/CSRF tokens, log and audit redaction. |
config.py |
Fail-closed settings. Production requires secure cookies, CSRF, and a non-development repository. |
errors.py |
Domain errors with stable codes. Messages are generic so responses cannot enumerate accounts. |
repository.py |
AccountRepository protocol plus the thread-safe in-memory development repository. |
dev_repository.py |
Optional local JSON store: 0600 file, 0700 directory, atomic writes, refuses production. |
service.py |
AccountService: enrollment, authentication, first-password change, sessions, administration, audit. |
schemas.py |
Pydantic request/response models. Request models forbid unknown fields. |
app.py |
Application factory, security middleware, CSRF, error mapping, dependencies. |
routes_auth.py, routes_uadmin.py |
/api/auth/* and /api/uadmin/*. |
cli.py |
Hidden-prompt operator command and a loopback development server. |
migrations/ |
Proposed, unapplied phase-1 identity SQL and its rollback. |
Enrollment boundary
Both modes are separated by type rather than by convention, and neither accepts a password from a client:
temporary_password, operator path:AccountService.create_accountrequires anOperatorContext, which only the operator CLI builds, plus amust_change_password = TrueArgon2id credential. The plaintext exists only inside the hidden prompt call and is never persisted, logged, or audited.temporary_password, administrator path:AccountService.provision_admin_temporary_passwordgenerates the one-time password in process, validates it against the local policy, hashes it, and returns it once in the creation response asIssuedTemporaryPassword. The value is excluded from that object'sreprand is not retrievable again by any endpoint.oidcrequires an authenticated administrator principal and stores no local password. Anoidccredential with a hash is rejected by the model.
CreateAccountRequest has no password field and refuses extra fields, so a
submitted password is rejected by the schema before a handler runs. Account
creation returns CreateAccountResponse, whose temporary_password field is
populated only for the generated-password mode.
API
GET /api/health
POST /api/auth/login
POST /api/auth/change-first-password
POST /api/auth/logout
POST /api/auth/logout-everywhere
GET /api/auth/me
GET /api/uadmin/accounts
POST /api/uadmin/accounts (either enrollment mode)
GET /api/uadmin/accounts/{id}
PATCH /api/uadmin/accounts/{id}/status
POST /api/uadmin/accounts/{id}/revoke-sessions
GET /api/uadmin/audit
- Sessions are server-side. The cookie holds a random token; only its hash is
stored. The cookie is
HttpOnly,SameSite=Lax, andSecurewhenever production settings are used. - CSRF uses the double-submit pattern: the readable
umbratrans_csrfcookie plus anX-CSRF-Tokenheader compared in constant time.POST /api/auth/loginis exempt because it carries credentials an attacker cannot know. A safe request (for exampleGET /api/healthorGET /api/auth/me) issues the cookie and returns the token. - No CORS middleware is installed; the API is same-origin behind the approved reverse proxy.
- Error bodies are
{"error": {"code", "message"}}. Validation failures report field names only, because FastAPI's default payload would echo a submitted password. POST /api/auth/loginreturns the CSRF token;GET /api/auth/mereturns it again, so a client can bootstrap without a separate endpoint.
Operator bootstrap
cd /home/umbra/src/UmbraTrans
UMBRA_ENV=development \
/home/umbra/.local/bin/uv run umbratrans-bootstrap-admin \
--username Umbraadmin --repository-path .local/data/development/umbratrans-web/dev-accounts.json
The command reads the one-time password twice from a hidden TTY prompt, refuses to run without a terminal, refuses production mode, and writes only to the local development store. Nothing it prints contains the password. It has not been run in this workspace.
python -m umbratrans_web bootstrap-admin is equivalent. python -m umbratrans_web serve starts the development API on 127.0.0.1 and refuses a
non-loopback bind or a production environment.
Development
cd /home/umbra/src/UmbraTrans
/home/umbra/.local/bin/uv sync --python /usr/bin/python3
/home/umbra/.local/bin/uv run pytest -q
/home/umbra/.local/bin/uv run ruff check .
/home/umbra/.local/bin/uv run ruff format --check .
/home/umbra/.local/bin/uv run python -m compileall -q umbratrans_web
The suite uses the in-memory repository and a reduced Argon2 cost for speed;
tests/test_security.py asserts the production parameter profile separately.
Test passwords are generated per run, so no fixture file carries credential
material.
Not in this slice
- The PostgreSQL adapter, the OIDC callback and subject mapping, MFA enforcement, project membership, invitations, and audit export.
- Connection to
umbracontrol, application ofmigrations/0001_*, and the first administrator in any approved environment. - Serving the static prototype:
web/is unchanged and is not mounted.