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).