feat(m7.1): wire tenant claims, status enforcement, and db scoping helper #82

Open
sharang wants to merge 2 commits from feat/m7.1-tenant-claims into main
Owner

Summary

Lays the platform-wide M7.1 multi-tenancy infrastructure on top of the existing Keycloak JWT signature validation.

  • JWT claims extraction. Claims struct now decodes tenant_id, tenant_slug, org_roles, products, plan, tenant_status, plus sub / name / preferred_username. require_jwt_auth builds a compliance_core::TenantContext from them and attaches it to request extensions. Missing tenant_id -> 401; missing tenant_status -> defaults to Trial with a warn-log.
  • TenantContext type in compliance-core. Single source of truth, re-exported alongside OrgRole and TenantStatus. The OrgRole enum is forward-compatible (#[serde(other)] Unknown variant) so a new realm role doesn't 401 the world.
  • TenantCtx Axum extractor. Handlers consume the context via TenantCtx(ctx): TenantCtx. Returns 401 if the upstream middleware didn't insert one.
  • require_tenant_status middleware. Enforces §5c of PLATFORM_ARCHITECTURE.md: Active/Trial/Demo pass, Frozen returns 402 on non-GET/HEAD/OPTIONS, Archived returns 410 on every method.
  • compliance_core::db::tenant_filter helper. Returns doc! { "tenant_id": ... }. tenant_filter_merge combines with extra conditions and refuses to let the caller override the tenant key.

Per-collection wiring (adding tenant_id to each model + threading the filter through every find / update_* / delete_* call across the 38 query call-sites) lands in a follow-up PR.

Test plan

  • cargo fmt --all -- --check clean
  • cargo clippy --workspace --exclude compliance-dashboard -- -D warnings clean (matches baseline)
  • cargo test -p compliance-core --lib — 7 tests pass (4 tenant, 3 db)
  • cargo test -p compliance-agent --lib api:: — 32 tests pass, including 6 new auth_middleware tests and 2 tenant_ctx tests
  • cargo test -p compliance-agent --test tenant_status_middleware — 6 integration tests pass (active / trial / demo / frozen / archived / no-context paths)
  • Ship Keycloak protocol mapper for tenant_status claim (separate ticket)
  • Per-collection scoping migration (M7.2)
## Summary Lays the platform-wide M7.1 multi-tenancy infrastructure on top of the existing Keycloak JWT signature validation. - **JWT claims extraction.** `Claims` struct now decodes `tenant_id`, `tenant_slug`, `org_roles`, `products`, `plan`, `tenant_status`, plus `sub` / `name` / `preferred_username`. `require_jwt_auth` builds a `compliance_core::TenantContext` from them and attaches it to request extensions. Missing `tenant_id` -> 401; missing `tenant_status` -> defaults to Trial with a warn-log. - **`TenantContext` type in compliance-core.** Single source of truth, re-exported alongside `OrgRole` and `TenantStatus`. The `OrgRole` enum is forward-compatible (`#[serde(other)] Unknown` variant) so a new realm role doesn't 401 the world. - **`TenantCtx` Axum extractor.** Handlers consume the context via `TenantCtx(ctx): TenantCtx`. Returns 401 if the upstream middleware didn't insert one. - **`require_tenant_status` middleware.** Enforces §5c of PLATFORM_ARCHITECTURE.md: Active/Trial/Demo pass, Frozen returns 402 on non-GET/HEAD/OPTIONS, Archived returns 410 on every method. - **`compliance_core::db::tenant_filter` helper.** Returns `doc! { "tenant_id": ... }`. `tenant_filter_merge` combines with extra conditions and refuses to let the caller override the tenant key. Per-collection wiring (adding `tenant_id` to each model + threading the filter through every `find` / `update_*` / `delete_*` call across the 38 query call-sites) lands in a follow-up PR. ## Test plan - [x] `cargo fmt --all -- --check` clean - [x] `cargo clippy --workspace --exclude compliance-dashboard -- -D warnings` clean (matches baseline) - [x] `cargo test -p compliance-core --lib` — 7 tests pass (4 tenant, 3 db) - [x] `cargo test -p compliance-agent --lib api::` — 32 tests pass, including 6 new auth_middleware tests and 2 tenant_ctx tests - [x] `cargo test -p compliance-agent --test tenant_status_middleware` — 6 integration tests pass (active / trial / demo / frozen / archived / no-context paths) - [ ] Ship Keycloak protocol mapper for `tenant_status` claim (separate ticket) - [ ] Per-collection scoping migration (M7.2)
sharang added 1 commit 2026-05-20 08:15:02 +00:00
feat(m7.1): wire tenant claims, status enforcement, and db scoping helper
CI / Check (pull_request) Successful in 10m50s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
05c01ea547
Lays the platform-wide multi-tenancy infrastructure on top of the
existing Keycloak signature validation. JWTs now carry tenant_id,
tenant_slug, org_roles, products, plan, and tenant_status; the
middleware decodes them into a TenantContext and attaches it to the
request extensions. A TenantCtx Axum extractor exposes the context to
handlers, and a tenant_status middleware enforces the §5c lifecycle
(frozen tenants are 402 on writes; archived tenants are 410 on every
method).

A db::tenant_filter helper in compliance-core gives every future
collection a single grep-able pattern for tenant-scoped queries.
Per-collection wiring (adding tenant_id to each model + threading the
filter through every find/update/delete call) lands in a follow-up.

Tests: 6 inline unit tests for claims→context mapping, 2 for the
extractor, 6 integration tests for status middleware, 3 for db
filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sharang added 1 commit 2026-05-20 15:20:40 +00:00
fix(m7.1): correct middleware layer order so JwksState is visible
CI / Check (pull_request) Successful in 11m31s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
cb7b1b86f5
Axum applies layers outermost-first. With the previous ordering
(`Extension(jwks_state)` first, `require_jwt_auth` last), the JWT
middleware ran before the Extension layer attached `JwksState` to
the request, so `request.extensions().get::<JwksState>()` always
returned None and the middleware silently passed through every
request as if Keycloak weren't configured.

Verified end-to-end against the local CERTifAI Keycloak realm:
- no token / bad token -> 401
- active / trial -> 200 read, write reaches handler
- frozen -> 200 read, 402 on writes
- archived -> 410 on every method

The bug was invisible to the unit + integration tests because they
construct the layer stack manually; only the live wiring exhibited it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Some checks are pending
CI / Check (pull_request) Successful in 11m31s
CI / Detect Changes (pull_request) Has been skipped
CI / Deploy Agent (pull_request) Has been skipped
CI / Deploy Dashboard (pull_request) Has been skipped
CI / Deploy Docs (pull_request) Has been skipped
CI / Deploy MCP (pull_request) Has been skipped
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/m7.1-tenant-claims:feat/m7.1-tenant-claims
git checkout feat/m7.1-tenant-claims
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sharang/compliance-scanner-agent#82