Three layers: a short note at the top, the key lines with our take in the middle, the full source at the bottom.
Test
0004_rls_cross_tenant.sql
Cross-tenant isolation tests run against a live database. A failure here would block every release.
Repo path infra/postgres/tests/0004_rls_cross_tenant.sqlLanguage SQL
What this is
A test that runs against a live database. It creates two workspaces, signs in as the first, and tries to read rows that belong to the second. The test must fail to read those rows — if it succeeds, the build is stopped.
What it proves
Backs the promise that one tenant cannot read another tenant's records. This is the test that catches a regression — even one — before it ships. Read the promise →
What to look for in the source below
- Two distinct workspace ids in the setup.
- A read query that attempts to cross from one workspace into the other.
- An assertion that the read returns zero rows.
Show the full file (147 lines)
146 lines
-- Cross-tenant RLS contract test for migration 0004 (PR-9 part 1).
--
-- Pre-private-beta audit F4: the only place tenant isolation truly
-- holds is the database. The application layer asserts intent; the
-- database enforces it. This test runs against a live Postgres that
-- has applied migrations 0001-0005 + the v4 follow-up (PR-9 part 1).
-- It assumes the test runner has created two non-owner roles
-- `tenant_role_a` and `tenant_role_b` and that the test driver
-- connects as one role per session.
--
-- How to run locally (assumes Docker postgres):
-- docker run --name muntin-rls-test -e POSTGRES_PASSWORD=test \
-- -p 5433:5432 -d postgres:16
-- PGUSER=postgres PGPASSWORD=test psql -h localhost -p 5433 \
-- -f infra/postgres/migrations/0001_extractions.sql \
-- -f infra/postgres/migrations/0002_insights.sql \
-- -f infra/postgres/migrations/0003_verdicts.sql \
-- -f infra/postgres/migrations/0004_templates.sql \
-- -f infra/postgres/migrations/0005_provenance_and_method.sql \
-- -f infra/postgres/tests/0004_rls_cross_tenant.sql
--
-- Expected: every assertion passes (RAISE NOTICE on failure halts
-- the script). Any unhandled error is a tenant-isolation regression.
BEGIN;
-- Set up two non-owner roles. In production these are the Neon
-- connection roles apps/api uses; the migration role retains owner.
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'tenant_role') THEN
CREATE ROLE tenant_role NOLOGIN;
END IF;
END $$;
GRANT SELECT, INSERT, UPDATE ON extraction_templates TO tenant_role;
GRANT SELECT, INSERT, UPDATE ON template_observations TO tenant_role;
-- Seed: org_a owns one template; org_b owns one template.
INSERT INTO extraction_templates
(id, org_id, vendor_id, layout_hash, version, state,
rules_json, structure_signature_json, observed_count)
VALUES
('tpl_org_a', 'org_a', 'ven_x', 'hash_a', 1, 'candidate',
'[]'::jsonb, '{}'::jsonb, 0),
('tpl_org_b', 'org_b', 'ven_x', 'hash_b', 1, 'candidate',
'[]'::jsonb, '{}'::jsonb, 0);
-- ---------------------------------------------------------------
-- Assertion 1: connecting as a non-owner with no GUC set returns 0
-- rows (fail-closed posture). The audit fix removed the `, true` arg
-- so current_setting raises if missing -- but the policy resolves
-- the missing GUC to a comparison failure (not an error), so SELECT
-- returns no rows. Either behaviour is acceptable; the contract is
-- "no leak", not the specific error mode.
-- ---------------------------------------------------------------
SET ROLE tenant_role;
DO $$
DECLARE
visible_count int;
BEGIN
BEGIN
SELECT count(*) INTO visible_count FROM extraction_templates;
EXCEPTION WHEN OTHERS THEN
visible_count := 0; -- the raise is acceptable
END;
IF visible_count != 0 THEN
RAISE EXCEPTION 'F4 BLOCKER: tenant_role with no GUC saw % rows', visible_count;
END IF;
END $$;
RESET ROLE;
-- ---------------------------------------------------------------
-- Assertion 2: with app.org_id='org_a', tenant_role sees ONLY
-- org_a's template (1 row), not org_b's.
-- ---------------------------------------------------------------
SET ROLE tenant_role;
SET LOCAL app.org_id = 'org_a';
DO $$
DECLARE
visible_count int;
has_org_b boolean;
BEGIN
SELECT count(*) INTO visible_count FROM extraction_templates;
IF visible_count != 1 THEN
RAISE EXCEPTION 'F4 BLOCKER: org_a session saw % templates, expected 1',
visible_count;
END IF;
SELECT EXISTS (
SELECT 1 FROM extraction_templates WHERE org_id = 'org_b'
) INTO has_org_b;
IF has_org_b THEN
RAISE EXCEPTION 'F4 BLOCKER: org_a session saw an org_b row';
END IF;
END $$;
RESET ROLE;
-- ---------------------------------------------------------------
-- Assertion 3: with app.org_id='org_a', INSERT WITH org_id='org_b'
-- is REJECTED by the WITH CHECK clause. This is the F2 contract.
-- ---------------------------------------------------------------
SET ROLE tenant_role;
SET LOCAL app.org_id = 'org_a';
DO $$
BEGIN
BEGIN
INSERT INTO extraction_templates
(id, org_id, vendor_id, layout_hash, version, state,
rules_json, structure_signature_json, observed_count)
VALUES
('tpl_attempted_spoof', 'org_b', 'ven_x', 'hash_spoof', 1,
'candidate', '[]'::jsonb, '{}'::jsonb, 0);
RAISE EXCEPTION 'F2 BLOCKER: org_a session inserted an org_b row';
EXCEPTION
WHEN insufficient_privilege OR check_violation OR others THEN
-- expected: RLS WITH CHECK rejects the row
NULL;
END;
END $$;
RESET ROLE;
-- ---------------------------------------------------------------
-- Assertion 4: same WITH CHECK contract on template_observations.
-- ---------------------------------------------------------------
SET ROLE tenant_role;
SET LOCAL app.org_id = 'org_a';
DO $$
BEGIN
BEGIN
INSERT INTO template_observations
(id, extraction_id, org_id, vendor_id, layout_hash,
field_path, fingerprint, observed_value_hash, user_action,
user_id)
VALUES
('obs_spoof', 'ext_x', 'org_b', 'ven_x', 'hash_b',
'total', 'fp_x', encode(digest('100', 'sha256'), 'hex'),
'confirmed', 'usr_x');
RAISE EXCEPTION 'F2 BLOCKER: template_observations write spoofed';
EXCEPTION
WHEN insufficient_privilege OR check_violation OR others THEN
NULL;
END;
END $$;
RESET ROLE;
ROLLBACK;See also
This is the file as it lives at the moment of this build. The canonical history lives in git. If you want the full history or a specific commit, write to hello@muntin.digital.