diff --git a/docs/superpowers/specs/2026-07-20-saas-auth-orgs-design.md b/docs/superpowers/specs/2026-07-20-saas-auth-orgs-design.md index dd41306..71c86d3 100644 --- a/docs/superpowers/specs/2026-07-20-saas-auth-orgs-design.md +++ b/docs/superpowers/specs/2026-07-20-saas-auth-orgs-design.md @@ -11,7 +11,7 @@ Turn Vantage from a single-admin, single global-OIDC tool into a multi-tenant app: 1. **Replace** the global Authentik/env-based OIDC with **local email/password accounts** as the primary login. -2. **Organizations** — every user belongs to an org; every domain object (servers, keys, secrets, assignments, workflows, steps, runs, audit, monitor, notification) carries an `org_id` and all queries are scoped to the caller's org. +2. **Organizations** — every user belongs to an org; every domain object (servers, keys, secrets, assignments, workflows, steps, runs, audit logs, monitors, notification channels, console sessions, incidents, uptime rollups) carries an `org_id` and all queries are scoped to the caller's org. 3. **Per-org OpenID** — an org admin can configure their own OIDC provider (issuer/client id/secret); users in that org can then sign in through it. No billing, no seat/server limits this iteration (schema leaves room). @@ -65,7 +65,9 @@ Unique index on `email` (global — email identifies the account and its org). ``` ### Existing collections — add `org_id` -`servers`, `keys`, `assignments`, `secrets`, `workflows`, `workflow_steps`, `workflow_runs`, `audit` each gain `org_id string`. A **migration** backfills all existing documents into a default org (see §7). +`servers`, `keys`, `assignments`, `secrets`, `workflows`, `workflow_steps`, `workflow_runs`, `audit_logs`, `monitors`, `notification_channels`, `console_sessions`, `incidents`, `monitor_rollups` each gain `org_id string`. A **migration** backfills all existing documents into a default org (see §7). + +> The audit collection is named `audit_logs` and the notification channel collection `notification_channels`. Earlier drafts of this document called them `audit` and `channels`; those names were copied verbatim into the migration's scoped-collection list and silently skipped both collections. Use the real names. --- @@ -82,7 +84,7 @@ Unique index on `email` (global — email identifies the account and its org). ### Per-org OIDC - `GET/PUT /api/org/oidc` — read/save the caller org's provider config (admin only). Secret stored encrypted. UI shows the exact redirect URL the admin must register with their provider: `https://.vantage.hostxtra.co.uk/auth/oidc/callback`. -- `GET /auth/oidc/start` — org resolved from host (subdomain slug). Look up org's `org_oidc`, build provider on demand (cache per org). Redirect URL **derived from host** (`https:///auth/oidc/callback`), not stored. State carries `org_id`. +- `GET /auth/oidc/start` — org resolved from host (subdomain slug). Look up org's `org_oidc`, build provider on demand (cache per org). Redirect URL **derived from host** (`https:///auth/oidc/callback`), not stored. State carries `org_id`. The per-org provider cache is evicted when the org's OIDC config is saved, so a rotated issuer takes effect without a restart; the oauth2 config is built per request (its redirect URL is host-derived) and never cached. - `GET /auth/oidc/callback` — exchange code, match/provision the user by email **within that org**, create session. - If the email exists in the org → log in. If not → provision a `member` with `auth_source=oidc` (org admin can promote). Reject if email belongs to a different org. @@ -101,7 +103,7 @@ Unique index on `email` (global — email identifies the account and its org). ### Host-based org resolution (per-org subdomain) - Wildcard DNS `*.vantage.hostxtra.co.uk` + wildcard TLS cert (Let's Encrypt DNS-01). One record, one cert, no per-org ops. -- Middleware extracts subdomain label from `Host` header → look up `orgs.slug` → org. Cache slug→org_id. +- Middleware extracts subdomain label from `Host` header → look up `orgs.slug` → org. Cache slug→org_id (hits only; misses are never cached so a freshly bootstrapped org resolves immediately). The app root label (the `vantage` in `.vantage.`) is read from `APP_ROOT_LABEL`, defaulting to `vantage` — deployments on another root must set it or no host resolves to an org. - **Hostname is a routing/UX hint, NOT an authorization boundary.** Authorization stays session `org_id` (spec §9). If session org ≠ host org → reject (or redirect to correct host). Never trust `Host` to grant access. - `/auth/oidc/start` reads org from host — drops the "type your org" box. - Session cookie set on the **exact host** (`doms-org.vantage...`), not parent `.vantage...`, so cookies don't leak across orgs. @@ -120,8 +122,10 @@ Unique index on `email` (global — email identifies the account and its org). ## 7. Migration One-shot migration run at startup (idempotent): -1. If `orgs` is empty AND `servers`/`keys`/etc. contain documents without `org_id`: create a **default org** ("Default"). -2. Set `org_id = ` on all existing `servers`, `keys`, `assignments`, `secrets`, `workflows`, `workflow_steps`, `workflow_runs`, `audit` documents missing it. +1. If any scoped collection contains documents without `org_id`: reuse the org with slug `default`, creating it ("Default") if absent. Note this is looser than "`orgs` is empty AND ..." — the implementation is the authoritative and safer form, since an instance can have an org already (created by first-run bootstrap) while legacy documents still lack `org_id`; the stricter condition would skip the backfill and strand that data. +2. Set `org_id = ` on all existing `servers`, `keys`, `assignments`, `secrets`, `workflows`, `workflow_steps`, `workflow_runs`, `audit_logs`, `monitors`, `notification_channels` documents missing it. + - `console_sessions`, `incidents` and `monitor_rollups` are also scoped, but their org is derived from the owning `servers`/`monitors` record rather than defaulted (migration `0003`), since defaulting them would mix one org's console history and incident timeline into another's. + - Migration `0003` also re-runs step 2 for `audit_logs` and `notification_channels`: the original `0001` listed them under the wrong names (`audit`, `channels`) and wrote its marker regardless, so those documents need a second, separately-markered pass to converge. 3. If `OIDC_ISSUER` env was set previously and an admin email is known, optionally seed an owner user (documented manual step) — otherwise first-run bootstrap handles owner creation. Guard with a marker (e.g. a `migrations` collection entry) so it runs once.