Build Admin RLS Guard middleware
epic-admin-portal-foundation-task-006 — Implement the AdminRlsGuard service that intercepts every Supabase client session for admin-role users and injects org-scope JWT claims before each request. The guard must read the authenticated user's org_id from the session store, call the Supabase set_config RPC to inject the claim, and expose a guardedClient getter used by all admin repository methods.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 3 - 413 tasks
Can start after Tier 2 completes
Implementation Notes
In Flutter/Riverpod, implement AdminRlsGuard as a class provided by a Riverpod Provider (not StateNotifier — it holds no mutable state). The guardedClient getter should be an async method: (1) fetch supabase.auth.currentSession, (2) extract org_id from session.user.appMetadata['org_id'], (3) validate role from appMetadata['role'], (4) call await supabase.rpc('set_config', params: {'key': 'app.current_org_id', 'value': orgId}), (5) return the SupabaseClient instance. Because set_config is session-scoped in PostgreSQL (not transaction-scoped), it persists for the connection lifetime — but Supabase uses connection pooling so always re-inject on each logical request. Consider wrapping in a helper executeWithGuard
Testing Requirements
Unit tests (flutter_test with mocked SupabaseClient): (1) guardedClient with valid org_admin session calls set_config with correct org_id. (2) guardedClient with super_admin session injects global scope sentinel or skips scoping as per design. (3) Unauthenticated session throws AuthException. (4) Session with non-admin role throws AuthException.
(5) set_config RPC failure (network error) propagates as a typed exception rather than silent failure. (6) Second call to guardedClient within same session still calls set_config (no stale caching). Integration test: confirm an actual Supabase query through guardedClient is restricted to the injected org's subtree.
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.
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.
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.