---
title: "Testar Supabase RLS: 30 de 87 permissões não faziam nada"
description: "Testar Supabase RLS política a política não mostra o que o utilizador vê. Uma prova por função numa cópia de produção achou 30 de 87 permissões inúteis."
url: "https://madsalt.tech/pt/blog/how-to-test-supabase-rls/"
author: "Denys Zaichenko"
language: "pt"
published: 2026-10-14
---

# Testar Supabase RLS: 30 de 87 permissões não faziam nada

Em resumo

Teste o row level security (RLS, segurança ao nível da linha) do Supabase da forma como um membro o encontra: entre com cada função, execute a ação real com uma permissão desligada e depois ligada, e verifique se o resultado muda. Numa aplicação em produção com cinco funções editáveis, isto mostrou que 30 das 87 caixas de permissão que um administrador podia guardar não mudavam nada na base de dados. Um teste política a política não consegue ver isto, porque testa a política que escreveu, não a caixa no ecrã.

Sou o Denys, engenheiro full-stack e de IA. No início deste ano construí em Supabase um sistema de gestão de projetos e de registo de horas para uma empresa de auditoria, onde todas as regras de acesso vivem na base de dados. Abaixo está o método que uso para provar essas regras, o que encontrou e quanto custa executá-lo.

## Porque é que os testes de políticas não chegam para testar Supabase RLS?

Um teste de política responde a "esta política faz o que escrevi?". Os seus utilizadores fazem outra pergunta: "se eu desmarcar esta caixa para esta função, ainda conseguem fazê-lo?". Numa aplicação pequena, as duas perguntas têm a mesma resposta. Numa aplicação com uma página de definições onde os administradores editam uma matriz de permissões, afastam-se.

A aplicação tinha 41 tabelas com RLS ativo e 402 instruções `CREATE POLICY` em 255 migrações. O ecrã de administração mostrava uma matriz de áreas (projetos, tarefas, folhas de horas, ficheiros, etc.) contra ações (ver, criar, editar, arquivar), com uma coluna por função. Cada caixa guardada era uma promessa ao administrador. Nada verificava se a base de dados a cumpria.

