feat(store): video_connections table + RLS + connection store methods
Add the video_connections table (data-model VIDEO_CONNECTION) with FORCE row-level security keyed off tapir.current_user_id, identical to migration 003's per-user isolation pattern — connections are user-owned data and must be isolated at the DB layer, not only by application WHERE clauses. Store methods (UpsertConnection / ConnectionsForUser / DeleteConnection) all route through withUser so RLS scopes every access. UpsertConnection is idempotent on (user_id, provider). The OAuth refresh token never lives here; token_ref is the opaque SecretStore reference. Extend the RLS isolation proof to cover video_connections: seeded per user, included in the deny-all + scoped-read assertions, and added to the cross-user write-invisibility and survivor checks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
-- Migration 005: video_connections — a user's connected video account
|
||||
-- (data-model.md VIDEO_CONNECTION). The OAuth refresh token never lives here;
|
||||
-- token_ref is the opaque SecretStore reference that resolves to it. Revocation
|
||||
-- flips status, it does not delete the row (history is kept).
|
||||
--
|
||||
-- One connection per (user, provider): re-connecting the same provider upserts
|
||||
-- in place (the connect flow's ON CONFLICT (user_id, provider) target).
|
||||
CREATE TABLE video_connections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider TEXT NOT NULL,
|
||||
provider_account TEXT,
|
||||
token_ref TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
connected_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
CONSTRAINT video_connections_user_provider_unique UNIQUE (user_id, provider)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_video_connections_user_id ON video_connections(user_id);
|
||||
|
||||
-- Per-user isolation, identical to migration 003's pattern: this is user-owned
|
||||
-- data, so user A must never read or write user B's connections even with a wrong
|
||||
-- application-level WHERE. ENABLE + FORCE so the table owner (tapir) is subject to
|
||||
-- the policy too; the policy keys off the per-request GUC tapir.current_user_id
|
||||
-- set by the store's withUser helper. An unset GUC yields NULL -> deny-all.
|
||||
ALTER TABLE video_connections ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE video_connections FORCE ROW LEVEL SECURITY;
|
||||
CREATE POLICY video_connections_isolation ON video_connections
|
||||
FOR ALL
|
||||
USING (user_id = current_setting('tapir.current_user_id', true)::uuid);
|
||||
Reference in New Issue
Block a user