Skip to main content

Cross-tenant admin views without waiting for the upstream auth provider

The blocker

KAHN’s operator-as-admin opens the SPA at kahn.host. The nav says alex@devarno.cloud · admin. The agents view says “no agent runs” — but the dogfood tenant has 44 runs visible at the API layer. The screenshots show a working surface that’s displaying empty data correctly: the operator’s session resolves to their personal tenant, not the dogfood tenant. Root cause: the auth provider’s handoff JWT emits {sub, email, name, role, nonce} but not org_id. The SPA + backend correctly fall back to tenant = sub → operator’s personal tenant. RLS does its job. Nothing is broken. The standard fix is to ask the auth provider’s owner to add org_id to the JWT. That’s a cross-repo, operator-mediated change. It works; it’s just slow.

The shipped fix

Three coordinated PRs that don’t wait on the auth provider:
  1. Truthful empty-state copy — the SPA’s empty state names the actual root cause (“your session may be scoped to a different tenant; admin selector forthcoming”) instead of misleading text about CI runs.
  2. Backend admin tenant overrideX-KAHN-Tenant: <uuid> request header honored by the backend’s resolve_effective_ tenant() iff principal.role == "admin". UUID-validated, tenant-existence-validated, structured-log-audited. Read-only by construction (mutations use a separate dependency).
  3. Frontend admin selector — dropdown in the nav populated from GET /api/admin/tenants. Visible only to admin role. On change, dispatches hashchange to re-paint the current view; every subsequent fetch carries the new header.
End-to-end: operator picks kahn-internal (dogfood) → SPA re-fetches with X-KAHN-Tenant: a0e539b3-... → backend resolves the override → app.current_tenant set → RLS gates rows correctly → operator sees the populated agents view.

Why this works without the upstream change

Two layers of authorization compose cleanly:
  • JWT verification (unchanged) → principal.role.
  • Application-layer override (new) → principal.role == "admin" enables the header path; non-admin silently ignored.
  • RLS (unchanged) → gates rows on app.current_tenant, which is set from the override-resolved UUID.
Both the application-layer gate and RLS would have to be wrong simultaneously for cross-tenant escape. The integration test (tests/integration/test_agent_rls.py) proves the composition. When the upstream auth provider eventually adds org_id, this KAHN-side override becomes a power-user feature — operators can still impersonate any tenant, and the JWT becomes the default binding for non-admin sessions. Nothing has to be torn down.

What it cost

  • PR 1: ~3 hours including the planner pass. 4 files, 334 insertions.
  • PR 2: ~2 hours. 4 files, 505 insertions including 13 unit tests + 3 integration tests.
  • PR 3: ~2 hours. 5 files, 382 insertions including 3 vitest cases + 2 Playwright cases.
  • Cross-repo deps: zero. The auth provider’s roadmap remains the auth provider’s roadmap.

Where this generalises

Any RLS-scoped multi-tenant SaaS where:
  • The JWT is verified upstream but doesn’t carry tenant claims yet.
  • The principal’s role is available via the JWT.
  • Admin operators need read-side cross-tenant visibility for support / debugging / dogfood inspection.
The pattern is documented as a doctrine at atlas/doctrines/admin-tenant-override-without-jwt-update.doctrine.md.

Cross-references

  • KAHN PRs: #33 (immediate), #34 (backend), #35 (frontend).
  • Implementation files: backend/kahn/cloud_auth.py::resolve_effective_tenant, frontend/src/auth.ts::setActiveTenant, frontend/src/components/nav.ts (selector).
  • Companion learning: atlas/learnings/2026-04-27-kahn-northstar- delivery-shape.md — the broader sequential-PR pattern.