-- 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.';