-- 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));