---
title: "Supabase RLS multi-tenant: utilizador em duas orgs"
description: "Uma política de RLS que verifica a pertença a uma org é verdadeira nas duas para quem pertence a ambas. O que a sonda achou, a correção e um teste rápido."
url: "https://madsalt.tech/pt/notes/supabase-rls-user-in-two-organizations/"
author: "Denys Zaichenko"
language: "pt"
published: 2026-10-18
---

# Supabase RLS multi-tenant: utilizador em duas orgs

Em resumo

Uma política como `auth_in_org(organization_id)` pergunta "o utilizador é membro da org a que esta linha pertence?". Para um utilizador que pertence à org A e à org B, a resposta é sim nas duas. Na minha sonda, só com RLS esse utilizador tinha acesso de leitura e escrita a um documento da outra org. Ao acrescentar à consulta a org do pedido atual, o resultado foi 0 linhas.

Encontrei isto ao reforçar a [plataforma de documentos de construção](https://madsalt.tech/pt/work/construction-document-platform/), uma aplicação Supabase multi-tenant. Todas as tabelas de tenant tinham row level security ativo e forçado, por isso parecia concluída.

## O que parecia seguro?

Todas as políticas tinham a mesma forma: o `organization_id` da linha tem de ser uma org de que o utilizador é membro. Por isso, algumas server actions procuravam um documento só pelo id. O raciocínio era "o RLS filtra de qualquer forma".

```ts
const { data } = await supabase
  .from('documents')
  .select('*')
  .eq('document_id', id)
  .single()
```

Para um utilizador com uma só pertença isto funciona, e um teste com esse utilizador passa.

## Porque é que o RLS deixa passar a outra org?

A política é um teste de pertença. O Postgres avalia-a linha a linha, como descreve a [documentação de row security](https://www.postgresql.org/docs/current/ddl-rowsecurity.html), e não sabe em nome de que organização o pedido está a agir, porque isso vive na aplicação: no URL, num cookie ou no seletor de orgs. Para um utilizador que é membro das duas orgs, todas as linhas das duas passam.

![A org do pedido não está na política. Um utilizador, membro da org A e da org B, a trabalhar na org A. Só a política de RLS: Pedido na org A → Membro da org da linha? → Sim, A ou B → Linha da org B; RLS mais a org deste pedido: Pedido na org A → Membro da org da linha? → Linha na org A? → 0 linhas da org B.](https://madsalt.tech/charts/pt/supabase-rls-user-in-two-organizations.svg)

*A minha sonda com um utilizador de teste em duas orgs, setembro de 2026: só com RLS, leitura e escrita no documento da org B; com a org do pedido, 0 linhas.*

Ou seja, o filtro por `organization_id` na aplicação não é uma segunda camada por cima do RLS. Para este utilizador, é o único controlo.

Já que está nesses helpers, verifique se olham para `organizations.is_active`. Os helpers de pertença muitas vezes não olham, e então uma org desativada mantém acesso de leitura e escrita na camada de RLS. Quando a única verificação de `is_active` vive no resolvedor de tenant da aplicação, qualquer consulta que ignore o resolvedor ignora também a desativação.

## O que resolveu?

A org do pedido passou a ser um argumento obrigatório de todas as funções de serviço que leem ou escrevem dados de tenant. Um ponto de chamada não se pode esquecer dela, porque a função não compila sem ela.

```ts
export async function getDocument(orgId: string, id: string) {
  return supabase
    .from('documents')
    .select('*')
    .eq('organization_id', orgId)
    .eq('document_id', id)
    .single()
}
```

A revisão deu mais duas regras:

- Nunca tome a org da linha que vai escrever. `update ... where id = $1 and organization_id = <o organization_id da própria linha>` é sempre verdadeiro, por isso não protege nada.
- Uma chave estrangeira composta em `(id, organization_id)` faz a base de dados recusar uma ligação entre linhas de orgs diferentes. Verifique todas as tabelas filhas: é fácil acrescentá-la a três e esquecer a quarta.

## Como verificar a sua aplicação?

Crie um utilizador com uma pertença ativa em duas orgs. Depois, no editor SQL de uma cópia local ou de staging, aja como esse utilizador e peça uma linha da outra org:

```sql
begin;
set local role authenticated;
set local request.jwt.claims = '{"sub": "<user id>", "role": "authenticated"}';

-- RLS alone: returns the org B document
select document_id from documents where document_id = '<org B document id>';

-- with the org of the request: 0 rows
select document_id from documents
where document_id = '<org B document id>'
  and organization_id = '<org A id>';
rollback;
```

Repita com um `update`, e outra vez depois de pôr a segunda org como inativa. Se a primeira consulta devolver uma linha, veja todos os sítios que consultam dados de tenant só pelo id.

## Quão grave é?

Dimensione-o com honestidade. O atacante precisa de uma pertença real nas duas orgs, por isso isto é sobre integridade e trilhos de auditoria: alguém com acesso a dois clientes edita o errado, e o registo diz que era permitido. Não é uma forma de um estranho ler todos os tenants.

A lição geral é a de [como testo o Supabase RLS](https://madsalt.tech/pt/blog/how-to-test-supabase-rls/): teste o que um utilizador real consegue fazer, com dados reais, e não só o que cada política diz. Três notas relacionadas: [uma função tirada dos metadados do registo](https://madsalt.tech/pt/notes/supabase-raw-user-meta-data-role/), [um NULL que saltou uma verificação de permissão](https://madsalt.tech/pt/notes/plpgsql-if-not-null-fails-open/) e [que schemas a Data API serve](https://madsalt.tech/pt/notes/supabase-exposed-schemas/). O teste das duas empresas é a primeira verificação da minha [lista de segurança para apps Lovable](https://madsalt.tech/pt/blog/lovable-app-security-checklist/). Para as políticas, grants e definições à volta, a [Auditoria de segurança Supabase](https://madsalt.tech/pt/tools/supabase-security-check/) gratuita executa 46 regras do OWASP Top 10:2025; se uma política deixa passar um utilizador de duas orgs é uma questão de forma dos dados a que só a consulta acima responde.
