[object Object]

← back to Rentv

test(pr-crm): tenant-isolation RLS invariant guard (Class-A, TK-10290)

2fc31e97b9b8b2795d56ba6f193c7c657242cb26 · 2026-08-10 07:59:34 -0700 · Steve Abrams

Fails if any future table with a tenant_id column ships without RLS+FORCE+policy
(the classic cross-tenant-leak footgun), except the two documented pre-tenant auth
tables. Completes the hardening recommended in the approved setPassword memo. Passes
on the current schema; suite 99->100 green. Test-only — no runtime change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 2fc31e97b9b8b2795d56ba6f193c7c657242cb26
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 10 07:59:34 2026 -0700

    test(pr-crm): tenant-isolation RLS invariant guard (Class-A, TK-10290)
    
    Fails if any future table with a tenant_id column ships without RLS+FORCE+policy
    (the classic cross-tenant-leak footgun), except the two documented pre-tenant auth
    tables. Completes the hardening recommended in the approved setPassword memo. Passes
    on the current schema; suite 99->100 green. Test-only — no runtime change.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 test/pr/tenant-rls-invariant.test.js | 42 ++++++++++++++++++++++++++++++++++++
 1 file changed, 42 insertions(+)

diff --git a/test/pr/tenant-rls-invariant.test.js b/test/pr/tenant-rls-invariant.test.js
new file mode 100644
index 00000000..28e67d98
--- /dev/null
+++ b/test/pr/tenant-rls-invariant.test.js
@@ -0,0 +1,42 @@
+'use strict';
+// Regression guard (TK-10290, Class A): EVERY table with a tenant_id column must have RLS
+// enabled + FORCEd + a policy — except the two documented pre-tenant auth tables (pr_users,
+// pr_sessions), which are RLS-exempt by design and manually tenant-scope their queries.
+// A new tenant table that ships without RLS would silently leak across tenants; this test
+// fails loudly if that ever happens. Runs against a THROWAWAY DB (rentv_pr_test).
+process.env.PR_DB_NAME = 'rentv_pr_test';
+delete process.env.PR_DATABASE_URL;
+
+const test = require('node:test');
+const assert = require('node:assert');
+const { execSync } = require('child_process');
+
+execSync(
+  `psql -d postgres -tAc "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='rentv_pr_test' AND pid <> pg_backend_pid()" 2>/dev/null; `
+  + 'dropdb --if-exists rentv_pr_test 2>/dev/null; createdb rentv_pr_test',
+  { shell: '/bin/bash' },
+);
+
+const db = require('../../src/pr/db');
+
+// Documented RLS-exempt auth tables (login must resolve a user across tenants pre-context).
+const RLS_EXEMPT = ['pr_users', 'pr_sessions'];
+
+test('every tenant_id table has RLS+FORCE+policy (except documented auth tables)', async () => {
+  await db.runMigrations({});
+  const unguarded = await db.rows(`
+    SELECT c.relname
+    FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace AND n.nspname='public'
+    WHERE c.relkind='r'
+      AND EXISTS (SELECT 1 FROM pg_attribute a WHERE a.attrelid=c.oid AND a.attname='tenant_id' AND NOT a.attisdropped)
+      AND c.relname <> ALL($1::text[])
+      AND NOT (c.relrowsecurity AND c.relforcerowsecurity
+               AND EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid=c.oid))
+    ORDER BY c.relname`, [RLS_EXEMPT]);
+  assert.deepEqual(
+    unguarded.map((r) => r.relname), [],
+    `tenant_id table(s) missing RLS+FORCE+policy: ${unguarded.map((r) => r.relname).join(', ')} — `
+    + `add ENABLE+FORCE ROW LEVEL SECURITY + a tenant policy (see migration 007/008), or if it is a `
+    + `pre-tenant auth table, add it to RLS_EXEMPT here AND ensure its queries manually scope tenant_id.`,
+  );
+});

← 87c2b9fa auto-data-snapshot: 2026-08-10T07:52:37 (7 data files) — dat  ·  back to Rentv  ·  auto-data-snapshot: 2026-08-10T08:23:29 (7 data files) — dat 93f14ecf →