feat: Remove plans
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,158 +0,0 @@
|
||||
# SaaS: Auth + Organizations — Design
|
||||
|
||||
**Date:** 2026-07-20
|
||||
**Status:** Approved (design) — ready for implementation planning
|
||||
**Scope:** Local auth + organizations + per-org OIDC, and org-scoping of existing data. Billing/plan-limits explicitly deferred. Fleet Inventory and Server Workflows are separate sub-projects.
|
||||
|
||||
---
|
||||
|
||||
## 1. Summary
|
||||
|
||||
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 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).
|
||||
|
||||
---
|
||||
|
||||
## 2. Locked decisions
|
||||
|
||||
| Topic | Decision |
|
||||
|-------|----------|
|
||||
| Primary auth | Local email + password (bcrypt). Replaces global Authentik. |
|
||||
| Org SSO | Per-org OIDC provider, configured by org admin, resolved dynamically at login. |
|
||||
| Isolation | `org_id` on every collection; every service query filtered by org. Enforced in the request layer via session→org. |
|
||||
| Roles | `owner`, `admin`, `member` (v1: owner/admin can manage users + org OIDC + all resources; member can use resources). Keep minimal. |
|
||||
| Bootstrapping | First-run creates the initial org + owner account (setup flow) when no users exist. |
|
||||
| Sessions | Keep existing Redis session store; session now carries `user_id`, `org_id`, `role`, `email`. |
|
||||
| Agent auth | Unchanged (per-server agent tokens). Servers gain `org_id`; agent RPCs resolve org from the server record. |
|
||||
| gRPC endpoint | Single shared `grpc-vantage.hostxtra.co.uk` — no per-org subdomain. Org resolved from `server_id`/token, never from host. Agent configs unchanged. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Data model
|
||||
|
||||
### `orgs`
|
||||
```json
|
||||
{ "_id":"ObjectId", "org_id":"uuid", "name":"Doms Org", "slug":"doms-org", "created_at":"ISODate" }
|
||||
```
|
||||
- `slug` derived from `name` at creation: lowercase, spaces/underscores → `-`, strip non `[a-z0-9-]`, collapse repeat `-`, trim leading/trailing `-`. `Doms Org` → `doms-org`.
|
||||
- **Unique index on `slug`** (global). On collision append `-2`, `-3`, … or reject and ask user to pick.
|
||||
- Length 3–40. Reserved slugs blocked: `www`, `api`, `app`, `admin`, `auth`, `install`, `static`, `_next`, plus the bare apex.
|
||||
- Slug is the DNS label → `doms-org.vantage.hostxtra.co.uk`. Treat as **immutable in v1** (rename breaks bookmarks, cookies, OIDC redirect URLs). Renaming deferred.
|
||||
|
||||
### `users`
|
||||
```json
|
||||
{
|
||||
"_id":"ObjectId", "user_id":"uuid", "org_id":"uuid",
|
||||
"email":"a@b.com", "password_hash":"bcrypt...", "role":"owner|admin|member",
|
||||
"auth_source":"local|oidc", "created_at":"ISODate", "last_login":"ISODate|null"
|
||||
}
|
||||
```
|
||||
Unique index on `email` (global — email identifies the account and its org).
|
||||
|
||||
### `org_oidc` (per-org provider config)
|
||||
```json
|
||||
{
|
||||
"_id":"ObjectId", "org_id":"uuid",
|
||||
"issuer":"https://id.acme.com", "client_id":"...",
|
||||
"client_secret_enc":"AES...", // encrypted with existing crypto.go
|
||||
"enabled": true, "updated_at":"ISODate"
|
||||
}
|
||||
```
|
||||
|
||||
### Existing collections — add `org_id`
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
## 4. Auth flows
|
||||
|
||||
### Local
|
||||
- `POST /auth/register` — only allowed during first-run bootstrap (creates org + owner) OR by an org admin inviting a user (see below). Not open self-serve.
|
||||
- `POST /auth/login` — email + password → verify bcrypt → create session with `{user_id, org_id, role, email}`.
|
||||
- `POST /auth/logout` — destroy session.
|
||||
- `GET /auth/me` — returns current user + org.
|
||||
|
||||
### Org-admin user management
|
||||
- `GET /api/org/users` / `POST /api/org/users` (create local user in caller's org) / `PUT /api/org/users/:id/role` / `DELETE /api/org/users/:id`.
|
||||
|
||||
### 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://<slug>.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://<host>/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.
|
||||
|
||||
### First-run bootstrap
|
||||
- `GET /auth/bootstrap-status` → `{ needs_setup: bool }` (true when `users` is empty).
|
||||
- Setup page collects org name + owner email/password → creates org + owner → session.
|
||||
|
||||
---
|
||||
|
||||
## 5. Request scoping
|
||||
|
||||
- `auth.Middleware` already loads the session; extend `Session` to include `OrgID`, `UserID`, `Role`. Add helper `auth.OrgID(c) string`.
|
||||
- **Every service function that reads/writes a scoped collection takes an `orgID` argument** and adds `"org_id": orgID` to its filter and on insert. Handlers pass `auth.OrgID(c)`.
|
||||
- Add a `requireRole(role)` gin middleware for admin-only routes (org user mgmt, org OIDC).
|
||||
- Agent-facing gRPC: resolve `org_id` from the `servers` record (already tied to `server_id`); inventory/keys/sync operate on that org implicitly.
|
||||
|
||||
### 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 (hits only; misses are never cached so a freshly bootstrapped org resolves immediately). The app root label (the `vantage` in `<slug>.vantage.<tld>`) 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.
|
||||
- Apex `vantage.hostxtra.co.uk` (no subdomain): serves bootstrap + login-by-email fallback; after login redirect to the user's org host.
|
||||
|
||||
---
|
||||
|
||||
## 6. Removing global Authentik
|
||||
|
||||
- Delete/retire env-driven `InitOIDC` global provider (`OIDC_ISSUER` etc.). Keep the `go-oidc`/`oauth2` machinery but move it behind the per-org resolver.
|
||||
- `authEnabled` global replaced by "auth always on" (there is always local auth). Update `middleware.go` accordingly (no more `if !authEnabled { next }` bypass — except the bootstrap endpoints and login/register which are unauthenticated).
|
||||
- Login page (`web/app/login` or existing) offers: email/password form + "Sign in with your organization's SSO" (enter org, redirect to `/auth/oidc/start`).
|
||||
|
||||
---
|
||||
|
||||
## 7. Migration
|
||||
|
||||
One-shot migration run at startup (idempotent):
|
||||
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 = <default>` 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.
|
||||
|
||||
---
|
||||
|
||||
## 8. Frontend
|
||||
|
||||
- **Login/Setup:** `web/app/login/page.tsx` (email/password + org SSO entry) and `web/app/setup/page.tsx` (first-run). Redirect logic based on `bootstrap-status` and `auth/me`.
|
||||
- **Org settings:** `web/app/settings/org/` — members list + invite/create user + role management; OIDC provider form (issuer/client id/secret/enabled).
|
||||
- Existing pages unchanged functionally but now implicitly org-scoped by the backend. Show current org + user in the sidebar/header.
|
||||
|
||||
---
|
||||
|
||||
## 9. Security
|
||||
|
||||
- Passwords: bcrypt (cost ≥ 12). Never returned.
|
||||
- Org OIDC client secret encrypted at rest (reuse `services/crypto.go` AES).
|
||||
- Cross-org access prevented at the service layer (org_id in every filter) — the primary isolation boundary. Handlers must never accept an `org_id` from the client; always derive from session.
|
||||
- OIDC callback must bind the returned identity to the org that initiated the flow (state carries org_id) to prevent org-mixing.
|
||||
- Role checks on all org-admin mutations.
|
||||
|
||||
---
|
||||
|
||||
## 10. Out of scope
|
||||
|
||||
- Billing, plans, seat/server limits.
|
||||
- Cross-org resource sharing, org switching for a single user (one user = one org in v1).
|
||||
- SCIM / directory sync, SAML.
|
||||
- Email delivery for invites (create-user sets a password or invite token; email sending deferred — document as manual/console output).
|
||||
- Tests (skipped, consistent with prior iterations).
|
||||
Reference in New Issue
Block a user