security(F013): cross-tenant isolation tests + nightly RBAC regression gate#197
Merged
Conversation
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 bytenant_id; cross-tenant lookups resolve to 404; writes attachcurrent_user.tenant_ideven when the payload smuggles a different one.services/api/tests/test_alerts_tenant_isolation.py— every read/write/queue/claim path on/alertsbindstenant_idinto 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 bytenant_id; new rows bind the caller's tenant;emit_auditis invoked with caller's tenant + actor (CredentialVaultstubbed 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 ofcompose-smoke-nightlyat 09:00 UTC) and onworkflow_dispatch. Uploads JUnit XML on failure and opens asecurity-labelled tracking issue with forensics.Design choices
select()→aliased()).Query(...)defaults so the test cost is millisecond-scale (~0.56s for all 45 tests locally).tenant_idvalue 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.ci.ymlso cron flakes onmain(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_idpredicate in the corresponding endpoint:get_alertAlert.tenant_id == current_user.tenant_idno bound tenant_id parameterget_llm_credentiallist_iocs/get_ioc/ etc.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 filesFiles
.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