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:
co-authored by
Claude Sonnet 5
parent
f539d43136
commit
1b3ba67468
+11
-3
@@ -3,6 +3,11 @@
|
|||||||
-- "A Postgres function copies role/org_id into JWT claims (custom
|
-- "A Postgres function copies role/org_id into JWT claims (custom
|
||||||
-- access token hook) so RLS can check them cheaply."
|
-- access token hook) so RLS can check them cheaply."
|
||||||
--
|
--
|
||||||
|
-- Also copies department_id — needed so RLS policies can scope Staff
|
||||||
|
-- reads to "published + assigned to their department" (04-database-
|
||||||
|
-- schema.md section 3) using JWT claims only, without any policy ever
|
||||||
|
-- querying the profiles table from another table's policy.
|
||||||
|
--
|
||||||
-- Run this AFTER schema.sql (needs the profiles table to exist).
|
-- Run this AFTER schema.sql (needs the profiles table to exist).
|
||||||
-- After running, enable the hook in the Supabase dashboard (see bottom
|
-- After running, enable the hook in the Supabase dashboard (see bottom
|
||||||
-- of this file for the exact steps) — running the SQL alone does not
|
-- of this file for the exact steps) — running the SQL alone does not
|
||||||
@@ -17,10 +22,11 @@ as $$
|
|||||||
claims jsonb;
|
claims jsonb;
|
||||||
user_role public.user_role;
|
user_role public.user_role;
|
||||||
user_org_id uuid;
|
user_org_id uuid;
|
||||||
|
user_department_id uuid;
|
||||||
begin
|
begin
|
||||||
-- Look up this user's role and org_id from profiles.
|
-- Look up this user's role, org_id, and department_id from profiles.
|
||||||
select role, org_id
|
select role, org_id, department_id
|
||||||
into user_role, user_org_id
|
into user_role, user_org_id, user_department_id
|
||||||
from public.profiles
|
from public.profiles
|
||||||
where id = (event->>'user_id')::uuid;
|
where id = (event->>'user_id')::uuid;
|
||||||
|
|
||||||
@@ -29,9 +35,11 @@ as $$
|
|||||||
if user_role is not null then
|
if user_role is not null then
|
||||||
claims := jsonb_set(claims, '{role}', to_jsonb(user_role));
|
claims := jsonb_set(claims, '{role}', to_jsonb(user_role));
|
||||||
claims := jsonb_set(claims, '{org_id}', to_jsonb(user_org_id));
|
claims := jsonb_set(claims, '{org_id}', to_jsonb(user_org_id));
|
||||||
|
claims := jsonb_set(claims, '{department_id}', to_jsonb(user_department_id));
|
||||||
else
|
else
|
||||||
claims := jsonb_set(claims, '{role}', 'null');
|
claims := jsonb_set(claims, '{role}', 'null');
|
||||||
claims := jsonb_set(claims, '{org_id}', 'null');
|
claims := jsonb_set(claims, '{org_id}', 'null');
|
||||||
|
claims := jsonb_set(claims, '{department_id}', 'null');
|
||||||
end if;
|
end if;
|
||||||
|
|
||||||
event := jsonb_set(event, '{claims}', claims);
|
event := jsonb_set(event, '{claims}', claims);
|
||||||
|
|||||||
+45
-4
@@ -24,12 +24,19 @@
|
|||||||
-- access via RLS is only granted where the matrix explicitly says so
|
-- access via RLS is only granted where the matrix explicitly says so
|
||||||
-- (insert own, read/write, etc). Everything else is mutated exclusively
|
-- (insert own, read/write, etc). Everything else is mutated exclusively
|
||||||
-- by service-role /api routes, which bypass RLS entirely.
|
-- 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)
|
-- orgs (not in matrix — read own org only, no client write)
|
||||||
-- ============================================================
|
-- ============================================================
|
||||||
alter table orgs enable row level security;
|
alter table orgs enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists orgs_read on orgs;
|
||||||
create policy orgs_read on orgs
|
create policy orgs_read on orgs
|
||||||
for select using (
|
for select using (
|
||||||
id = (auth.jwt() ->> 'org_id')::uuid
|
id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
@@ -41,23 +48,27 @@ create policy orgs_read on orgs
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
alter table departments enable row level security;
|
alter table departments enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists departments_read on departments;
|
||||||
create policy departments_read on departments
|
create policy departments_read on departments
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists departments_insert_admin on departments;
|
||||||
create policy departments_insert_admin on departments
|
create policy departments_insert_admin on departments
|
||||||
for insert with check (
|
for insert with check (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
and (auth.jwt() ->> 'role') = 'admin'
|
and (auth.jwt() ->> 'role') = 'admin'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists departments_update_admin on departments;
|
||||||
create policy departments_update_admin on departments
|
create policy departments_update_admin on departments
|
||||||
for update using (
|
for update using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
and (auth.jwt() ->> 'role') = 'admin'
|
and (auth.jwt() ->> 'role') = 'admin'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists departments_delete_admin on departments;
|
||||||
create policy departments_delete_admin on departments
|
create policy departments_delete_admin on departments
|
||||||
for delete using (
|
for delete using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
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
|
create policy profiles_read on profiles
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists profiles_update_admin on profiles;
|
||||||
create policy profiles_update_admin on profiles
|
create policy profiles_update_admin on profiles
|
||||||
for update using (
|
for update using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
alter table sops enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists sops_read on sops;
|
||||||
create policy sops_read on sops
|
create policy sops_read on sops
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
@@ -103,20 +129,21 @@ create policy sops_read on sops
|
|||||||
and exists (
|
and exists (
|
||||||
select 1
|
select 1
|
||||||
from sop_assignments sa
|
from sop_assignments sa
|
||||||
join profiles p on p.department_id = sa.department_id
|
|
||||||
where sa.sop_id = sops.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
|
create policy sops_insert_editor on sops
|
||||||
for insert with check (
|
for insert with check (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
and (auth.jwt() ->> 'role') in ('editor','admin')
|
and (auth.jwt() ->> 'role') in ('editor','admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists sops_update_editor on sops;
|
||||||
create policy sops_update_editor on sops
|
create policy sops_update_editor on sops
|
||||||
for update using (
|
for update using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
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
|
create policy sop_versions_read on sop_versions
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
@@ -142,9 +170,8 @@ create policy sop_versions_read on sop_versions
|
|||||||
and exists (
|
and exists (
|
||||||
select 1
|
select 1
|
||||||
from sop_assignments sa
|
from sop_assignments sa
|
||||||
join profiles p on p.department_id = sa.department_id
|
|
||||||
where sa.sop_id = sop_versions.sop_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;
|
alter table approvals enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists approvals_read on approvals;
|
||||||
create policy approvals_read on approvals
|
create policy approvals_read on approvals
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
and (auth.jwt() ->> 'role') in ('editor','approver','admin')
|
and (auth.jwt() ->> 'role') in ('editor','approver','admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists approvals_insert on approvals;
|
||||||
create policy approvals_insert on approvals
|
create policy approvals_insert on approvals
|
||||||
for insert with check (
|
for insert with check (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
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
|
create policy sop_assignments_read on sop_assignments
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
alter table acknowledgements enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists ack_insert_own on acknowledgements;
|
||||||
create policy ack_insert_own on acknowledgements
|
create policy ack_insert_own on acknowledgements
|
||||||
for insert with check (
|
for insert with check (
|
||||||
user_id = auth.uid()
|
user_id = auth.uid()
|
||||||
and org_id = (auth.jwt() ->> 'org_id')::uuid
|
and org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists ack_read on acknowledgements;
|
||||||
create policy ack_read on acknowledgements
|
create policy ack_read on acknowledgements
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
alter table sop_translations enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists translations_read on sop_translations;
|
||||||
create policy translations_read on sop_translations
|
create policy translations_read on sop_translations
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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
|
create policy translations_insert_editor on sop_translations
|
||||||
for insert with check (
|
for insert with check (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
and (auth.jwt() ->> 'role') in ('editor','admin')
|
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
|
create policy translations_update_editor on sop_translations
|
||||||
for update using (
|
for update using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
alter table audit_log enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists audit_read on audit_log;
|
||||||
create policy audit_read on audit_log
|
create policy audit_read on audit_log
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
and (auth.jwt() ->> 'role') in ('approver','admin')
|
and (auth.jwt() ->> 'role') in ('approver','admin')
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists audit_insert on audit_log;
|
||||||
create policy audit_insert on audit_log
|
create policy audit_insert on audit_log
|
||||||
for insert with check (
|
for insert with check (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
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
|
create policy ai_log_read_admin on ai_log
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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;
|
alter table incidents enable row level security;
|
||||||
|
|
||||||
|
drop policy if exists incidents_insert_own on incidents;
|
||||||
create policy incidents_insert_own on incidents
|
create policy incidents_insert_own on incidents
|
||||||
for insert with check (
|
for insert with check (
|
||||||
reporter_id = auth.uid()
|
reporter_id = auth.uid()
|
||||||
and org_id = (auth.jwt() ->> 'org_id')::uuid
|
and org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists incidents_read on incidents;
|
||||||
create policy incidents_read on incidents
|
create policy incidents_read on incidents
|
||||||
for select using (
|
for select using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
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') )
|
or (auth.jwt() ->> 'role') in ('editor','approver','admin') )
|
||||||
);
|
);
|
||||||
|
|
||||||
|
drop policy if exists incidents_update on incidents;
|
||||||
create policy incidents_update on incidents
|
create policy incidents_update on incidents
|
||||||
for update using (
|
for update using (
|
||||||
org_id = (auth.jwt() ->> 'org_id')::uuid
|
org_id = (auth.jwt() ->> 'org_id')::uuid
|
||||||
|
|||||||
Reference in New Issue
Block a user