Skip to content

feat(console): per-tenant ledger read scoping (RBAC + tenant_id column)#4

Closed
New1Direction wants to merge 1 commit into
fix/critical-security-3from
fix/ledger-tenant-scoping
Closed

feat(console): per-tenant ledger read scoping (RBAC + tenant_id column)#4
New1Direction wants to merge 1 commit into
fix/critical-security-3from
fix/ledger-tenant-scoping

Conversation

@New1Direction
Copy link
Copy Markdown
Owner

Completes the authorization half of the console finding. Stacked on #3 (which closed the unauthenticated-access hole); base is fix/critical-security-3.

What this adds

  • auth_guard.tenant_scope — derives the ledger read filter from the authenticated principal:
    • admin role (and the explicit dev opt-out) → read across all tenants;
    • any other principal → restricted to its own tenant_id;
    • authenticated but no tenant_id claim403 (can't be scoped safely).
  • ledger_router/traces, /trace/{id}, /summary now filter by the caller's tenant. One tenant can no longer see another's traces (a cross-tenant id resolves to 404).
  • SchemaExecutionTrace gains tenant_id (default "default"); StateLedger persists it and migrates legacy DBs in place (ALTER TABLE ADD COLUMN). It is metadata only — excluded from the content-addressed state hash, so determinism is unaffected.

Verification

  • Full CI Python suite: 1263 passed, 0 failed
  • New tests (TDD, RED→GREEN): test_ledger_tenant_scope.py (read isolation: own-tenant only / cross-tenant 404 / admin sees all / no-tenant 403) + test_ledger_tenant.py (persist + legacy migration + default).
  • ruff check + format --check: clean.

Honest follow-up (not in this PR)

The orchestrator write-path is tenant-blind — it has no request context, so traces it writes persist as tenant_id = "default". Read-side isolation is fully enforced here; threading the real request tenant into those writes is a separate change (needs tenant context plumbed through the execution path).

🤖 Generated with Claude Code

Completes the authZ half of the console finding (PR #3 closed the authN hole).

- auth_guard.tenant_scope: derives the read filter from JWT claims — admins (and
  the dev opt-out) read across tenants; a non-admin is restricted to its own
  tenant_id; a token with no tenant_id claim is 403 (cannot be scoped).
- ledger_router: /traces, /trace/{id}, /summary now filter by the caller's tenant,
  so one tenant can never read another's traces (cross-tenant id -> 404).
- ExecutionTrace gains tenant_id (default 'default'); StateLedger persists it and
  migrates legacy DBs in place (ALTER ADD COLUMN). Excluded from the state hash.

FOLLOW-UP: thread the real request tenant into orchestrator-written traces (the
execution path is currently tenant-blind, so those rows persist as 'default').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@New1Direction
Copy link
Copy Markdown
Owner Author

Consolidated. All commits from this PR are now in rust-core-loadbearing (fast-forwarded through the full stack), together with the 17 previously-uncommitted source files the package needed to import. The integration branch now imports cleanly, the committed test suite passes, and ruff/mypy/parse gates are green on a fresh checkout. Closing as superseded — history is preserved on rust-core-loadbearing.

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