diff --git a/volumes/db/cc/76-cabinet-memberships-rls-definer.sql b/volumes/db/cc/76-cabinet-memberships-rls-definer.sql new file mode 100644 index 0000000..29cc838 --- /dev/null +++ b/volumes/db/cc/76-cabinet-memberships-rls-definer.sql @@ -0,0 +1,88 @@ +-- 76-cabinet-memberships-rls-definer.sql +-- Fix cabinet_memberships/file_cabinets/files recursive RLS. +-- +-- The original cabinet membership policies joined file_cabinets directly, while +-- the file_cabinets/files membership policies joined cabinet_memberships +-- directly. Under an authenticated as-user SELECT this creates an RLS cycle and +-- PostgreSQL raises 42P17 (infinite recursion detected in policy for relation +-- "cabinet_memberships"). +-- +-- SECURITY DEFINER helpers evaluate the ownership/membership checks as the +-- function owner (bypassing the inner RLS checks) while still keying the result +-- to auth.uid(), matching the class-management helper pattern in 71. +-- +-- The existing public tables are owned by supabase_admin; Supabase migrations +-- run as that table owner on dev/prod so the SECURITY DEFINER helpers are owned +-- by a role that bypasses the inner RLS checks. + +create or replace function public.is_cabinet_owner(p_cabinet uuid) +returns boolean +language sql +stable +security definer +set search_path = public +as $$ + select exists ( + select 1 + from public.file_cabinets c + where c.id = p_cabinet + and c.user_id = auth.uid() + ) +$$; + +create or replace function public.is_cabinet_member(p_cabinet uuid) +returns boolean +language sql +stable +security definer +set search_path = public +as $$ + select exists ( + select 1 + from public.cabinet_memberships m + where m.cabinet_id = p_cabinet + and m.profile_id = auth.uid() + ) +$$; + +-- Keep function execution available to the roles used by RLS policies. The +-- functions disclose only a boolean about the caller's own auth.uid() state. +revoke all on function public.is_cabinet_owner(uuid) from public, anon; +revoke all on function public.is_cabinet_member(uuid) from public, anon; +grant execute on function public.is_cabinet_owner(uuid) to authenticated, service_role; +grant execute on function public.is_cabinet_member(uuid) to authenticated, service_role; + +-- Re-declare cabinet_memberships policies without direct file_cabinets subqueries. +drop policy if exists cm_read_self_or_owner on public.cabinet_memberships; +create policy cm_read_self_or_owner on public.cabinet_memberships + for select to authenticated + using (profile_id = auth.uid() or public.is_cabinet_owner(cabinet_id)); + +drop policy if exists cm_insert_by_owner on public.cabinet_memberships; +create policy cm_insert_by_owner on public.cabinet_memberships + for insert to authenticated + with check (public.is_cabinet_owner(cabinet_id)); + +drop policy if exists cm_update_by_owner on public.cabinet_memberships; +create policy cm_update_by_owner on public.cabinet_memberships + for update to authenticated + using (public.is_cabinet_owner(cabinet_id)) + with check (public.is_cabinet_owner(cabinet_id)); + +drop policy if exists cm_delete_by_owner on public.cabinet_memberships; +create policy cm_delete_by_owner on public.cabinet_memberships + for delete to authenticated + using (public.is_cabinet_owner(cabinet_id)); + +-- Re-declare membership-based cabinet/file read policies without direct +-- cabinet_memberships subqueries, so selecting cabinets/files does not recurse +-- back through cabinet_memberships RLS. +drop policy if exists "User can access cabinets via membership" on public.file_cabinets; +create policy "User can access cabinets via membership" on public.file_cabinets + for select to authenticated + using (public.is_cabinet_member(id)); + +drop policy if exists "User can access files via membership" on public.files; +create policy "User can access files via membership" on public.files + for select to authenticated + using (public.is_cabinet_member(cabinet_id));