02 - Conceito mais detalhado de RLS no PostgresSQL na prática
Vide o artigo antes:
01 - Multi-Tenant
O Row-Level Security (RLS) no PostgreSQL é uma ferramenta poderosa e ideal para o cenário de aplicação multi-tenant com esquema e banco de dados compartilhados. Ele permite que você defina políticas para controlar quais linhas (registros) de uma tabela são visíveis e podem ser modificadas por um determinado usuário, ou, no seu caso, por um determinado Tenant.
🛡️ Conceito de Row-Level Security (RLS)
O RLS funciona aplicando uma política de segurança diretamente nas tabelas. Quando o RLS está ativado em uma tabela, todas as consultas que tentam acessar ou modificar os dados (como SELECT, INSERT, UPDATE, DELETE) são automaticamente reescritas pelo PostgreSQL para incluir a lógica de segurança definida na política.
- Política: É uma expressão booleana que o PostgreSQL avalia para determinar se uma linha deve ser retornada ou se uma operação deve ser permitida. Essa expressão geralmente usa informações do contexto de sessão atual (como o usuário que está conectado ou uma variável de configuração).
⚙️ RLS na Prática e o tenant_id
No meu entendimento, RLS me possibilita a idéia de que eu não precise incluir o tenant_id manualmente em nenhuma das instruções SQL da aplicação.
1. Estabelecer o Tenant (Contexto de Sessão)
Para que o RLS funcione sem que você passe o tenant_id nas suas consultas, você precisa estabelecer o tenant_id no contexto da sessão do PostgreSQL no momento em que a aplicação se conecta (ou logo após) e antes de executar qualquer consulta.
Isso é feito utilizando variáveis de configuração personalizadas (ou, no jargão do PostgreSQL, Custom Configuration Variables).
- Passo Prático: Imediatamente após a aplicação abrir uma conexão com o banco de dados e antes de executar a primeira consulta de dados, você deve rodar a seguinte instrução:
SET app.current_tenant_id = 'O_ID_DO_TENANT_ATUAL';
Onde app.current_tenant_id é o nome da variável de sessão que você irá criar para armazenar o ID do tenant. Lembre-se que essa variável deve ser restrita àquela conexão/sessão.
Seu usar app.current_tenant_id nas políticas, é essa variável que terá que ser alimentada pelo comando SET no middleware que você terá que criar na aplicação BACKEND.
2. Criação e Ativação do RLS na Tabela
O RLS deve ser ativado por tabela.
- Ativar RLS:
ALTER TABLE nome_da_tabela ENABLE ROW LEVEL SECURITY;
3. Definir a Política de Segurança (O Coração do RLS)
Esta é a parte crucial. Você define uma política que compara a coluna tenant_id da tabela com a variável de sessão que você acabou de definir.
-
Política de Isolamento para
SELECT(e operações de leitura):Para garantir que um tenant só possa ver seus próprios dados, você cria uma política do tipo
USING.CREATE POLICY tenant_isolation_select ON nome_da_tabela FOR SELECT USING text = current_setting('app.current_tenant_id');USING (...): Esta expressão é adicionada como umWHEREimplícito a toda instruçãoSELECT. O PostgreSQL só retornará as linhas onde otenant_idda linha for igual ao valor que você definiu na variável de sessãoapp.current_tenant_id.
🔒 Garantindo a Segurança (Consulta, Insert, Update, Delete)
Para garantir o isolamento total, você precisa de políticas que cubram todas as operações de manipulação de dados.
1. Garantia para Consultas (SELECT)
A política FOR SELECT que definimos acima é suficiente.
-
Política:
CREATE POLICY tenant_isolation_select ON nome_da_tabela FOR SELECT USING text = current_setting('app.current_tenant_id');- Resultado: Se o seu código executar
SELECT * FROM nome_da_tabela;, o PostgreSQL o transforma internamente em:
- Resultado: Se o seu código executar
SELECT * FROM nome_da_tabela WHERE tenant_id = current_setting('app.current_tenant_id')::bigint;
Note que é usado ::text e ::bigint para garantir a comparação entre a string da variável de sessão e o tipo bigint da coluna.
2. Garantia para Inserção (INSERT)
Para garantir que novos registros sejam sempre criados com o tenant_id correto (e para evitar que alguém tente inserir um registro para outro tenant), você usa a cláusula WITH CHECK.
- Política:
CREATE POLICY tenant_isolation_insert ON nome_da_tabela
FOR INSERT
WITH CHECK text = current_setting('app.current_tenant_id');
Resultado: Antes de permitir o INSERT, o PostgreSQL verifica se o valor do tenant_id que está sendo inserido é o mesmo definido na sessão. Isso protege contra um atacante que tenta rodar um INSERT explicitamente passando um ID de outro tenant. Se o INSERT da sua aplicação não passar o tenant_id, você precisará de um default na coluna, ou seu ORM/framework precisa passar esse ID.
3. Garantia para Atualização (UPDATE) e Exclusão (DELETE)
Para UPDATE e DELETE, você precisa garantir duas coisas:
-
O tenant só pode acessar/selecionar as linhas dele para atualizar/deletar (cláusula
USING). -
O tenant não pode alterar o
tenant_idde uma linha para o ID de outro tenant (cláusulaWITH CHECK).
-
Política:
CREATE POLICY tenant_isolation_update_delete ON nome_da_tabela FOR ALL -- Ou FOR UPDATE, FOR DELETE individualmente USING text = current_setting('app.current_tenant_id') WITH CHECK text = current_setting('app.current_tenant_id');-
Resultado:
-
USING: Garante que oUPDATEouDELETEsó consiga encontrar linhas pertencentes ao tenant atual. -
WITH CHECK: Garante que, se for uma operação deUPDATE, o novo valor dotenant_id(se for atualizado) ainda pertença ao tenant atual.
-
-
Ao implementar essas políticas, você obtém um isolamento de dados robusto no nível do banco de dados, e sua aplicação ganha em simplicidade, pois as instruções SQL dentro do código podem se concentrar apenas na lógica de negócios, sem se preocupar com a filtragem do tenant_id.
Exemplo Prático
Segue abaixo um exemplo focado na camada de Domínio/Infraestrutura onde o RLS é implementado, e como a sua camada de Adaptação (REST) deve gerenciar o contexto do tenant.
🏛️ Exemplo Prático com Arquitetura Hexagonal e RLS
Neste exemplo, assumimos que você já tem uma tabela orders e order_items, ambas com a coluna tenant_id BIGINT NOT NULL.
Passo 1: Configuração do RLS no PostgreSQL
Primeiro, garanta que o RLS esteja ativo nas tabelas e que as políticas de segurança estejam definidas.
-- 1. Ativação do RLS nas tabelas
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
ALTER TABLE order_items ENABLE ROW LEVEL SECURITY;
-- 2. Política de Isolamento para TODAS as operações
-- A política irá comparar o tenant_id da linha com a variável de sessão 'app.current_tenant_id'
CREATE POLICY tenant_isolation ON orders
FOR ALL
TO PUBLIC -- Aplica a todos os usuários, exceto superusuários
USING text = current_setting('app.current_tenant_id')
WITH CHECK text = current_setting('app.current_tenant_id');
CREATE POLICY tenant_isolation ON order_items
FOR ALL
TO PUBLIC
USING text = current_setting('app.current_tenant_id')
WITH CHECK text = current_setting('app.current_tenant_id');
Passo 2: O Middleware na Camada de Adaptação (REST / chi)
Este é o ponto onde você captura o tenant_id (assumimos que ele vem de um JWT, um cabeçalho ou alguma outra forma de autenticação) e o define no contexto da sessão do banco de dados.
O Segredo: Como o sqlx ou database/sql utiliza um pool de conexões, você NUNCA deve definir a variável de sessão diretamente na conexão do pool. Você deve fazer isso dentro de uma transação ou ao obter uma conexão emprestada do pool.
api/middleware/tenant_contexto.go
package middleware
import (
"context"
"net/http"
)
// Chave para armazenar o tenant_id no Context do Go.
type ContextKey string
const TenantIDKey ContextKey = "tenantID"
// TenantContext é um middleware que simula a obtenção do TenantID
// (Na vida real, isso viria de um token JWT, por exemplo)
func TenantContext(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// --- SIMULAÇÃO DE AUTENTICAÇÃO E EXTRAÇÃO DE TENANT_ID ---
// Exemplo: Extrair o tenantID do header "X-Tenant-ID"
tenantID := r.Header.Get("X-Tenant-ID")
if tenantID == "" {
http.Error(w, "Tenant ID not provided", http.StatusUnauthorized)
return
}
// Adiciona o tenantID ao contexto da requisição
ctx := context.WithValue(r.Context(), TenantIDKey, tenantID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Passo 3: O Repositório na Camada de Infraestrutura (sqlx)
Esta é a camada que interage com o banco de dados e deve garantir que a variável de sessão RLS seja definida para cada consulta.
infra/repository/order_repository.go
package repository
import (
"context"
"fmt"
"github.com/jmoiron/sqlx"
"suaapp/domain" // Assuma que Order é um struct definido aqui
)
// DBAdapter implementa a porta do repositório de domínio.
type DBAdapter struct {
DB *sqlx.DB // O pool de conexões do sqlx
}
// ensureRLSContext define a variável de sessão do PostgreSQL para o RLS.
// Isso deve ser executado no MESMO objeto de conexão que fará a consulta.
func (r *DBAdapter) ensureRLSContext(ctx context.Context) (*sqlx.Conn, string, error) {
// 1. Obter o TenantID do contexto da requisição Go
tenantID, ok := ctx.Value("tenantID").(string)
if !ok || tenantID == "" {
return nil, "", fmt.Errorf("tenant ID not found in context")
}
// 2. Emprestar uma conexão do pool (crucial para o isolamento)
conn, err := r.DB.Connx(ctx)
if err != nil {
return nil, "", fmt.Errorf("could not get connection from pool: %w", err)
}
// 3. Definir a variável de sessão RLS na conexão EMPRESTADA
// O comando SET só afeta esta conexão específica e sua sessão atual.
setQuery := fmt.Sprintf("SET app.current_tenant_id = '%s'", tenantID)
_, err = conn.ExecContext(ctx, setQuery)
if err != nil {
conn.Close() // Fecha a conexão em caso de erro
return nil, "", fmt.Errorf("failed to set RLS context: %w", err)
}
return conn, tenantID, nil
}
// FindOrderByID implementa o método de busca, agora protegido por RLS.
func (r *DBAdapter) FindOrderByID(ctx context.Context, orderID int64) (*domain.Order, error) {
conn, tenantID, err := r.ensureRLSContext(ctx)
if err != nil {
return nil, err
}
defer conn.Close() // IMPORTANTE: Devolver a conexão para o pool
// A consulta SQL é simples, sem WHERE tenant_id. O RLS faz o trabalho.
query := `SELECT id, tenant_id, status, created_at FROM orders WHERE id = $1`
var order domain.Order
// Utilizamos a conexão específica
err = conn.GetContext(ctx, &order, query, orderID)
if err != nil {
return nil, fmt.Errorf("query failed for tenant %s: %w", tenantID, err)
}
// Se a ordem for encontrada, o RLS garante que ela pertence ao tenantID
// Se a ordem não for encontrada, é porque o RLS a filtrou (ou ela não existe)
return &order, nil
}
🚀 Como a Aplicação Trata N Usuários (Pool de Conexões)
Este é o ponto crucial de como o RLS trabalha com o sqlx (ou qualquer pool de conexões) em um ambiente de alta concorrência:
-
Isolamento de Sessão por Conexão: O PostgreSQL garante que o
SET app.current_tenant_id = '...'afete apenas a sessão atual (ou seja, aquela conexão que foi emprestada do pool). -
O Pool é Compartilhado: O pool (
r.DB) contém várias conexões abertas e prontas. -
Fluxo Transacional por Requisição:
-
Usuário A (Tenant 1) faz uma requisição.
-
O Repositório (
FindOrderByID) chamar.DB.Connx(ctx)e pega a Conexão X. -
Ele executa
SET app.current_tenant_id = '1'na Conexão X. -
Ele executa o
SELECTna Conexão X. O RLS do Postgres aplica o filtroWHERE tenant_id = 1. -
Ele chama
defer conn.Close(), que DEVOLVE a Conexão X para o pool. (O PostgreSQL automaticamente limpa a variável de sessãoapp.current_tenant_idou ela é sobrescrita na próxima utilização, dependendo da configuração do seu pool e do driver, mas é prática segura definir antes de cada uso). -
Usuário B (Tenant 2) faz uma requisição concorrente.
-
O Repositório chama
r.DB.Connx(ctx)e pega a Conexão X (ou uma nova, Y). -
Ele executa
SET app.current_tenant_id = '2'na Conexão X. -
Ele executa o
SELECTna Conexão X. O RLS do Postgres aplica o filtroWHERE tenant_id = 2.
-
Ao usar r.DB.Connx(ctx) e defer conn.Close(), você garante que cada requisição (cada usuário/tenant) tem seu próprio contexto de sessão RLS na conexão que está usando, sem vazar esse contexto para outras requisições concorrentes que podem estar usando outras conexões do pool.
Se você tentar usar o pool (r.DB) diretamente para executar a consulta sem antes obter uma conexão específica (r.DB.Connx(ctx)) e definir o RLS nela, você terá um problema de isolamento de sessão e segurança.
❌ Por Que Não Usar o Pool Direto para RLS
-
Vazamento de Contexto (O Problema de Segurança): Se você simplesmente executar o
SET app.current_tenant_id = '1'usando or.DB(o pool), esse comando pode ser executado em uma conexão aleatória do pool. Essa conexão, com o RLS definido para o Tenant 1, pode ser imediatamente reutilizada por uma requisição do Tenant 2 antes que o Tenant 2 tenha chance de executar seu próprioSET. O Tenant 2 então acidentalmente veria os dados do Tenant 1. -
Transitoriedade do SET: O
SETé uma instrução de sessão. Ele só se aplica à conexão onde foi executado. Osqlx.DB(o pool) não garante qual conexão será usada para oSETe qual será usada para oSELECT.
A solução correta é sempre:
-
Obter uma conexão (
conn, err := r.DB.Connx(ctx)). -
Definir o RLS (
SET ...) nessa conexão. -
Executar todas as operações na mesma conexão (
conn.GetContext,conn.ExecContext). -
Devolver a conexão ao pool (
defer conn.Close()).
Este padrão é a maneira mais segura e performática de implementar RLS com um pool de conexões em Go.
🏗️ Camada de Serviço (Usecase) com Isolamento
Na arquitetura hexagonal, a Camada de Serviço (ou Usecase) é o núcleo que orquestra a lógica de negócios, independente de como os dados são armazenados (Infraestrutura) ou como são acessados (Adaptação/REST).
O RLS e a gestão do tenant_id são responsabilidades que terminam na Camada de Infraestrutura (o Repositório). O Usecase não deve saber que existe RLS. Sua única responsabilidade é garantir que o tenant_id (que ele recebe do Adaptador REST) seja passado para o Repositório, através do context.Context de Go.
1. Definição da Porta de Domínio
O Usecase precisa de uma porta (interface) para interagir com o Repositório.
domain/ports.go
package domain
import (
"context"
)
// OrderRepository é a Porta de Saída (Outbound Port)
type OrderRepository interface {
FindByID(ctx context.Context, orderID int64) (*Order, error)
Create(ctx context.Context, order *Order) error
// ... outros métodos ...
}
// OrderService é a Porta de Entrada (Inbound Port)
type OrderService interface {
GetOrder(ctx context.Context, orderID int64) (*Order, error)
// ... outros métodos ...
}
2. Implementação do Usecase
O Usecase recebe o OrderRepository via injeção de dependência e apenas lida com a lógica de negócios, sem passar o tenant_id explicitamente como argumento nos métodos. O tenant_id está encapsulado no context.Context.
core/usecase/order_service.go
package usecase
import (
"context"
"suaapp/domain"
)
// OrderService implementa a porta de entrada.
type OrderService struct {
Repo domain.OrderRepository // Injeção da dependência (adaptador de infraestrutura)
}
// GetOrder executa a lógica de negócios para buscar um pedido.
func (s *OrderService) GetOrder(ctx context.Context, orderID int64) (*domain.Order, error) {
// 1. Lógica de Negócios (ex: validação, permissão, etc.)
// O Usecase não precisa do tenant_id, mas precisa do Context que o contém.
// 2. Chamada ao Repositório
// O Context é passado, garantindo que o Repositório tenha acesso ao tenant_id
// para configurar o RLS.
order, err := s.Repo.FindByID(ctx, orderID)
if err != nil {
// O erro pode ser 'Not Found' (se o RLS filtrou) ou um erro de DB.
return nil, err
}
// 3. Mais Lógica (ex: calcular desconto, enriquecer dados, etc.)
return order, nil
}
Resumo do Fluxo com RLS e Arquitetura Hexagonal
| Camada | Responsabilidade | Ação com tenant_id |
|---|---|---|
| Adaptador (REST/Chi) | Capturar entrada e Autenticação. | 1. Extrai tenant_id do JWT/Header. 2. Insere tenant_id no context.Context de Go. |
| Usecase (Core/Service) | Lógica de Negócios. | Propaga o context.Context (com o tenant_id embutido) para o Repositório. Não se preocupa com o tenant_id. |
| Infraestrutura (Repository/sqlx) | Acesso ao Banco de Dados. | 1. Extrai tenant_id do context.Context. 2. Empresta uma conexão (r.DB.Connx). 3. Executa SET app.current_tenant_id = '...'. 4. Executa a consulta SQL simples. 5. Devolve a conexão. |
| PostgreSQL (RLS) | Garantia de Isolamento. | Aplica o filtro WHERE tenant_id = current_setting('...') em toda consulta do Repositório. |
| Essa separação de responsabilidades mantém o seu Core (Usecase) limpo, testável e independente dos detalhes de persistência e segurança, que são movidos para a camada de Infraestrutura, onde o RLS é gerenciado com segurança usando a conexão emprestada do pool. |