From e856de79b82fe88d50cedcd237079cd84cf575b5 Mon Sep 17 00:00:00 2001 From: Hermes cc-worker Date: Mon, 8 Jun 2026 01:45:56 +0000 Subject: [PATCH] [verified] add taxonomy storage RLS policies (cherry picked from commit bebd0e6312370620dc1e9df97f7e80076a7654be) --- volumes/db/cc/77-storage-taxonomy-rls.sql | 108 ++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 volumes/db/cc/77-storage-taxonomy-rls.sql diff --git a/volumes/db/cc/77-storage-taxonomy-rls.sql b/volumes/db/cc/77-storage-taxonomy-rls.sql new file mode 100644 index 0000000..a8bdc06 --- /dev/null +++ b/volumes/db/cc/77-storage-taxonomy-rls.sql @@ -0,0 +1,108 @@ +-- 77-storage-taxonomy-rls.sql +-- RLS for taxonomy storage buckets. cc.examboards intentionally gets no +-- direct object-read policy: exam-board PDF bytes are exposed only through +-- API-issued, time-limited signed URLs. + +create or replace function public.is_platform_admin() +returns boolean +language sql +stable +security definer +set search_path = public +as $$ + select exists ( + select 1 + from public.admin_profiles ap + where ap.id = auth.uid() + and coalesce(ap.is_super_admin, false) + ) +$$; + +grant execute on function public.is_platform_admin() to authenticated; + +-- Use the first path segment of cc.institutes objects as the institute id. +-- Expected layout: cc.institutes//... +create or replace function public.storage_object_first_path_segment_uuid(p_name text) +returns uuid +language plpgsql +immutable +as $$ +declare + v_segment text; +begin + v_segment := split_part(coalesce(p_name, ''), '/', 1); + if v_segment = '' then + return null; + end if; + begin + return v_segment::uuid; + exception when invalid_text_representation then + return null; + end; +end; +$$; + +grant execute on function public.storage_object_first_path_segment_uuid(text) to authenticated; + +-- Exam-board catalogue rows are authenticated metadata. PDF bytes remain +-- protected separately in cc.examboards storage and require signed URLs. +alter table public.eb_specifications enable row level security; +alter table public.eb_exams enable row level security; + +drop policy if exists "eb_specifications read by authenticated users" on public.eb_specifications; +drop policy if exists "eb_exams read by authenticated users" on public.eb_exams; + +create policy "eb_specifications read by authenticated users" + on public.eb_specifications + for select + to authenticated + using (true); + +create policy "eb_exams read by authenticated users" + on public.eb_exams + for select + to authenticated + using (true); + +alter table storage.objects enable row level security; + +-- Replace only the policies owned by this migration. The absence of any +-- cc.examboards SELECT policy is the deny-by-default posture for raw reads. +drop policy if exists "cc.admin objects read by platform admins" on storage.objects; +drop policy if exists "cc.institutes objects read by institute members" on storage.objects; +drop policy if exists "cc.public objects read by authenticated users" on storage.objects; + +create policy "cc.admin objects read by platform admins" + on storage.objects + for select + to authenticated + using ( + bucket_id = 'cc.admin' + and public.is_platform_admin() + ); + +create policy "cc.institutes objects read by institute members" + on storage.objects + for select + to authenticated + using ( + bucket_id = 'cc.institutes' + and public.storage_object_first_path_segment_uuid(name) in ( + select public.user_institute_ids() + ) + ); + +create policy "cc.public objects read by authenticated users" + on storage.objects + for select + to authenticated + using (bucket_id = 'cc.public'); + +comment on policy "cc.admin objects read by platform admins" on storage.objects + is 'cc.admin raw reads are limited to platform admins.'; +comment on policy "cc.institutes objects read by institute members" on storage.objects + is 'cc.institutes raw reads are scoped by leading path segment institute uuid.'; +comment on policy "cc.public objects read by authenticated users" on storage.objects + is 'cc.public raw reads are available to any authenticated user.'; +comment on table storage.objects + is 'Classroom Copilot taxonomy buckets use RLS; cc.examboards raw reads stay denied and are served through API signed URLs.';