Marcio Cunha

Por que usar Row-Level Security no PostgreSQL para isolar clientes

Aprenda a isolar dados de múltiplos clientes na mesma base de dados usando o Row-Level Security do PostgreSQL, garantindo segurança a nível de linha com contexto de sessão.

Marcio Cunha•6 min
Também disponível em:EnglishEspañol
Resumo
  • O isolamento de dados por linha evita vazamentos acidentais entre diferentes clientes na mesma tabela relacional
  • Variaveis de sessao do PostgreSQL permitem injetar o identificador do inquilino de forma dinamica e segura
  • Politicas de segurança reduzem a complexidade do codigo da aplicacao ao delegar o filtro para o banco
  • Conexoes persistentes exigem limpeza de contexto para evitar contaminacao de dados entre requisicoes
  • Testar politicas com usuarios restritos garante que o banco proteja os dados mesmo em falhas de codigo

O desafio de isolar dados sem multiplicar bancos de dados

Quando criamos sistemas que atendem varias empresas ou clientes no mesmo ambiente, surge um problema critico: como garantir que a empresa A nunca veja os dados da empresa B. A abordagem tradicional envolve criar um banco de dados separado para cada cliente ou adicionar clausulas em todas as consultas SQL. No entanto, gerenciar centenas de bancos aumenta o custo operacional e complica os backups. Por outro lado, confiar apenas no codigo da aplicacao para filtrar os dados em cada consulta abre brechas perigosas para falhas humanas e vazamentos acidentais. É exatamente aqui que o conceito de estrategias de isolamento multi-tenant em bases de dados relacionais se torna fundamental para manter a arquitetura limpa e segura.

Para resolver esse dilema na raiz, o PostgreSQL oferece uma ferramenta poderosa chamada Row-Level Security, ou seguranca em nivel de linha. Em termos simples, trata-se de um mecanismo onde o proprio banco de dados decide quais linhas podem ser vistas ou modificadas por quem esta fazendo a consulta. Quando um usuario envia um comando, o banco aplica filtros de forma invisivel nos bastidores, baseando-se em regras pre-definidas. Na pratica, isso significa que mesmo que um desenvolvedor esqueca de colocar um filtro na busca, o banco bloqueia o acesso indevido automaticamente. Essa protecao em camadas transforma o banco de dados no ultimo guardiao da informacao.

Como o PostgreSQL entende quem esta pedindo os dados

Para que o banco saiba qual cliente esta fazendo a solicitacao, precisamos injetar um contexto na sessao atual. O PostgreSQL permite criar variaveis personalizadas que acompanham a conexao enquanto ela estiver aberta. Quando a aplicacao recebe uma requisicao web, ela primeiro valida a identidade do usuario e descobre a qual empresa ele pertence. Em seguida, antes de rodar qualquer consulta de negocios, a aplicacao executa um comando rapido para gravar esse identificador na sessao do banco de dados, usando parametros configuraveis.

Esse processo funciona como um cracha digital que o aplicativo entrega ao banco assim que a conexao e estabelecida para aquela requisicao especifica. Para entender melhor como estruturar essas conexoes eficientemente, vale a pena conferir o guia sobre por que o Prisma quebra no PostgreSQL e como usar o PgBouncer, que aborda os cuidados necessarios com o reaproveitamento de conexoes. Sem esse cracha atualizado, o banco nao saberia quem e o usuario e as regras de seguranca nao conseguiriam filtrar os registros corretamente. É a combinacao entre essa variavel de sessao e as regras de tabela que da vida a seguranca multi-tenant.

Criando a tabela e ativando as travas de seguranca

Vamos imaginar um cenario pratico onde temos uma tabela de pedidos corporativos que armazena informacoes de varios clientes diferentes. O primeiro passo e criar essa tabela normalmente, garantindo que exista uma coluna dedicada a identificar o dono de cada registro. Em seguida, precisamos explicitamente ligar o mecanismo de seguranca nessa tabela especifica, informando ao banco que ele deve comecar a prestar atencao nas regras que criaremos logo depois. Veja como estruturar essa base inicial com comandos SQL puros.

CREATE TABLE pedidos (
    id SERIAL PRIMARY KEY,
    tenant_id UUID NOT NULL,
    descricao TEXT NOT NULL,
    valor NUMERIC(10, 2) NOT NULL
);

-- Ativa a seguranca a nivel de linha na tabela
ALTER TABLE pedidos ENABLE ROW LEVEL SECURITY;

