Document real Auth0 OIDC + tenant-picker verification, closing §3a/§12
A free Auth0 developer tenant was wired into enterprise-auth via a local-only docker-compose.override.yml and driven through a real browser: login correctly failed closed with no tenant membership while still creating the users row, then succeeded after -grant-membership-* and issued a real session. With a second real membership granted, the multi-membership path landed on the real /select-tenant page, rendered both real tenants with correct roles via a real credentialed cross-origin request to the real enterprise-auth container, and selecting either one issued a session that POST /internal/authorize confirmed matched. Updates the runbook's top-level verification status, §3a, and §12, plus the threat model's "Read this first" finding and summary table to reflect what's now genuinely confirmed versus what still needs SAML's real IdP (§3b) or a real cluster (§7/§11).
This commit is contained in:
+63
-20
@@ -60,18 +60,29 @@ Docker-free testing could have caught:
|
|||||||
`/docs/security/threat-model.md`'s "Read this first" section for the
|
`/docs/security/threat-model.md`'s "Read this first" section for the
|
||||||
full account.
|
full account.
|
||||||
|
|
||||||
**What's still not verified, and why**: §3a (OIDC) and §3b (SAML) both
|
**What's still not verified, and why**: §3a (OIDC) and §12 (tenant
|
||||||
need a real external IdP (Auth0/Okta developer tenant, or similar) that
|
picker, both single- and multi-membership paths) are now closed --
|
||||||
this environment doesn't have credentials for -- the fake-IdP tests
|
verified against a real Auth0 developer tenant, full browser round
|
||||||
(real RS256/XML crypto, no stand-in shortcuts) are the strongest
|
trips, real session cookies authorizing correctly, including selecting
|
||||||
evidence available without one. §12's frontend-against-a-real-
|
between two real tenant memberships and getting back the right
|
||||||
`enterprise-auth`-container check is blocked by the same root cause:
|
tenant/role each time (see §3a and §12 below). That pass also found and
|
||||||
there's no way to land on `/select-tenant` with a genuine pending-login
|
fixed a real bug: `web/Dockerfile` only declared `ARG`/`ENV` for
|
||||||
cookie from the real binary without a real IdP redirecting through it
|
`VITE_API_BASE_URL`, so `docker-compose.yml`'s build args for
|
||||||
first. §7 and §11's live-cluster steps need `kind`/`kubectl`, which
|
`VITE_ALERTING_API_BASE_URL`/`VITE_ENTERPRISE_AUTH_BASE_URL` were
|
||||||
aren't installed in this environment -- their offline-only checks
|
silently dropped by Docker (an undeclared `--build-arg` is dropped, not
|
||||||
(`go build`/`go vet`/`go test`, `helm lint`, `helm template` + parsing
|
an error) -- `enterpriseAuthBase` came out `undefined` in the built
|
||||||
the rendered YAML) all pass and are documented as such below.
|
bundle, so the tenant-picker page threw "enterprise-auth is not
|
||||||
|
configured" against a real running container even though
|
||||||
|
`docker-compose.yml` looked correct. Fixed by declaring all three.
|
||||||
|
|
||||||
|
§3b (SAML) still needs a real external IdP with SAML app support, which
|
||||||
|
this environment doesn't have credentials for -- the fake-IdP test (real
|
||||||
|
XML signing/verification, no stand-in shortcuts) is the strongest
|
||||||
|
evidence available without one. §7 and §11's live-cluster steps need
|
||||||
|
`kind`/`kubectl`, which aren't installed in this environment -- their
|
||||||
|
offline-only checks (`go build`/`go vet`/`go test`, `helm lint`, `helm
|
||||||
|
template` + parsing the rendered YAML) all pass and are documented as
|
||||||
|
such below.
|
||||||
|
|
||||||
If you're reading this to decide whether Phase 4 is production-ready:
|
If you're reading this to decide whether Phase 4 is production-ready:
|
||||||
closer than before, but not yet -- see
|
closer than before, but not yet -- see
|
||||||
@@ -128,18 +139,30 @@ curl -s http://localhost:8082/auth/features
|
|||||||
# (no OIDC_ISSUER_URL/SAML_IDP_METADATA_URL set in this compose file)
|
# (no OIDC_ISSUER_URL/SAML_IDP_METADATA_URL set in this compose file)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 3a. `enterprise-auth`: human login via OIDC (new -- unlike everything
|
## 3a. `enterprise-auth`: human login via OIDC (now genuinely verified
|
||||||
else in this runbook, the underlying flow *was* verified live in this
|
against a real external IdP, not just a real fake one)
|
||||||
session, just not against a real running `enterprise-auth` container or
|
|
||||||
a real external IdP)
|
|
||||||
|
|
||||||
`enterprise/internal/loginhandler`'s tests already prove the mechanism
|
`enterprise/internal/loginhandler`'s tests already prove the mechanism
|
||||||
works end to end against a real fake IdP (`go test
|
works end to end against a real fake IdP (`go test
|
||||||
./internal/loginhandler/... -v` from `enterprise/`, no Docker needed --
|
./internal/loginhandler/... -v` from `enterprise/`, no Docker needed --
|
||||||
see `enterprise/README.md`). What's still unverified is wiring it into
|
see `enterprise/README.md`). **Closed for real in this pass**: a free
|
||||||
this actual running stack. To try that for real, point
|
Auth0 developer tenant was created, wired into a local-only
|
||||||
`docker-compose.yml`'s `enterprise-auth` service at a real OIDC IdP
|
`docker-compose.override.yml` (never committed -- see `.gitignore`), and
|
||||||
(a free Auth0/Okta developer tenant, or any IdP you control):
|
the full human login flow was driven through a real browser end to end:
|
||||||
|
`GET /auth/oidc/login` redirected to Auth0's real hosted login page; the
|
||||||
|
first attempt (before any `tenant_memberships` row existed) correctly
|
||||||
|
failed closed with "this identity has no tenant membership" while still
|
||||||
|
creating the `users` row via `UpsertUserBySSO`, exactly as designed;
|
||||||
|
after `-grant-membership-*` granted that real Auth0 identity
|
||||||
|
(`[email protected]`) an `admin` role on `acme`, a second login
|
||||||
|
completed the full OAuth code exchange, consent screen, and redirect to
|
||||||
|
`web`, landing a real `sentry_session` cookie; `POST
|
||||||
|
/internal/authorize` with that cookie returned
|
||||||
|
`{"tenant_id":"acme","user_id":"...","role":"admin"}` -- the exact
|
||||||
|
membership granted, round-tripped through a real external IdP, not a
|
||||||
|
stand-in. To reproduce, point `docker-compose.yml`'s `enterprise-auth`
|
||||||
|
service at a real OIDC IdP (a free Auth0/Okta developer tenant, or any
|
||||||
|
IdP you control):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Add to enterprise-auth's environment in docker-compose.yml (or a
|
# Add to enterprise-auth's environment in docker-compose.yml (or a
|
||||||
@@ -656,6 +679,26 @@ frontend-adjacent (`getAuthFeatures` on the settings page, existing
|
|||||||
Phase 0-3 routes) predates this runbook and was never re-verified here
|
Phase 0-3 routes) predates this runbook and was never re-verified here
|
||||||
either.
|
either.
|
||||||
|
|
||||||
|
**Since closed for real against the actual running containers**, not a
|
||||||
|
stand-in: `docker-compose.override.yml` (local-only, gitignored) pointed
|
||||||
|
`enterprise-auth` at the same real Auth0 developer tenant §3a used. Two
|
||||||
|
real tenants (`acme`, `globex`) each got a real `tenant_memberships` row
|
||||||
|
for the same Auth0 identity (`admin` and `viewer` respectively, via
|
||||||
|
`-grant-membership-*`). Logging in through `/auth/oidc/login` against
|
||||||
|
the real IdP landed on the real `/select-tenant` page, which rendered
|
||||||
|
both real memberships with their real display names and roles ("Acme
|
||||||
|
Corp -- Admin", "Globex Corporation -- Viewer") -- fetched via a real
|
||||||
|
credentialed cross-origin request to the real `enterprise-auth`
|
||||||
|
container, not the fake Node stand-in. Selecting "Globex Corporation"
|
||||||
|
fired a real `POST /auth/select-tenant`, redirected to `web`, and
|
||||||
|
`POST /internal/authorize` with the resulting cookie returned
|
||||||
|
`{"tenant_id":"globex","user_id":"...","role":"viewer"}` -- proving the
|
||||||
|
picker's selection genuinely determines the issued session's tenant, not
|
||||||
|
just that the UI renders correctly. This run is also what found and
|
||||||
|
fixed the `web/Dockerfile` build-arg bug described in this doc's top
|
||||||
|
"Verification status" section -- the picker was unreachable against the
|
||||||
|
real container until that fix landed.
|
||||||
|
|
||||||
## 13. Ingest tenant identity
|
## 13. Ingest tenant identity
|
||||||
|
|
||||||
The identity mechanism was chosen deliberately (config-supplied
|
The identity mechanism was chosen deliberately (config-supplied
|
||||||
|
|||||||
@@ -51,13 +51,21 @@ Neither engine does a live per-write check (a database/HTTP round trip
|
|||||||
per record would be a real throughput cost neither implementation
|
per record would be a real throughput cost neither implementation
|
||||||
accepts), so a minute-wide staleness window remains on both sides by
|
accepts), so a minute-wide staleness window remains on both sides by
|
||||||
design, not by oversight. What's left: two tenants (`acme`, `globex`)
|
design, not by oversight. What's left: two tenants (`acme`, `globex`)
|
||||||
have been provisioned and exercised end-to-end in this environment, but
|
have been provisioned and exercised end-to-end in this environment.
|
||||||
only via direct ClickHouse-user connections and Go integration tests —
|
Human login itself is now verified for real — a real Auth0 identity
|
||||||
not yet via a real logged-in human session walking through `POST
|
logged in via OIDC, selected between both tenants, and
|
||||||
/query` in a browser, since that still needs OIDC/SAML wired to a real
|
`POST /internal/authorize` confirmed each selection issued the right
|
||||||
external IdP (see §3a/§3b below). And **whether a given deployment
|
tenant/role (see §3a/§12 below) — but that walkthrough ran against plain
|
||||||
actually runs the isolated binaries** remains a deployment-time
|
`api` serving `web`'s traffic, not `enterprise-api`, so the specific
|
||||||
decision, not a code-level guarantee — see below.
|
combination of "real human OIDC session" and "real per-tenant ClickHouse
|
||||||
|
routing via `chrunner`" in the same request hasn't been driven end to
|
||||||
|
end yet; each half is independently confirmed (real ClickHouse
|
||||||
|
connections and Go integration tests for the routing half, real Auth0
|
||||||
|
sessions for the human-login half), just not together in one request.
|
||||||
|
SAML's human-login half still needs a real external IdP with SAML app
|
||||||
|
support (see §3b below). And **whether a given deployment actually runs
|
||||||
|
the isolated binaries** remains a deployment-time decision, not a
|
||||||
|
code-level guarantee — see below.
|
||||||
|
|
||||||
**ClickHouse (the SQL path) is built and now genuinely verified live.**
|
**ClickHouse (the SQL path) is built and now genuinely verified live.**
|
||||||
`enterprise/internal/tenantprovision` (real `CREATE DATABASE`/`CREATE
|
`enterprise/internal/tenantprovision` (real `CREATE DATABASE`/`CREATE
|
||||||
@@ -554,9 +562,9 @@ terms:
|
|||||||
| Ingest tenant *write-routing*, Tantivy | **Built and genuinely verified** — `search/src/consumer.rs` routes each record into its own tenant's index via `IndexRegistry`, same registry the (already-verified) read side uses; no Docker needed, real tests pass. Active-tenant-gated too: `tenants::ActiveTenantTracker` polls `enterprise-auth` every 60s (off unless configured), refusing any tenant not in the polled allowlist — same one-minute staleness bound as ClickHouse's now-refreshing snapshot, no more asymmetry between the two |
|
| Ingest tenant *write-routing*, Tantivy | **Built and genuinely verified** — `search/src/consumer.rs` routes each record into its own tenant's index via `IndexRegistry`, same registry the (already-verified) read side uses; no Docker needed, real tests pass. Active-tenant-gated too: `tenants::ActiveTenantTracker` polls `enterprise-auth` every 60s (off unless configured), refusing any tenant not in the polled allowlist — same one-minute staleness bound as ClickHouse's now-refreshing snapshot, no more asymmetry between the two |
|
||||||
| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced** — `api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO |
|
| Deployment actually routing traffic to `enterprise-api` (Helm) | **Enforced** — `api`/`enterprise-api` are mutually exclusive, same flag as RBAC/audit/SSO |
|
||||||
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Enforced, verified live** — `api`/`enterprise-api` are mutually exclusive via `COMPOSE_PROFILES`, same flag choice as Helm's `enterprise.enabled`; a real `docker compose up` of `enterprise-api` was run in this environment (and caught/fixed a startup-crashing duplicate `GET /healthz` route registration bug in the process), not just `docker compose config` |
|
| Deployment actually routing traffic to `enterprise-api` (docker-compose) | **Enforced, verified live** — `api`/`enterprise-api` are mutually exclusive via `COMPOSE_PROFILES`, same flag choice as Helm's `enterprise.enabled`; a real `docker compose up` of `enterprise-api` was run in this environment (and caught/fixed a startup-crashing duplicate `GET /healthz` route registration bug in the process), not just `docker compose config` |
|
||||||
| Human SSO login — OIDC | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
|
| Human SSO login — OIDC | **Enforced, verified live** — real login against a real Auth0 developer tenant, full browser round trip; correctly failed closed on an identity with no `tenant_memberships` row, then succeeded and issued a real session after `-grant-membership-*`, with `POST /internal/authorize` returning exactly the granted tenant/role |
|
||||||
| Human SSO login — SAML | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
|
| Human SSO login — SAML | **Built, verified with a real fake IdP** (not yet tried against a real external IdP) |
|
||||||
| Multi-tenant-membership login (tenant picker) | **Backend and frontend built and verified** (`GET /auth/memberships`, `POST /auth/select-tenant`, a pending-login token distinct from a real session; `web/src/routes/select-tenant` calls it via credentialed cross-origin fetch, genuinely exercised in a real browser) — not yet tried against a real running `enterprise-auth` container, still blocked on the same "no real external IdP" gap as OIDC/SAML above (there's no way to reach `/select-tenant` with a genuine pending-login cookie from the real binary without one) |
|
| Multi-tenant-membership login (tenant picker) | **Enforced, verified live** — a real Auth0 identity with two real tenant memberships (`acme` Admin, `globex` Viewer) landed on the real `/select-tenant` page against the real `enterprise-auth` container, rendered both with correct display names/roles via a real credentialed cross-origin `GET /auth/memberships`, and selecting either one issued a session that `POST /internal/authorize` confirmed matched — the selection genuinely determines the issued session's tenant, not just renders correctly. This pass also found and fixed a real bug: `web/Dockerfile` never declared `ARG`/`ENV` for `VITE_ALERTING_API_BASE_URL`/`VITE_ENTERPRISE_AUTH_BASE_URL`, so `docker-compose.yml`'s build args for them were silently dropped, leaving `enterpriseAuthBase` `undefined` in the built bundle |
|
||||||
| Per-resource dashboard grants (`own/granted`) | **Enforced, verified live** — real Postgres integration tests for `dashboard_permissions` CRUD and the `PermissionStore` adapter all pass (only when `enterprise-api` serves traffic — plain `api` falls back to own/Admin only) |
|
| Per-resource dashboard grants (`own/granted`) | **Enforced, verified live** — real Postgres integration tests for `dashboard_permissions` CRUD and the `PermissionStore` adapter all pass (only when `enterprise-api` serves traffic — plain `api` falls back to own/Admin only) |
|
||||||
| Query audit logging (routine queries) | **Enforced**, fail-open, and now wired to a real writer via `enterprise-api` (`audit.QueryAPILogger`) |
|
| Query audit logging (routine queries) | **Enforced**, fail-open, and now wired to a real writer via `enterprise-api` (`audit.QueryAPILogger`) |
|
||||||
| Audit log tamper detection (hash chain) | **Enforced**, verified live |
|
| Audit log tamper detection (hash chain) | **Enforced**, verified live |
|
||||||
|
|||||||
Reference in New Issue
Block a user