Implement RLS policies for audit log table
epic-bufdir-reporting-export-foundation-task-004 — Write and apply Row Level Security policies on bufdir_export_audit_log to enforce organisational data isolation. Policy must allow INSERT for authenticated users where org_id matches their JWT claim, allow SELECT only for rows belonging to the user's organisation, and deny UPDATE and DELETE for all roles including service_role via triggers.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 1 - 540 tasks
Can start after Tier 0 completes
Implementation Notes
The critical risk here is using the wrong JWT claim path for org_id — verify with the rest of the codebase how org_id is embedded in the JWT (check existing RLS policies on other tables for the established pattern). Common patterns in Supabase: auth.jwt()->'app_metadata'->>'org_id', or a custom claim added via a Supabase Auth hook. Match the existing convention exactly. The INSERT WITH CHECK policy prevents a malicious client from writing audit records for another organisation — this is important because audit records affect billing and compliance for each organisation.
Do not add an UPDATE policy even as a 'deny all' policy — absence of an UPDATE policy combined with the trigger is cleaner and avoids any risk of a permissive policy accidentally overriding. Apply FORCE ROW LEVEL SECURITY in the same migration file as the policies. If the project uses Supabase Edge Functions for the orchestrator backend, ensure those functions use the user's JWT (not service_role) when inserting audit records, so RLS is exercised in the normal code path.
Testing Requirements
Write a SQL-level integration test or a Dart integration test using two separate Supabase clients authenticated with different org JWTs. Test cases: (1) User A inserts a row with org_id='org-a' — succeeds; (2) User A selects all rows — returns exactly their own row; (3) User B selects all rows — returns empty set; (4) User A attempts INSERT with org_id='org-b' (mismatched claim) — rejected by WITH CHECK policy; (5) User A attempts UPDATE — rejected by trigger; (6) service_role client attempts UPDATE — rejected by trigger. Include these as named test cases in a test file under test/database/ or equivalent. All tests must pass against local Supabase instance in CI.
RLS policies for the audit log and schema config tables must correctly handle multi-chapter membership hierarchies (up to 1,400 local chapters for NHF). Incorrect policies could either over-expose data across organisations or prevent legitimate coordinator access, both of which are serious compliance failures.
Mitigation & Contingency
Mitigation: Design RLS policies using the existing org hierarchy resolver pattern. Write integration tests that verify cross-organisation isolation with representative fixture data covering NHF's multi-level hierarchy before merging.
Contingency: If RLS policies prove too complex to express safely in Postgres, implement a Supabase Edge Function as a data access proxy that enforces isolation in application code, with RLS serving as a secondary defence layer.
Bufdir's column schema is expected to evolve as Norse Digital Products negotiates a simplified digital reporting format. If the schema config versioning model is too rigid, applying Bufdir schema updates without a code deployment could fail, forcing emergency releases.
Mitigation & Contingency
Mitigation: Design the schema config table to store the full JSON column mapping as a JSONB field with a version number. Provide an admin API to upsert new versions without any schema migration required.
Contingency: If the versioning model is insufficient for a Bufdir schema change, fall back to a code deployment with the updated default schema, using the database config only for org-specific overrides.