Depois de ativar a trava na tabela, o comportamento padrao do PostgreSQL muda drasticamente para qualquer usuario que nao seja o superusuario ou o dono da tabela. Se nenhuma regra for criada apos ativar o recurso, o banco simplesmente escondera todas as linhas, retornando resultados vazios por padrao. Isso acontece por questoes de seguranca extrema: o banco prefere bloquear tudo a deixar qualquer dado exposto por descuido. Portanto, o proximo passo obrigatorio e ensinar o banco exatamente quem pode ver o que atraves das politicas de acesso.

Escrevendo as regras de acesso baseadas no contexto

As politicas de seguranca sao as instrucoes logicas que dizem ao banco quando liberar ou barrar uma linha. Criamos uma regra usando o comando CREATE POLICY, definindo se ela se aplica apenas a leitura, insercao ou alteracao de dados. Dentro dessa regra, usamos a funcao do sistema que le a variavel de sessao que definimos anteriormente. Desta forma, o banco compara o identificador guardado na linha da tabela com o identificador guardado na sessao atual do usuario.

Abaixo temos um exemplo pratico de como escrever essa regra diretamente no console do seu banco de dados relacional. Repare que utilizamos uma funcao para converter a variavel de texto da sessao para o formato correto de identificador unico universal, garantindo comparacoes precisas.

CREATE POLICY tenant_isolation_policy ON pedidos
    AS RESTRICTIVE
    FOR ALL
    USING (tenant_id = nullif(current_setting('app.current_tenant_id', true), '')::uuid)
    WITH CHECK (tenant_id = nullif(current_setting('app.current_tenant_id', true), '')::uuid);

Essa regra e dividida em duas partes fundamentais para garantir protecao total. A clausula USING controla quais linhas existentes podem ser vistas ou alteradas nas consultas de leitura, atualizacao e exclusao. Ja a clausula WITH CHECK garante que novos registros inseridos ou modificados pertencam obrigatoriamente ao mesmo cliente ativo na sessao, impedindo que um usuario mal intencionado insira dados falsos vinculados a outra empresa. Essa simetria elimina brechas logicas comuns em sistemas distribuidos.

Cuidados operacionais e armadilhas ao usar conexoes compartilhadas

Implementar seguranca por contexto de sessao exige muita disciplina no codigo da sua aplicacao. Quando usamos ferramentas de pool de conexoes para otimizar a performance, as conexoes com o banco sao recicladas e reutilizadas por diferentes usuarios em momentos distintos. Se a sua aplicacao esquecer de limpar ou redefinir a variavel de sessao ao devolver a conexao para o grupo, o proximo usuario que pegar aquela mesma conexao herdara o contexto anterior, podendo visualizar dados confidenciais por engano. Para evitar esse tipo de desastre, garanta sempre que a definicao do contexto ocorra dentro de uma transacao isolada logo apos a obtencao da conexao.

Outro ponto de atencao diz respeito ao desempenho das consultas em tabelas muito grandes. Como o banco precisa aplicar o filtro de comparacao em cada linha escaneada, e absolutamente obrigatorio criar indices eficientes na coluna que identifica o inquilino. Sem um indice adequado nessa coluna, o banco sera obrigado a fazer varreduras completas na tabela inteira a cada requisicao, degradando severamente a performance do sistema a medida que a base cresce. Planejar a infraestrutura e os indices com cuidado garante que a seguranca nao custe a velocidade da sua aplicacao. Para aprofundar esses conceitos na prática em produção, confira também nossa análise técnica sobre Disco lento no Linux? Como achar gargalos de IOPS e latência.

Consideracoes finais

O uso de Row-Level Security no PostgreSQL representa uma evolucao madura para arquiteturas multi-tenant, combinando simplicidade de manutencao com seguranca reforcada diretamente no nucleo do armazenamento. Ao delegar o isolamento dos dados para o banco, removemos uma camada pesada de validacao manual do codigo e criamos uma barreira intransponivel contra vazamentos acidentais. Embora exija rigor na gestao das sessoes e no uso de agrupadores de conexao, os ganhos em robustez e tranquilidade operacional compensam amplamente o esforco de configuracao inicial.

Adotar essa estrategia desde o inicio do projeto evita refatoracoes complexas no futuro, permitindo que a sua aplicacao escale de forma saudavel e segura. Lembre-se sempre de testar exaustivamente as politicas com usuarios restritos e manter um monitoramento ativo do desempenho das consultas para garantir que sua base continue respondendo com agilidade sob qualquer volume de acesso.