O Supabase documenta bem as ferramentas habituais: os [testes pgTAP](https://supabase.com/docs/guides/database/extensions/pgtap) e os [helpers de teste](https://supabase.com/docs/guides/local-development/testing/pgtap-extended) permitem criar utilizadores, mudar para eles e verificar o que conseguem ler. Use-os. O método abaixo assenta por cima: é guiado pela matriz que o administrador vê, não pela lista de políticas.

## Como funciona a prova da matriz?

Cada caixa da matriz torna-se uma sonda. Uma sonda entra como membro da função, executa uma ação real e regista o resultado. Fá-lo duas vezes: uma com a caixa desligada e outra com a caixa ligada, com tudo o resto igual. Se o resultado muda, a caixa é aplicada. Se não muda, a caixa não faz nada, por mais convincente que pareça no ecrã.

1. **Uma caixa da matriz**. Exemplo: Membro Standard · Tarefas · Criar
2. **Caixa desligada: agir como membro da função**. Criar uma tarefa pelo mesmo caminho que a aplicação usa. Registar o resultado.
3. **Caixa ligada: repetir a mesma ação**. Mesmo utilizador, mesmos dados. Só a caixa mudou.
4. **Comparar os dois resultados**. O veredicto depende apenas do que a base de dados fez.
- **O resultado mudou**. Aplicada. Recusada quando desligada, permitida quando ligada.
- **Igual nos dois casos**. Não faz nada. O ecrã promete um controlo que não existe.
- **Ecrã e base de dados discordam**. Um bug. A página mostra um botão que a base de dados recusa, ou o contrário.

*Figura 1. Cada veredicto vem de um par de resultados reais, nunca da leitura do código.*

"Resultado" significa aqui o que quer que faça quem consome a permissão: uma política devolve linhas ou recusa uma escrita, uma função da base de dados lança um erro, um trigger bloqueia ou uma edge function responde 403. Uma caixa pode proteger várias destas coisas, por isso as 87 caixas exigiram 279 verificações por função. Cada verificação terminou num destes veredictos:

| Veredicto                      | Verificações por função | O que significa                                                      |
| ------------------------------ | ----------------------- | -------------------------------------------------------------------- |
| Efeito                         | 227                     | O resultado mudou com a caixa                                        |
| Morta                          | 30                      | O resultado ficou igual com a caixa desligada e ligada               |
| Inalcançável                   | 19                      | A ação não está ao alcance desta função, provado à parte             |
| Edge function                  | 1                       | Aplicada por uma edge function, verificada por HTTP                  |
| Só no ecrã                     | 1                       | Muda apenas o que a página mostra, coberta por um teste de front-end |
| Ecrã e base de dados coincidem | 1                       | O que a página oferece corresponde ao que a base de dados permite    |
| Falha                          | 0                       | Uma verificação cujo resultado não correspondeu ao esperado          |

## O que encontrou a prova?

A execução de 15 de setembro de 2026 cobriu as cinco funções que um administrador pode editar (Admin, Senior, Standard, Restricted e uma função personalizada clonada). Os resultados foram idênticos em todas as funções.

![87 caixas de permissão que um administrador podia guardar. Pelo que realmente aplicava cada caixa. Igual para as cinco funções. Aplicadas pela base de dados: 55; Aplicadas noutro sítio: 1 edge function, 1 ecrã: 2; Não mudavam nada: 30.](https://madsalt.tech/charts/pt/how-to-test-supabase-rls.svg)

*Figura 2. As 87 caixas de permissão que um administrador podia guardar, pelo que realmente as aplicava. O mesmo resultado para cada uma das cinco funções.*

Nenhuma das 30 deixava entrar ninguém. Em 29 delas, nenhuma política, função ou trigger lia a caixa; a última deixou de importar depois de uma política ser reescrita. Mesmo assim é um problema real: um administrador que desmarca "Apagar" para uma função acredita que essa função não consegue apagar. Escondemos as 30 em vez de as ligar, porque cada uma teria exigido uma decisão de produto, e uma caixa que não faz nada não deve estar no ecrã enquanto essa decisão é tomada.

O mesmo trabalho revelou mais quatro defeitos:

| O que estava errado                                                                                             | Como se manifestou                                                  | Correção                                                          |
| --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------- |
| 30 caixas não faziam nada                                                                                       | Veredicto "morta", resultado idêntico desligada e ligada            | Escondidas da matriz                                              |
| Três funções da base de dados deixavam passar uma ação quando uma tarefa não tinha responsável                  | Uma sonda que devia ser recusada foi permitida                      | As funções passaram a recusar quando o valor falta                |
| Repor uma folha de horas em rascunho dependia da caixa errada                                                   | Ecrã e base de dados discordavam para uma função                    | Passou a depender da permissão de reabrir, a que lhe pertence     |
| Um membro só via permissões novas depois de recarregar                                                          | A base de dados tinha mudado, a página não                          | As permissões atualizam-se a cada 60 segundos e ao focar a janela |
| O valor por omissão de uma coluna violava a sua própria check constraint, e os inserts sem essa coluna falhavam | Encontrado pela revisão de segurança da correção, no mesmo trabalho | Valor por omissão alterado para um que a constraint aceita        |

O bug do responsável em falta foi o que importou. As verificações estavam escritas assim em PL/pgSQL:

```sql
if not (has_permission or assignee_id = auth.uid()) then
  raise exception 'forbidden';
end if;
```

Quando `assignee_id` é null, `assignee_id = auth.uid()` é null, `false or null` é null, `not null` continua null, e o `if` trata null como falso. A exceção nunca é lançada. Em produção nenhuma tarefa tinha responsável definido, por isso não era um caso limite: aplicava-se a todas as tarefas abrangidas. A correção envolve a condição em `coalesce(…, false)` para que um valor em falta seja recusado, e foi publicada primeiro, sozinha, antes do resto do lote.

## Como se executa contra uma cópia de produção?

Uma prova contra um esquema diferente do de produção não prova nada sobre produção. O harness corre na stack local do Supabase, numa base de dados separada que é uma cópia do esquema de produção mais as migrações em revisão. Antes de cada execução compara uma impressão digital das duas bases de dados locais: 799 linhas que descrevem tabelas, políticas, funções e grants. Qualquer diferença trava a execução.

1. **Esquema de produção**. Copiado para uma única máquina de confiança. Nunca vai para um runner de CI alojado.
2. **Stack local: base de dados da aplicação**. Servida pela API e pela autenticação locais, usada pelas verificações end-to-end.
3. **Stack local: base de dados isolada da prova**. Mesmo esquema, usada apenas pela prova da matriz.
4. **Verificação da impressão digital**. 799 linhas por base de dados. Qualquer diferença trava a execução antes da primeira sonda.
5. **Sondas executadas como cada função**. Como membros reais com sessão iniciada, pelos mesmos caminhos que a aplicação usa.
6. **Sem deixar rasto**. As contagens de linhas no fim têm de ser iguais às do início.

*Figura 3. A prova só conta quando a base de dados onde correu coincide com produção.*

Dois pormenores tornaram o harness fiável:

- **Primeiro, partir de propósito.** Antes de confiar numa execução verde, substituí uma política de insert por uma que aceita tudo e voltei a correr a prova. Tinha de ficar vermelha nas caixas que essa política protege. Um harness que continua verde com uma política partida é decoração.
- **Verificar também o estado guardado.** A primeira passagem começa cada função a partir de uma base limpa. Uma segunda passagem começa a partir da matriz atualmente guardada para cada função, porque uma caixa pode comportar-se de outra forma ao lado das caixas que um administrador realmente marcou.

## Como se mantém verde depois do lançamento?

Uma prova executada uma vez fica desatualizada com a migração seguinte. A prova corre agora antes de cada push que mexa nas migrações, no código de autenticação ou na própria matriz.

1. **git push**. A alteração mexe em migrações, autenticação ou na matriz.
2. **Hook de pre-push: prova da matriz**. Corre localmente contra a base de dados da prova. Vermelho bloqueia o push.
3. **O push chega à main**. O deploy corre como habitualmente.
4. **Execução de CI self-hosted**. A mesma prova outra vez, na máquina que guarda a cópia do esquema de produção.

*Figura 4. A barreira que bloqueia é local, porque a cópia do esquema de produção fica numa só máquina.*

A prova são 135 testes em 11 ficheiros, ao lado de cerca de 3100 testes unitários normais no mesmo projeto.

## O que não funcionou?

- **O CI alojado não a conseguia correr.** A prova precisa da cópia do esquema de produção, e eu não a queria num runner alojado. Por isso a barreira que bloqueia é um hook de pre-push, e o CI numa máquina self-hosted é uma segunda verificação pela qual o deploy não espera.
- **As contagens fixas de linhas partiram-se.** A primeira verificação "sem rasto" comparava com números fixos. A migração legítima seguinte acrescentou uma linha de permissão e pô-la vermelha. Agora compara as contagens do fim com as do início.
- **Ignorado não é aprovado.** As verificações end-to-end precisam do edge runtime local. Quando está em baixo, são ignoradas, e é fácil ler um teste ignorado como verde.
- **É lenta num portátil ocupado.** As passagens por função precisaram de timeouts maiores numa máquina carregada.
- **Prova o que as sondas descrevem.** Uma caixa para a qual ninguém escreveu uma sonda é reportada como lacuna de cobertura, não como segura.

## Como repetir isto na sua aplicação Supabase

1. Liste todas as permissões que o ecrã deixa alguém guardar: função × área × ação. Essa lista é a especificação, não as suas políticas.
2. Para cada caixa, anote a ação real que deve controlar e como se manifesta uma recusa (sem linhas, um erro, um 403).
3. Monte uma base de dados igual à de produção e tire-lhe a impressão digital, para que uma divergência de esquema trave a execução.
4. Execute cada ação como membro com sessão iniciada, caixa desligada e depois ligada, e compare.
5. Parta uma política de propósito e confirme que a prova fica vermelha.
6. Esconda ou ligue cada caixa que não faz nada e faça da prova uma barreira antes do push.

Se a sua aplicação já tem RLS mas não tem a certeza de que faz o que o ecrã de definições diz, o mesmo método serve como auditoria de um código existente, o primeiro passo de um [resgate de projeto](https://madsalt.tech/pt/services/project-rescue/). Escrevi sobre o projeto por trás destes números no [caso do registo de horas para a empresa de auditoria](https://madsalt.tech/pt/work/audit-firm-time-tracking/).

Não tem a certeza de que o seu RLS corresponde ao ecrã de definições?

Troco impressões com gosto sobre como o testa. Sem pitch.

[Fale comigo](https://madsalt.tech/pt/#contact)
