From 9b10663c104802dea1e4a309041b1ba49e5a0176 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Sun, 6 Sep 2026 15:55:55 +0000 Subject: [PATCH] MultiSite.md: describe only current state and design rationale Drop all before/after contrasts, deferred ideas, and possible follow-ups (Identity layer, per-realm remote-auth policy flag, same-device redirect variant, legacy-module deletion note). --- docs/MultiSite.md | 71 ++++++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 41 deletions(-) diff --git a/docs/MultiSite.md b/docs/MultiSite.md index c69915a..bee0a0b 100644 --- a/docs/MultiSite.md +++ b/docs/MultiSite.md @@ -17,15 +17,15 @@ global users/orgs, N realms. **Global (single instance, shared across realms):** -| Data | Notes | -| ------------------------------- | -------------------------------------------------------------- | -| Organizations, Roles, Users | unchanged structs | -| Permissions | `domain` field host-scopes effectiveness | -| Sessions | host-bound (`Session.host`, exact match) | -| Credentials/passkeys | global collection, each stamped with its `rp_id` | -| Reset tokens | user-bound; global | -| Avatars | `paskia.data/users//profile.webp` | -| Auth codes, remote-auth manager | in-memory; carry rp-id fields | +| Data | Notes | +| ------------------------------- | -------------------------------------------------------- | +| Organizations, Roles, Users | one global collection | +| Permissions | `domain` field host-scopes effectiveness | +| Sessions | host-bound (`Session.host`, exact match) | +| Credentials/passkeys | global collection, each stamped with its `rp_id` | +| Reset tokens | user-bound; global | +| Avatars | `paskia.data/users//profile.webp` | +| Auth codes, remote-auth manager | in-memory; carry rp-id fields | **Per-realm (registry, keyed by rp-id):** @@ -36,13 +36,11 @@ global users/orgs, N realms. | `site_url`/`site_path` | runtime derivation, per realm | | OIDC provider (keys, clients) | per rp-id — each realm is an independent issuer | -The key simplification: `db.data()` stays a plain global singleton. A -contextvar is needed only for the **current realm** (passkey, config, -OIDC view) — not for database access. +`db.data()` is a plain global singleton. A contextvar is needed only for +the **current realm** (passkey, config, OIDC view) — not for database +access. -A deferred idea remains an Identity layer above the org-owned User -(`Identity → N org memberships + N credentials`). The architectural rule -is already in place: **authentication establishes identity, not +The architectural rule: **authentication establishes identity, not organization** — the requested hostname selects the org/permission context after authentication (via `Permission.domain` host-scoping and session host binding). @@ -73,7 +71,7 @@ the WS. On top of that: - Origin rule: an origin is valid if it is in the rp-id subtree **or explicitly listed in the realm's configured origins**. Explicit listing - is the trust boundary (`allowed_origins` was already an allow-list). + is the trust boundary. - `GET /.well-known/webauthn` on the canonical rp-id host serves `{"origins": [...]}` from the realm's configured non-subdomain origins (404 when there are none). @@ -98,8 +96,7 @@ a cap (default 5) on related origins per realm. (2.B) and later consolidates to Related Origins re-enrolls: authenticate against the old realm (or via 2.C), register a new credential under the common rp-id, retire the old one. The per-credential rp-id badge (§9) -makes this visible. No automated credential migration is provided or -needed. +makes this visible. There is no automated credential migration. ### 2.B Multiple rp-id realms under one administrative instance @@ -311,8 +308,8 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No `RemoteAuthRequest.rp_id` — stamping them with the permitter's realm would break every cross-realm remote login. Registration-flow and OIDC codes stamp from the current realm (issue and redeem sides always - match). The host re-check at set-session already binds `CookieCode` - independently; the rp_id check is additive. + match). The host re-check at set-session independently binds + `CookieCode` to the host; the rp_id check complements it. ### 7.2 Remote authentication — cross-realm permits allowed @@ -325,14 +322,9 @@ effective_auth_host(realm) = realm.auth_host or first_configured_auth_host or No request's rp-id is shown to the permitting user ("device at app2.com requests login"). - No policy flag: cross-realm remote login is how the product works - (users are global). A future per-realm policy field can add isolation. + (users are global). - Exchange codes stay single-use, 60s, host-bound at redemption. -A possible follow-up is a same-device redirect variant ("logged in at the -auth host, bounce to the app host with a code"): reuse `CookieCode` with -`redirect_uri` + `state`; redeem at the target host's set-session as -today. - ## 8. OIDC: per-realm providers in one DB - `DB.oidc` is `dict[str, OIDC]` keyed by rp-id. Each realm is an @@ -360,9 +352,8 @@ today. - `GET /auth/api/settings`: per-request-Host realm values (rp_id, rp_name, effective `auth_host`, `own_auth_host`, site URLs). -- **Realm management (master admin only, `auth:admin`)** — how new rp-ids - are added after bootstrap, mirroring how rp-name was always edited post - setup: +- **Realm management (master admin only, `auth:admin`)** — how rp-ids are + managed after bootstrap: - `GET/POST /auth/api/admin/realms/` and `PATCH/DELETE /auth/api/admin/realms/{rp_id}`. Writes require recent authentication (5 minutes). @@ -388,16 +379,15 @@ today. - Credential listings: `Credential.rp_id` serializes automatically into user-info and admin user detail responses; the frontend shows an rp-id badge **only when `credential.rp_id !== settings.rp_id`** — single- - realm installs see no change. + realm installs never show a badge. - **Enrollment prompt (2.C)**: when the user has no passkey for the current realm, the profile view offers to add one (a fresh remote-auth session satisfies the recent-auth requirement of registration). - Bootstrap/reset links use the default realm's URL. -- **Bootstrap caveat handled**: the "admin has no credentials" startup - check tests for an admin credential **under the default realm's - rp-id** — with global users an admin may have passkeys only under - another realm, and the printed link must still be usable for the - default realm. +- **Bootstrap check**: the "admin has no credentials" startup check tests + for an admin credential **under the default realm's rp-id** — with + global users an admin may have passkeys only under another realm, and + the printed link must still be usable for the default realm. ## 10. Storage @@ -418,15 +408,14 @@ today. as an explicit pre-flight step in the serve command, before the read-only startup open — read-only opens never trigger adoption or writes. -- The legacy database's structs are kept in a separate module - (`paskia/db/legacy.py`) that will be deleted once adoption is no longer - supported. No multi-database merging exists or is needed. +- The legacy database's structs live in a separate module + (`paskia/db/legacy.py`). There is no multi-database merging. - The startup box prints per-realm lines. ## 11. Lifespan and background tasks - One `Kanta` for `paskia.kantadb`, opened once in the lifespan; one - background cleanup task — unchanged in shape (DB is global). + background cleanup task (DB is global). - The kanta bootstrap hook only ever fires for a database created by `paskia init`; the serve command never bootstraps. - The registry is built from the stored `Config` after open; per-realm @@ -459,7 +448,7 @@ today. host dispatch, the well-known endpoint via the admin realm API, and a cross-realm remote login (request at test.localhost, permit at localhost, session valid on test.localhost) including the enrollment - prompt UI. Related Origins get **no browser e2e**: a genuine related + prompt UI. Related Origins have **no browser e2e**: a genuine related origin needs a non-subdomain host over HTTPS, and the browser fetches the well-known document itself — server-side coverage is in pytest (`tests/test_realms.py`). @@ -485,7 +474,7 @@ today. with registry-validated target host. - **Users/orgs global**: deleting a user/org cascades across all realms — intended. `auth:admin` is deployment-wide. Permission `domain` - host-scoping unchanged. + host-scopes effectiveness per host. - **Cross-realm permit transparency**: requesting realm/host shown to the approver; both sides logged. - **Secret hygiene in logs**: the OIDC signing-key censoring matches the