Skip to content

feat(security): per-tenant isolation for spend, secrets, strict mode#337

Merged
Destynova2 merged 1 commit into
mainfrom
feat/multi-tenant-isolation
May 15, 2026
Merged

feat(security): per-tenant isolation for spend, secrets, strict mode#337
Destynova2 merged 1 commit into
mainfrom
feat/multi-tenant-isolation

Conversation

@Destynova2
Copy link
Copy Markdown
Contributor

Summary

Closes 3 multi-tenant isolation gaps flagged by PR #326's regression suite. The 3 `#[ignore]`'d tests now pass.

  • Per-tenant budget: `SpendTracker` grows `check_tenant_budget(tenant, …)` backed by per-tenant in-memory caches and per-tenant JSONL journals at `spend//.jsonl`. `record_tenant` no longer also accumulates into the global counter, so a tenant overspend cannot trip the global budget for other tenants.
  • Per-tenant secrets: `SecretBackend::get` takes `(tenant, name)`. All three backends (LocalEncrypted, Env, File) resolve `/` first and fall back to the legacy flat layout for global secrets so single-tenant deployments need no migration.
  • `strict_tenant` flag: new `[security] strict_tenant` config field plus `tenant_required_middleware` that returns HTTP 400 with a `missing_tenant` body when the flag is on and no tenant id can be resolved (X-Tenant-ID header, JWT claim, or virtual key).
  • Tenant id source priority: VirtualKeyContext > JWT claim > X-Tenant-ID header. Header is only consulted when no authenticated tenant exists, so a client cannot impersonate a JWT tenant.

API breaking changes (all internal except `SecretBackend` trait)

  • `SecretBackend::get`: now takes `&str tenant` before `&str name`.
  • `SpendTracker` grows `check_tenant_budget` plus `DEFAULT_TENANT` bucket.
  • `SpendTracker::record_tenant` stops also calling `self.record(...)`.

Test plan

  • `cargo test --lib --tests` — 266 passed, 1 ignored (anomaly detector waiting on feat(security): tool-call spike anomaly detector (T-AD1) #308)
  • `cargo test --test lib multi_tenant` — 8 multi-tenant integration tests pass; previously 3 were `#[ignore]`d as known gaps
  • `cargo clippy --lib --tests -- -D warnings` — clean
  • Single-tenant deployments unaffected (legacy flat layout fallback verified)

Origin

This commit was authored by background agent `a6ab9ef6391d6f6f5` on 2026-04-28 but the agent hit org usage cap before opening the PR. Commit content unchanged from `2fe3c1c`; cherry-picked onto current main (which already includes #326's regression suite).

🤖 Generated with Claude Code

Closes 3 multi-tenant isolation gaps flagged by PR #326's regression
suite. The 3 #[ignore]'d tests now pass.

* Per-tenant budget: SpendTracker grows check_tenant_budget(tenant,...)
  backed by per-tenant in-memory caches and per-tenant JSONL journals
  at spend/<tenant>/<YYYY-MM>.jsonl. record_tenant no longer also
  accumulates into the global counter, so a tenant overspend cannot
  trip the global budget for other tenants.
* Per-tenant secrets: SecretBackend::get takes (tenant, name). All
  three backends (LocalEncrypted, Env, File) resolve <tenant>/<name>
  first and fall back to the legacy flat layout for global secrets so
  single-tenant deployments need no migration.
* strict_tenant flag: new [security] strict_tenant config field plus
  tenant_required_middleware that returns HTTP 400 with a
  missing_tenant body when the flag is on and no tenant id can be
  resolved (X-Tenant-ID header, JWT claim, or virtual key).
* Tenant id source priority: VirtualKeyContext > JWT claim >
  X-Tenant-ID header. Header is only consulted when no authenticated
  tenant exists, so a client cannot impersonate a JWT tenant.

API breaking changes (all internal except SecretBackend trait):
- SecretBackend::get: now takes &str tenant before &str name.
- SpendTracker grows check_tenant_budget plus DEFAULT_TENANT bucket.
- SpendTracker::record_tenant stops also calling self.record(...).

Tests: 1345 nextest, fmt+clippy clean. The 3 previously #[ignore]'d
multi_tenant_isolation tests now pass alongside the 4 already-passing
ones (7 active, 1 still ignored for ToolSpikeDetector from PR #308).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Destynova2 Destynova2 enabled auto-merge May 15, 2026 09:32
@Destynova2 Destynova2 merged commit f02b472 into main May 15, 2026
43 checks passed
@Destynova2 Destynova2 deleted the feat/multi-tenant-isolation branch May 15, 2026 09:45
@github-actions
Copy link
Copy Markdown
Contributor

Mutation testing (PR diff sample)

Informational — never blocks merge. Full matrix runs on main.

Metric Value
Status missed
Duration 757 s
Total 14
Caught 0
Missed 0
Timeout 0
Unviable 0

Legend: clean (no survivors), missed (inspect artifact), timed-out (25 min cap reached).

Artifact: mutants-pr-results-52b2664853a86d444469cf3f504709a97c88c701.

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