M0: add department_id JWT claim, rewrite RLS to avoid profiles queries

auth-hook.sql now copies department_id into JWT claims alongside
role/org_id, so RLS can scope Staff reads without joining profiles
from another table's policy. policies.sql: sops/sop_versions Staff
read policies now use the department_id claim directly; every create
policy paired with a matching drop policy if exists so the file is
safe to re-run. Folds in the supabase_auth_admin profiles-select grant
so policies.sql is self-contained.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FcktbLXSSXzx23GCue813e
This commit is contained in:
Weei Han
2026-07-30 20:38:38 +08:00
co-authored by Claude Sonnet 5
parent f539d43136
commit 1b3ba67468
2 changed files with 56 additions and 7 deletions
+45 -4
View File
@@ -24,12 +24,19 @@
-- access via RLS is only granted where the matrix explicitly says so
-- (insert own, read/write, etc). Everything else is mutated exclusively
-- by service-role /api routes, which bypass RLS entirely.
--
-- All checks below read role, org_id, and department_id from JWT claims
-- (auth.jwt() ->> '...') set by db/auth-hook.sql. No policy in this file
-- ever queries the profiles table — the sops/sop_versions staff-scoping
-- checks use the department_id claim directly instead of joining
-- profiles, avoiding any cross-table RLS dependency.
-- ============================================================
-- orgs (not in matrix — read own org only, no client write)
-- ============================================================
alter table orgs enable row level security;
drop policy if exists orgs_read on orgs;
create policy orgs_read on orgs
for select using (
id = (auth.jwt() ->> 'org_id')::uuid
@@ -41,23 +48,27 @@ create policy orgs_read on orgs
-- ============================================================
alter table departments enable row level security;
drop policy if exists departments_read on departments;
create policy departments_read on departments
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
);
drop policy if exists departments_insert_admin on departments;
create policy departments_insert_admin on departments
for insert with check (
org_id = (auth.jwt() ->> 'org_id')::uuid
and (auth.jwt() ->> 'role') = 'admin'
);
drop policy if exists departments_update_admin on departments;
create policy departments_update_admin on departments
for update using (
org_id = (auth.jwt() ->> 'org_id')::uuid
and (auth.jwt() ->> 'role') = 'admin'
);
drop policy if exists departments_delete_admin on departments;
create policy departments_delete_admin on departments
for delete using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -74,11 +85,25 @@ create policy departments_delete_admin on departments
-- ============================================================
alter table profiles enable row level security;
-- CRITICAL: the JWT claims hook (db/auth-hook.sql) reads role/org_id/
-- department_id from this table on every login/token refresh, running
-- as supabase_auth_admin. Enabling RLS above blocks that read unless
-- this policy exists — without it, the hook silently returns null
-- claims and every other policy in this file fails closed. This grant
-- + policy is idempotent (grant is safe to re-run; the policy already
-- exists from db/auth-hook.sql, not recreated here to avoid a
-- duplicate-policy error).
grant select
on table public.profiles
to supabase_auth_admin;
drop policy if exists profiles_read on profiles;
create policy profiles_read on profiles
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
);
drop policy if exists profiles_update_admin on profiles;
create policy profiles_update_admin on profiles
for update using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -92,6 +117,7 @@ create policy profiles_update_admin on profiles
-- ============================================================
alter table sops enable row level security;
drop policy if exists sops_read on sops;
create policy sops_read on sops
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -103,20 +129,21 @@ create policy sops_read on sops
and exists (
select 1
from sop_assignments sa
join profiles p on p.department_id = sa.department_id
where sa.sop_id = sops.id
and p.id = auth.uid()
and sa.department_id = (auth.jwt() ->> 'department_id')::uuid
)
)
)
);
drop policy if exists sops_insert_editor on sops;
create policy sops_insert_editor on sops
for insert with check (
org_id = (auth.jwt() ->> 'org_id')::uuid
and (auth.jwt() ->> 'role') in ('editor','admin')
);
drop policy if exists sops_update_editor on sops;
create policy sops_update_editor on sops
for update using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -132,6 +159,7 @@ create policy sops_update_editor on sops
-- ============================================================
alter table sop_versions enable row level security;
drop policy if exists sop_versions_read on sop_versions;
create policy sop_versions_read on sop_versions
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -142,9 +170,8 @@ create policy sop_versions_read on sop_versions
and exists (
select 1
from sop_assignments sa
join profiles p on p.department_id = sa.department_id
where sa.sop_id = sop_versions.sop_id
and p.id = auth.uid()
and sa.department_id = (auth.jwt() ->> 'department_id')::uuid
)
)
)
@@ -155,12 +182,14 @@ create policy sop_versions_read on sop_versions
-- ============================================================
alter table approvals enable row level security;
drop policy if exists approvals_read on approvals;
create policy approvals_read on approvals
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
and (auth.jwt() ->> 'role') in ('editor','approver','admin')
);
drop policy if exists approvals_insert on approvals;
create policy approvals_insert on approvals
for insert with check (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -174,6 +203,7 @@ create policy approvals_insert on approvals
-- ============================================================
alter table sop_assignments enable row level security;
drop policy if exists sop_assignments_read on sop_assignments;
create policy sop_assignments_read on sop_assignments
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -185,12 +215,14 @@ create policy sop_assignments_read on sop_assignments
-- ============================================================
alter table acknowledgements enable row level security;
drop policy if exists ack_insert_own on acknowledgements;
create policy ack_insert_own on acknowledgements
for insert with check (
user_id = auth.uid()
and org_id = (auth.jwt() ->> 'org_id')::uuid
);
drop policy if exists ack_read on acknowledgements;
create policy ack_read on acknowledgements
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -203,17 +235,20 @@ create policy ack_read on acknowledgements
-- ============================================================
alter table sop_translations enable row level security;
drop policy if exists translations_read on sop_translations;
create policy translations_read on sop_translations
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
);
drop policy if exists translations_insert_editor on sop_translations;
create policy translations_insert_editor on sop_translations
for insert with check (
org_id = (auth.jwt() ->> 'org_id')::uuid
and (auth.jwt() ->> 'role') in ('editor','admin')
);
drop policy if exists translations_update_editor on sop_translations;
create policy translations_update_editor on sop_translations
for update using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -226,12 +261,14 @@ create policy translations_update_editor on sop_translations
-- ============================================================
alter table audit_log enable row level security;
drop policy if exists audit_read on audit_log;
create policy audit_read on audit_log
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
and (auth.jwt() ->> 'role') in ('approver','admin')
);
drop policy if exists audit_insert on audit_log;
create policy audit_insert on audit_log
for insert with check (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -243,6 +280,7 @@ create policy audit_insert on audit_log
-- ============================================================
alter table ai_log enable row level security;
drop policy if exists ai_log_read_admin on ai_log;
create policy ai_log_read_admin on ai_log
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -255,12 +293,14 @@ create policy ai_log_read_admin on ai_log
-- ============================================================
alter table incidents enable row level security;
drop policy if exists incidents_insert_own on incidents;
create policy incidents_insert_own on incidents
for insert with check (
reporter_id = auth.uid()
and org_id = (auth.jwt() ->> 'org_id')::uuid
);
drop policy if exists incidents_read on incidents;
create policy incidents_read on incidents
for select using (
org_id = (auth.jwt() ->> 'org_id')::uuid
@@ -268,6 +308,7 @@ create policy incidents_read on incidents
or (auth.jwt() ->> 'role') in ('editor','approver','admin') )
);
drop policy if exists incidents_update on incidents;
create policy incidents_update on incidents
for update using (
org_id = (auth.jwt() ->> 'org_id')::uuid