[verified] fix cabinet memberships recursive RLS
Some checks failed
supabase-ci / validate (push) Has been cancelled

(cherry picked from commit facdfd21c9c0c17f09e1a3a5fbe2e2b253f76d82)
This commit is contained in:
Hermes cc-worker 2026-06-08 00:59:02 +00:00
parent f8fcff600f
commit 0f2aca3a73

View File

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