Skip to content

Phase 3 SP1 — Multi-tenancy (OIDC + RBAC + tenant scoping + quota)#15

Merged
l17728 merged 20 commits into
mainfrom
feat/phase-3-sp1-multi-tenancy
May 18, 2026
Merged

Phase 3 SP1 — Multi-tenancy (OIDC + RBAC + tenant scoping + quota)#15
l17728 merged 20 commits into
mainfrom
feat/phase-3-sp1-multi-tenancy

Conversation

@l17728
Copy link
Copy Markdown
Owner

@l17728 l17728 commented May 18, 2026

Summary

  • OIDC login/callback + system-JWT Principal; casbin RBAC (require_perm, tenant-scoped); mandatory tenant_filtered scoping on all task routes (Invariant 8); per-tenant quota (strong-consistent insert-then-lock create check + event-sourced usage_records + leader-gated minute quota_snapshots).
  • Removes the single shared bearer_token; adds a system_admin service token + auth_dev_mode for the IdP-free path; fail-closed startup guard.
  • One alembic migration (usage_records / quota_snapshots / casbin_rule), idempotent default-tenant seed. Project-scoped RBAC deferred (SP1 = tenant-scoped only). Closes G1/G2 (user-plane). Phase 3 sub-project 1 of 4.
  • Process: 2-reviewer pre-execution plan review (caught 4–5 BLOCKERs pre-impl) → implementer-only per-task → controller milestone E2E per milestone → final whole-impl security review (caught 1 CRITICAL: missing app.state.casbin lifespan bootstrap — fixed + regression test added).

Test Plan

  • Full suite uv run pytest -q → 312 passed, 1 deselected (manual)
  • E2E-MT-* tests/e2e/test_tenant_isolation.py (cross-tenant + quota isolation + service token)
  • tests/test_lifespan_state.py drives the REAL lifespan (casbin/settings bootstrap regression)
  • python tools/lint_invariants.py + tools/test_lint_invariants.py + tools/lint_no_direct_status_write.py green
  • spectral lint api/openapi.yaml --fail-severity=error (0 errors) + swagger-cli validate green
  • uv.lock updated for authlib/casbin
  • CI green on all 12 jobs

Spec: docs/superpowers/specs/2026-05-18-phase-3-sp1-multi-tenancy-design.md
Plan: docs/superpowers/plans/2026-05-18-phase-3-sp1-multi-tenancy.md

🤖 Generated with Claude Code

l17728 and others added 20 commits May 18, 2026 22:16
…ota)

Brainstormed design spec for Phase 3 sub-project 1. Decomposes Phase 3
into 4 sub-projects; SP1 is the foundation: OIDC user auth + system-JWT,
casbin RBAC, mandatory tenant_id scoping (Invariant 8), per-tenant quota
(strong-consistent create check + event-sourced usage + minute snapshots).
Closes G1/G2 (user-plane). Companion plan to be written next.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14 bite-sized TDD tasks across 4 milestones (M1 identity core, M2 RBAC
+ tenant scoping, M3 quota, M4 isolation e2e + docs + PR). Complete code,
no placeholders; self-reviewed for spec coverage / type consistency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-execution review (2 opus reviewers) found 4-5 BLOCKER + 5 IMPORTANT
plan-level defects, all verified against code and fixed before impl:
- "completed" -> "succeeded" (real terminal status)
- model registration in db/models/__init__.py (not base.py)
- no information_schema CI scan exists (real gate: tools/lint_invariants.py)
- drop broken project_match -> SP1 tenant-scoped only
- NOT-NULL quota/is_active seed values; snapshot-less-tenant race fix
- quota.exceeded audit; casbin keyMatch + anchored regex
- accurate CI command list (no ruff/mypy/code-vs-yaml CI jobs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Folds Task 10's make_app_with_state casbin seeding here (Task 9 tests need
app.state.casbin). quota.py is a Task-9 stub; Task 11 replaces the body.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace DLW_BEARER_TOKEN / _TOKEN bearer auth with DLW_SYSTEM_JWT_SECRET +
principal_headers(role="tenant_admin") across all task-route test files.
Switch bare create_app() clients to make_app_with_state(ephemeral_ca).
Add casbin_rule, quota_snapshots, usage_records to test_alembic expected tables.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rocess

Module-top models import (create_all needs full Base.metadata in isolation);
drop_all->create_all clean slate (session-DB Tenant id=1 collision); subprocess
sets DLW_AUTH_DEV_MODE so the new SP1 fail-closed startup guard doesn't block
this executor-only e2e.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Function-scoped bootstrap (clean DB per test) — the plan's module-scoped
fixture leaked the prior test's tenant-A task into the quota test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…CAL)

Final whole-implementation review caught that app.state.casbin was set only
in the test helper make_app_with_state, never in production create_app/
lifespan — every user-plane task/quota request would 500 with AttributeError
in a real deployment (the whole suite masked it because ASGI tests bypass
lifespan). Bootstrap settings + casbin (grants from casbin_rule, empty in
SP1) unconditionally in lifespan, like the W3a auth substrate. Adds
tests/test_lifespan_state.py: a regression test driving the REAL lifespan
(would have caught this). Also commits the principal_headers/service_headers
conftest helpers required by the migrated test suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@l17728 l17728 merged commit fa08e6d into main May 18, 2026
12 checks passed
@l17728 l17728 deleted the feat/phase-3-sp1-multi-tenancy branch May 18, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant