From 1340b4cd79ae85deed2d32697c9deccff3e2a48e Mon Sep 17 00:00:00 2001 From: Moheeb Zara Date: Fri, 15 May 2026 22:11:33 -0700 Subject: [PATCH] fix(incidents): re-assert hardened incidents_insert policy + clearer error Production: a basic member filing an incident (incl. anonymous) hit an RLS violation on table incidents. Not reproducible from source; the code and the 016/017 policy both permit it, so it is a deployed policy/data divergence. - scripts/025 idempotently re-asserts incidents_insert verbatim from 017/schema.sql so a drifted deployment converges to the known-good policy. Access-neutral, no schema change. - fileIncident maps an RLS rejection to an actionable message and logs the technical detail server-side instead of raw Postgres. Minimal hotfix off origin/main: only the two production-affecting files. If the report persists post-deploy the cause is not policy drift; the discriminating SQL is tracked in the local stack's HANDOFF. --- lib/actions/incidents.ts | 22 ++++++++- .../025_reassert_incidents_insert_policy.sql | 49 +++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 scripts/025_reassert_incidents_insert_policy.sql diff --git a/lib/actions/incidents.ts b/lib/actions/incidents.ts index 71b6da8..1dba980 100644 --- a/lib/actions/incidents.ts +++ b/lib/actions/incidents.ts @@ -70,7 +70,27 @@ export async function fileIncident(formData: { .select() .single() - if (error) return { error: error.message } + if (error) { + // A row-level-security rejection here means the caller's membership is not + // resolving against this space in the database (see scripts/025 header). + // The raw Postgres text is not actionable for a reporter, so surface a + // clear message and keep the technical detail in the server log. + const isRls = error.code === '42501' || /row-level security/i.test(error.message) + if (isRls) { + console.error('fileIncident RLS rejection', { + space_id: member.space_id, + member_id: member.id, + is_anonymous: isAnonymous, + code: error.code, + message: error.message, + }) + return { + error: + 'Could not file the report: your membership is not fully linked to this space. Contact a space admin.', + } + } + return { error: error.message } + } // Don't include the reporter's identity in the activity log if anonymous. await logActivity( diff --git a/scripts/025_reassert_incidents_insert_policy.sql b/scripts/025_reassert_incidents_insert_policy.sql new file mode 100644 index 0000000..0aacad7 --- /dev/null +++ b/scripts/025_reassert_incidents_insert_policy.sql @@ -0,0 +1,49 @@ +-- ============================================================================= +-- 025: Re-assert the hardened incidents_insert policy (production convergence) +-- ============================================================================= +-- A basic, active member filing an incident report on production (including +-- the anonymous path) hit: +-- +-- new row violates row-level security policy for table "incidents" +-- +-- Tracing the code against this repo, the insert SHOULD pass: fileIncident() +-- sets reporter_id = NULL when anonymous (satisfying the second WITH CHECK +-- branch) and space_id = the member's own space, and getAuthMember() resolves +-- the member strictly by user_id = auth.uid() (so get_user_space_ids(auth.uid()) +-- contains that space_id). The failure is therefore a production divergence, +-- not reproducible from source. The three candidate root causes are: +-- +-- 1. auth.uid() is NULL inside the PostgREST request (SSR session/JWT not +-- reaching the DB for this action). +-- 2. The deployed incidents_insert policy differs from migrations 016/017 +-- (hand-edited, or an older variant the idempotent runner never replaced). +-- 3. The acting space_members row's user_id does not match the JWT sub on +-- production (data drift). +-- +-- This migration addresses cause #2 only: it idempotently drops and recreates +-- incidents_insert with the EXACT hardened expression from migration 017 and +-- scripts/schema.sql. It is access-neutral (the policy text is identical to the +-- intended state, so a correctly-deployed DB is unchanged) and it makes the +-- deployed policy converge to the known-good definition. +-- +-- It does NOT fix causes #1 or #3. If the report persists after this migration +-- deploys, run the discriminating queries in docs/HANDOFF.md to confirm whether +-- auth.uid() resolves and whether the member row's user_id matches. +-- +-- Idempotent: DROP POLICY IF EXISTS then CREATE. +-- ============================================================================= + +DROP POLICY IF EXISTS incidents_insert ON public.incidents; +CREATE POLICY incidents_insert ON public.incidents FOR INSERT + WITH CHECK ( + space_id IN (SELECT public.get_user_space_ids(auth.uid())) + AND ( + reporter_id IS NULL + OR EXISTS ( + SELECT 1 FROM public.space_members m + WHERE m.id = incidents.reporter_id + AND m.user_id = auth.uid() + AND m.space_id = incidents.space_id + ) + ) + );