critical priority high complexity database pending database specialist Tier 0

Acceptance Criteria

Audit covers all five tables: users, activities, reimbursements, organisations, user_roles — no table is skipped
For each table, audit checks all four operations: SELECT, INSERT, UPDATE, DELETE — a missing policy for any operation on any role is recorded as a gap
Audit distinguishes between: no RLS enabled (critical gap), RLS enabled but no policy for operation+role (gap), policy exists but uses overly permissive USING (true) clause (misconfiguration), and policy correctly scopes to org_id or subtree (compliant)
Gap report is a structured document (Markdown or CSV) with columns: table, operation, role, status (gap/misconfiguration/compliant), notes
Audit verifies that the authenticated user's JWT claims (org_id, role) are available and correctly referenced in existing policies via auth.jwt()->'claims'->>'org_id' or equivalent
Audit checks whether a recursive CTE or helper function for org subtree resolution already exists in the database — documents its name and correctness if found
Audit identifies any tables that have RLS disabled entirely — flags these as critical gaps regardless of application-level access control
Audit results are reviewed with a second engineer before task-003 and task-004 begin to confirm correctness
Gap report is committed to the repository under docs/security/rls-audit-{date}.md for traceability
Audit includes a count of total gaps by severity: critical (no RLS), high (missing policy for admin role), medium (overly permissive policy)

Technical Requirements

frameworks
Supabase (PostgreSQL RLS)
apis
Supabase Dashboard Policy Editor
psql or Supabase SQL Editor for pg_policies system catalog queries
data models
users
activities
reimbursements
organisations
user_roles
performance requirements
Audit queries against pg_policies must complete in under 5 seconds — these are metadata queries on small system tables
security requirements
Audit must be performed with a service_role key or direct database access — not with an application key that itself is subject to RLS
Audit findings must be treated as security-sensitive — gap report must not be shared publicly or committed to a public repository
JWT claim structure (org_id, role field names) must be verified against the actual Supabase Auth configuration, not assumed

Execution Context

Execution Tier
Tier 0

Tier 0 - 440 tasks

Implementation Notes

Use this SQL query to enumerate all existing RLS policies: SELECT schemaname, tablename, policyname, permissive, roles, cmd, qual, with_check FROM pg_policies WHERE schemaname = 'public' ORDER BY tablename, cmd; — run this in the Supabase SQL editor. Cross-reference results against the full table list. For tables not appearing in pg_policies results, check RLS enabled status via: SELECT tablename, rowsecurity FROM pg_tables WHERE schemaname = 'public'; — rowsecurity = false means RLS is disabled (critical gap). When reviewing existing policies, look for the pattern: USING (auth.uid() = user_id) which is user-scoped but not org-scoped — this is a gap for admin roles.

Document the JWT claim structure by checking Supabase Auth → JWT template in the Dashboard — the org_id and role claims must be present in the custom claims section. If they are not present, flag this as a blocker for task-003 and task-004 since RLS policies will depend on these claims.

Testing Requirements

This is an audit task — the primary output is a gap report document, not code. Verification: (1) Cross-check pg_policies catalog query results against the Supabase Dashboard policy editor UI to confirm no policies are missing from the query; (2) Run at least one manual test per table: use a test org_admin JWT and attempt SELECT/INSERT/UPDATE/DELETE via the Supabase client — confirm that operations return data scoped to the test org only (or are blocked for disallowed operations); (3) For any table with no RLS enabled, verify that direct table access without a WHERE clause returns all rows — this confirms the critical gap. Document test evidence (screenshots or query results) in the gap report appendix.

Component
Supabase RLS Policy Configuration
infrastructure high
Epic Risks (3)
high impact medium prob security

Missing RLS policies on one or more tables (e.g., a newly added join table or a Supabase view) could expose cross-org data to org_admin queries, creating a GDPR-reportable data breach.

Mitigation & Contingency

Mitigation: Enumerate all tables and views accessed by admin queries before writing any policy. Create an automated test that attempts a cross-org query for each table from an org_admin JWT and asserts an empty result set.

Contingency: If a gap is discovered post-deployment, immediately disable the affected query surface and deploy a hotfix policy before re-enabling. Log the incident and notify DPO if any cross-org data was returned.

high impact medium prob technical

The recursive CTE for NHF's deeply nested org tree (up to 5 levels, 1,400 local chapters) may exceed the 2-second dashboard load target when resolving large subtrees on every request.

Mitigation & Contingency

Mitigation: Benchmark the recursive CTE against a synthetic NHF-scale dataset during development. Introduce a short-TTL server-side cache for subtree resolution results. Index the parent_id column on the organisations table.

Contingency: If CTE performance remains insufficient, materialise the org subtree as a precomputed closure table updated on org structure changes, and switch the RLS guard to query the closure table instead.

high impact low prob security

Incorrect JWT claim injection in AdminRlsGuard (e.g., wrong claim key name or missing refresh on org switch) could silently apply the wrong org scope, causing org_admin to see a different organisation's data without an explicit error.

Mitigation & Contingency

Mitigation: Write unit tests for the guard that verify the injected claim value against the authenticated user's org_id for every admin route. Add a server-side assertion that the claim matches the user's database record before executing any query.

Contingency: Roll back the guard to a deny-all fallback, invalidate active admin sessions, and re-issue corrected JWTs. Audit query logs to identify any sessions that received incorrect scope.