Skip to content

security(F013): cross-tenant isolation tests + nightly RBAC regression gate#197

Merged
beenuar merged 2 commits into
mainfrom
issue-159-cross-tenant-isolation
May 20, 2026
Merged

security(F013): cross-tenant isolation tests + nightly RBAC regression gate#197
beenuar merged 2 commits into
mainfrom
issue-159-cross-tenant-isolation

Conversation

@beenuar

@beenuar beenuar commented May 20, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #159.

Three endpoint-level tenant-isolation test suites plus a dedicated nightly workflow so RBAC regressions surface as the first nightly signal — not buried under a downstream compose-smoke failure.

What's covered

  • services/api/tests/test_threat_intel_tenant_isolation.py — IOC, threat-actor, and feed list/get/create/delete are scoped by tenant_id; cross-tenant lookups resolve to 404; writes attach current_user.tenant_id even when the payload smuggles a different one.
  • services/api/tests/test_alerts_tenant_isolation.py — every read/write/queue/claim path on /alerts binds tenant_id into the compiled SQL or forwards it to the service layer (build_queue / claim_alert).
  • services/api/tests/test_llm_credentials_tenant_isolation.py — BYOK credential GET/PUT/DELETE scope by tenant_id; new rows bind the caller's tenant; emit_audit is invoked with caller's tenant + actor (CredentialVault stubbed so the assertions are on the persistence boundary, not crypto).
  • .github/workflows/cross-tenant-rbac.yml — runs all three suites nightly at 06:30 UTC (ahead of compose-smoke-nightly at 09:00 UTC) and on workflow_dispatch. Uploads JUnit XML on failure and opens a security-labelled tracking issue with forensics.

Design choices

  • Assertions read the compiled SQL bind parameters rather than the shape of any one query, so the suites don't break on benign rewrites (column reordering, CTE refactors, select()aliased()).
  • No live DB, no FastAPI request cycle, no asgi transport — endpoint functions are called directly with explicit Query(...) defaults so the test cost is millisecond-scale (~0.56s for all 45 tests locally).
  • Mocked sessions are deliberate: the contract being tested is what tenant_id value the endpoint puts into the query, not whether the DB returns the right rows for that value. RLS / pg policies are a separate (and complementary) test surface.
  • The nightly workflow is intentionally separate from ci.yml so cron flakes on main (which happen during quiet hours) don't show up as a red mainline check on every PR.

Mutation-tested

Each suite was verified by temporarily dropping the tenant_id predicate in the corresponding endpoint:

Endpoint Mutation Result
get_alert removed Alert.tenant_id == current_user.tenant_id 1 test failed — no bound tenant_id parameter
get_llm_credential removed tenant filter from `select(TenantLlmCredential)` 3 tests failed — same assertion
list_iocs / get_ioc / etc. (covered indirectly via the threat-intel mutations) suite reds

Every dropped predicate produced ≥1 failing test, so the suites are wired to the right surface.

Test plan

  • pytest services/api/tests/test_threat_intel_tenant_isolation.py — green (16 tests)
  • pytest services/api/tests/test_alerts_tenant_isolation.py — green (17 tests)
  • pytest services/api/tests/test_llm_credentials_tenant_isolation.py — green (12 tests)
  • ruff 0.4.10 check + ruff 0.4.10 format --check — clean on all three files
  • Mutation tests on the alerts and llm_credentials endpoints — predicates dropped → tests red, restored → tests green
  • After merge: confirm the first scheduled run (next 06:30 UTC) succeeds and uploads its artifact

Files

  • .github/workflows/cross-tenant-rbac.yml (+188 / -0)
  • CHANGELOG.md (+36 / -0)
  • services/api/tests/test_alerts_tenant_isolation.py (+609 / -0)
  • services/api/tests/test_llm_credentials_tenant_isolation.py (+351 / -0)
  • services/api/tests/test_threat_intel_tenant_isolation.py (+466 / -0)

Made with Cursor

beenuar and others added 2 commits May 19, 2026 19:20
…n gate

Closes #159.

Adds three endpoint-level isolation suites that exercise the tenant
boundary without a live DB or FastAPI request cycle, plus a dedicated
nightly workflow so a regression shows up as the first nightly signal
rather than buried in compose-smoke.

* services/api/tests/test_threat_intel_tenant_isolation.py
  IOC, actor, and feed list/get/create/delete are scoped by tenant_id,
  cross-tenant lookups resolve to 404, and writes attach
  current_user.tenant_id even when the payload smuggles a different
  one. Assertions read the compiled SQL bind parameters so they don't
  break on benign query rewrites.

* services/api/tests/test_alerts_tenant_isolation.py
  Every read/write/queue/claim path on /alerts binds tenant_id into
  the compiled SQL or forwards it to the service layer (build_queue,
  claim_alert). Includes regression coverage for the previously-noisy
  Query(default=...) sentinel handling when endpoint functions are
  called directly.

* services/api/tests/test_llm_credentials_tenant_isolation.py
  BYOK credential GET/PUT/DELETE scope by tenant_id, new rows bind
  the caller's tenant, and emit_audit is invoked with the caller's
  tenant + actor. CredentialVault is stubbed so the assertions are
  on the persistence boundary, not on crypto.

* .github/workflows/cross-tenant-rbac.yml
  Runs the three suites nightly on main at 06:30 UTC (ahead of
  compose-smoke-nightly so a tenant boundary failure surfaces as the
  first nightly signal) and on-demand via workflow_dispatch. On
  failure: uploads a JUnit report and opens a security-labelled
  tracking issue with triage steps pointing at the exact endpoint
  modules to inspect.

All three suites were mutation-tested by temporarily dropping the
tenant_id predicate in the corresponding endpoint -- every dropped
predicate produced at least one failing test, confirming the suites
are wired to the right surface. 45/45 tests pass locally in ~0.6s.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t-isolation

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	CHANGELOG.md
@beenuar beenuar merged commit a99ff41 into main May 20, 2026
24 of 25 checks passed
@beenuar beenuar deleted the issue-159-cross-tenant-isolation branch May 20, 2026 05:05
@beenuar beenuar mentioned this pull request May 30, 2026
4 tasks
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.

[F013] security: cross-tenant isolation tests + nightly CI for RBAC regression

1 participant