feat(store): migration 003 — enforce per-user isolation via forced RLS

Enable AND FORCE row-level security on every user-owned table (users, videos,
transcripts, summaries, summary_actions, sink_deliveries) per ADR-012. Each
policy keys off the per-request GUC tapir.current_user_id; an unset GUC yields
NULL → deny-all (the safe default).

FORCE is load-bearing: the app connects as the table owner (tapir), and owners
bypass RLS unless forced. Without FORCE the policies are dead for the prod user.

sink_deliveries has no user_id; its policy derives ownership from the summary it
belongs to via EXISTS against the GUC, so it is self-contained rather than
silently depending on summaries' own RLS being applied to a subquery.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-03 15:15:44 +02:00
co-authored by Claude Opus 4.8
parent aa3f1631a6
commit 6775e5f53d
2 changed files with 91 additions and 0 deletions
@@ -0,0 +1,23 @@
DROP POLICY IF EXISTS sink_deliveries_isolation ON sink_deliveries;
ALTER TABLE sink_deliveries NO FORCE ROW LEVEL SECURITY;
ALTER TABLE sink_deliveries DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS summary_actions_isolation ON summary_actions;
ALTER TABLE summary_actions NO FORCE ROW LEVEL SECURITY;
ALTER TABLE summary_actions DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS summaries_isolation ON summaries;
ALTER TABLE summaries NO FORCE ROW LEVEL SECURITY;
ALTER TABLE summaries DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS transcripts_isolation ON transcripts;
ALTER TABLE transcripts NO FORCE ROW LEVEL SECURITY;
ALTER TABLE transcripts DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS videos_isolation ON videos;
ALTER TABLE videos NO FORCE ROW LEVEL SECURITY;
ALTER TABLE videos DISABLE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS users_isolation ON users;
ALTER TABLE users NO FORCE ROW LEVEL SECURITY;
ALTER TABLE users DISABLE ROW LEVEL SECURITY;