Recursão de RLS entre tabelas cruzadas: como resolvi com SECURITY DEFINER
2026-03-20
Durante o desenvolvimento da plataforma HER, esbarrei num bug de Row Level Security que só aparece quando duas tabelas dependem uma da outra pra decidir quem pode ler o quê.
O sintoma
Uma policy de RLS na tabela beneficios checava se o usuário era associada ativa consultando a tabela associadas. A policy de associadas, por sua vez, checava se o usuário tinha acesso a algum benefício pra decidir se podia ver certos campos. Cada policy parecia razoável isolada — juntas, formavam um ciclo que o planner do Postgres às vezes resolvia, às vezes não, dependendo do plano de execução escolhido.
O resultado: consultas que funcionavam em dev e falhavam silenciosamente (retornando zero linhas, não um erro) em produção, sob certas condições de carga.
A causa raiz
RLS policies rodam com o role do usuário autenticado. Quando uma policy consulta outra tabela que também tem RLS ativo, essa consulta interna herda o mesmo contexto de permissão — e se a policy da segunda tabela depende de volta da primeira, o Postgres não tem garantia de ordem de avaliação. Não é bem uma recursão infinita (o planner não trava), é uma recursão inconsistente: o resultado varia com o plano escolhido.
A correção
A solução foi isolar a checagem cruzada numa função SECURITY DEFINER — uma função que roda com os privilégios de quem a criou (o dono do schema), não do usuário autenticado. Dentro dela, a consulta à tabela associadas acontece SEM re-disparar a RLS de associadas, quebrando o ciclo:
create function is_active_member(user_id uuid)
returns boolean
language sql
security definer
set search_path = public
as $$
select exists (
select 1 from associadas
where id = user_id and status = 'active'
);
$$;
A policy de beneficios passou a chamar is_active_member(auth.uid()) em vez de fazer o join direto. Mesma regra de negócio, zero ambiguidade de avaliação — porque a função definer não está sujeita à RLS que estava causando o ciclo.
A lição
RLS entre tabelas com dependência mútua não é um caso raro o suficiente pra ignorar. Se duas policies fazem referência cruzada, isole a checagem numa função SECURITY DEFINER desde o desenho — não espere o bug aparecer sob carga pra descobrir.