From 3cae58e88d329f5dd1e42074f7cd11cb5e2559de Mon Sep 17 00:00:00 2001 From: QSchlegel Date: Sun, 23 Aug 2026 16:28:19 +0200 Subject: [PATCH] fix(security): RLS for DocumentDraft and DocumentAttestation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other table in this schema gets Row Level Security in the migration that creates it — 20251215090000_enable_rls_disable_postgrest, its follow-up 20260706100000_enable_rls_followup_tables, and the per-table blocks in 20260805090000_add_document_signoff and 20260813000000_add_proxy_member. I added these two tables without it. Verified against a throwaway Postgres with the `anon` and `authenticated` roles present so both branches of the migration actually run: with this migration every table reports relrowsecurity = true, and four deny-all policies exist for the two without it DocumentDraft and DocumentAttestation are the ONLY two tables in the schema with RLS off This deployment is Supabase-backed, where RLS off plus the PostgREST roles is what stands between a table and the anon key. It matters more for these two than for most: DocumentDraft is the one table in the document stack that holds document BODIES rather than hashes, and DocumentAttestation holds the signed notary chain. Written as a follow-up rather than by editing those two migrations. Both are merged but applied nowhere, so editing them would work today — and would fail with a checksum error against any environment that had already applied them, and this repo ships migrations through an action that does not self-retry, so one failed deploy blocks every later migration too. The follow-up is correct under either state. No schema change: RLS is not modelled by Prisma, so prisma/schema.prisma is untouched and there is no drift. Co-Authored-By: Claude Opus 5 --- .../migration.sql | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 prisma/migrations/20260823090000_enable_rls_document_draft_attestation/migration.sql diff --git a/prisma/migrations/20260823090000_enable_rls_document_draft_attestation/migration.sql b/prisma/migrations/20260823090000_enable_rls_document_draft_attestation/migration.sql new file mode 100644 index 00000000..d52cfef7 --- /dev/null +++ b/prisma/migrations/20260823090000_enable_rls_document_draft_attestation/migration.sql @@ -0,0 +1,49 @@ +-- Row Level Security for DocumentDraft and DocumentAttestation. +-- +-- Every other table in this schema gets RLS in the migration that creates it — +-- see 20251215090000_enable_rls_disable_postgrest, its follow-up +-- 20260706100000_enable_rls_followup_tables, and the per-table blocks in +-- 20260805090000_add_document_signoff and 20260813000000_add_proxy_member. +-- These two tables were added without it, so they are the only ones in the +-- schema that PostgREST's anon and authenticated roles are not denied on. +-- +-- That matters more for these two than for most: DocumentDraft is the one table +-- in the document stack that holds document BODIES rather than hashes, and +-- DocumentAttestation holds the signed notary chain. +-- +-- Written as a follow-up rather than by editing those migrations, because a +-- migration that any environment has already applied cannot be edited without a +-- checksum failure on the next deploy — and this repo ships migrations through +-- an action that does not self-retry, so a failed deploy blocks every later +-- migration too. +-- +-- Same contract as the migrations above: RLS on unconditionally, deny-all +-- policies for the PostgREST roles only when those roles exist, and Prisma +-- continues to connect as the table owner / service role and bypass RLS. +DO $$ +DECLARE + tbl TEXT; +BEGIN + FOR tbl IN + SELECT unnest(ARRAY['DocumentDraft', 'DocumentAttestation']) + LOOP + -- Skip tables that don't exist + IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = tbl) THEN + EXECUTE format('ALTER TABLE %I ENABLE ROW LEVEL SECURITY', tbl); + + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'anon') THEN + EXECUTE format( + 'CREATE POLICY "deny_all_anon_%s" ON %I FOR ALL TO anon USING (false) WITH CHECK (false)', + tbl, tbl + ); + END IF; + + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'authenticated') THEN + EXECUTE format( + 'CREATE POLICY "deny_all_authenticated_%s" ON %I FOR ALL TO authenticated USING (false) WITH CHECK (false)', + tbl, tbl + ); + END IF; + END IF; + END LOOP; +END $$;