Module: DevDoc::Test::Lints::CrossTenantCanarySweep
- Defined in:
- lib/dev_doc/test/lints/cross_tenant_canary_sweep.rb
Overview
L1 of the cross-tenant canary sweep (see CrossTenantCanaryCheck for the doctrine and the honest coverage/miss statement). Passive: it asserts that no response a persona receives contains another tenant's canary marker. It adds no requests of its own — it rides whatever crawl/replay the host test already drives.
Include into the project's base integration test:
Glib::IntegrationTest.include DevDoc::Test::Lints::CrossTenantCanarySweep
The choke point
Every request helper (get/post/...) funnels through the verb methods
this module overrides (the module sits ahead of the framework Runner in
the MRO). A JSON crawler that drives requests via send(:get, ...) on
the test instance is caught the same way as a direct call — no crawler
changes needed. After super returns, response.body holds the body to
scan.
Where this has signal, and where it deliberately doesn't
The scan only sees what the response actually rendered:
- Full-render harnesses (a crawler that renders pages to follow their links; a permission replay run in FULL mode) → real signal: a foreign marker in a persona's own reachable page is a data-bleed leak.
- A permission replay in its default SKIP mode short-circuits the controller action to a status-only body, so there is no rendered content to scan. That harness catches cross-tenant access at the STATUS level (its response-matrix snapshot); this lint contributes no content signal there, and that is fine — the two are complementary, not redundant. Passing this lint in skip mode is not evidence of isolation; the matrix is. This layer is passive, so it never reaches a leak a persona is not already linked to — that is the adversarial replay's job (L2/L3), which the host app owns because param-mutation and mail-capture are too app-shaped to generalize. Keep this core pure.
Consumer contract
The including test must provide two things, refreshed per persona:
canary_registry→{ tenant_key => [markers...] }, scanned from fixtures once (static strings only — never runtime-minted, per the determinism doctrine).canary_allowed_tenant_keys→ the tenant keys the CURRENT persona may legitimately see. A single-tenant member returns its one key; a global admin returns every key (making the scan vacuous for it — correct, it sees everything); a parent in a tenant hierarchy returns itself plus its descendants. Returning nil disables the scan for that request (e.g. before any persona has logged in).