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.

⚙️ 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).

SET app.current_tenant_id = 'O_ID_DO_TENANT_ATUAL';
importante entender sobre a variável

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.

ainda sobre a variável

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.

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.

🔒 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.

SELECT * FROM nome_da_tabela WHERE tenant_id = current_setting('app.current_tenant_id')::bigint;
importante

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.

    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:

  1. O tenant só pode acessar/selecionar as linhas dele para atualizar/deletar (cláusula USING).

  2. O tenant não pode alterar o tenant_id de uma linha para o ID de outro tenant (cláusula WITH CHECK).

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.

importante

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:

  1. 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).

  2. O Pool é Compartilhado: O pool (r.DB) contém várias conexões abertas e prontas.

  3. Fluxo Transacional por Requisição:

    • Usuário A (Tenant 1) faz uma requisição.

    • O Repositório (FindOrderByID) chama r.DB.Connx(ctx) e pega a Conexão X.

    • Ele executa SET app.current_tenant_id = '1' na Conexão X.

    • Ele executa o SELECT na Conexão X. O RLS do Postgres aplica o filtro WHERE 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ão app.current_tenant_id ou 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 SELECT na Conexão X. O RLS do Postgres aplica o filtro WHERE 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.

importante

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

  1. Vazamento de Contexto (O Problema de Segurança): Se você simplesmente executar o SET app.current_tenant_id = '1' usando o r.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óprio SET. O Tenant 2 então acidentalmente veria os dados do Tenant 1.

  2. Transitoriedade do SET: O SET é uma instrução de sessão. Ele só se aplica à conexão onde foi executado. O sqlx.DB (o pool) não garante qual conexão será usada para o SET e qual será usada para o SELECT.

A solução correta é sempre:

  1. Obter uma conexão (conn, err := r.DB.Connx(ctx)).

  2. Definir o RLS (SET ...) nessa conexão.

  3. Executar todas as operações na mesma conexão (conn.GetContext, conn.ExecContext).

  4. 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.

Próximo

03 - Abordagem para Administradores (ignorando RLS)