Multitenancy com Row Level Security no PostgreSQL
1. Introdução à Multitenancy e RLS no PostgreSQL
A arquitetura multi-tenant é um padrão de design onde uma única instância de aplicação atende múltiplos clientes (tenants), mantendo seus dados isolados. Existem três abordagens principais: database por tenant (isolamento máximo, mas alto custo operacional), schema por tenant (isolamento moderado) e tabela compartilhada (eficiência de recursos, mas requer isolamento lógico).
O Row Level Security (RLS) no PostgreSQL é um recurso que permite definir políticas de acesso a nível de linha, controlando quais registros cada usuário ou sessão pode visualizar ou modificar. Diferente de filtros aplicados na camada de aplicação, o RLS opera diretamente no motor do banco, garantindo que mesmo consultas acidentais ou maliciosas não exponham dados de outros tenants.
As vantagens do RLS para multitenancy incluem: eliminação do risco de vazamento de dados por erros na aplicação, redução da complexidade do código (já que as regras de isolamento ficam no banco) e facilidade para adicionar novos tenants sem alterações estruturais.
2. Configuração Inicial do Ambiente Multi-tenant
Vamos criar um esquema básico para uma aplicação SaaS de gerenciamento de tarefas:
-- Criação da tabela compartilhada com tenant_id
CREATE TABLE tasks (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW(),
assigned_to TEXT
);
-- Criação da tabela de usuários com referência ao tenant
CREATE TABLE users (
id SERIAL PRIMARY KEY,
tenant_id INTEGER NOT NULL,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
role TEXT DEFAULT 'member'
);
-- Criação da tabela de tenants
CREATE TABLE tenants (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
plan TEXT DEFAULT 'basic',
active BOOLEAN DEFAULT true
);
Agora, habilitamos o RLS e criamos as políticas:
-- Habilitar RLS nas tabelas
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- Criar política para tasks
CREATE POLICY tenant_isolation_policy ON tasks
USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);
-- Criar política para users
CREATE POLICY tenant_isolation_policy ON users
USING (tenant_id = current_setting('app.current_tenant_id')::INTEGER);
3. Estratégias de Identificação e Contexto do Tenant
Para definir o tenant ativo em cada sessão, utilizamos variáveis de sessão personalizadas. A aplicação deve configurar essa variável imediatamente após estabelecer a conexão:
-- Função para definir o tenant ativo
CREATE OR REPLACE FUNCTION set_current_tenant(tenant_id INTEGER)
RETURNS VOID AS $$
BEGIN
PERFORM set_config('app.current_tenant_id', tenant_id::TEXT, false);
END;
$$ LANGUAGE plpgsql;
-- Função auxiliar para recuperar o tenant atual
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS INTEGER AS $$
BEGIN
RETURN current_setting('app.current_tenant_id')::INTEGER;
EXCEPTION
WHEN OTHERS THEN
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
No gerenciamento de pools de conexão (como PgBouncer), é importante usar o modo session-level pooling, pois o RLS depende de variáveis de sessão. Alternativamente, pode-se usar schemas diferentes por tenant com search_path configurado dinamicamente.
4. Políticas de Segurança por Tenant: Criação e Gerenciamento
Políticas completas para todas as operações:
-- Política para SELECT (já criada anteriormente)
CREATE POLICY select_policy ON tasks FOR SELECT
USING (tenant_id = current_tenant_id());
-- Política para INSERT
CREATE POLICY insert_policy ON tasks FOR INSERT
WITH CHECK (tenant_id = current_tenant_id());
-- Política para UPDATE
CREATE POLICY update_policy ON tasks FOR UPDATE
USING (tenant_id = current_tenant_id())
WITH CHECK (tenant_id = current_tenant_id());
-- Política para DELETE
CREATE POLICY delete_policy ON tasks FOR DELETE
USING (tenant_id = current_tenant_id());
Para cenários onde alguns usuários (como admins do sistema) precisam acessar múltiplos tenants:
-- Política que permite acesso total para admins
CREATE POLICY admin_policy ON tasks FOR ALL
USING (current_setting('app.user_role') = 'super_admin')
WITH CHECK (true);
-- Política combinada (admins veem tudo, usuários comuns veem apenas seu tenant)
CREATE POLICY combined_policy ON tasks FOR SELECT
USING (
tenant_id = current_tenant_id()
OR current_setting('app.user_role') = 'super_admin'
);
5. Performance e Indexação em Cenários Multi-tenant
Índices são cruciais para manter a performance com RLS:
-- Índice composto (tenant_id primeiro) para consultas frequentes
CREATE INDEX idx_tasks_tenant_status ON tasks (tenant_id, status);
-- Índice parcial para consultas específicas de um tenant
CREATE INDEX idx_tasks_tenant_pending ON tasks (tenant_id)
WHERE status = 'pending';
-- Índice para busca por usuário dentro do tenant
CREATE INDEX idx_tasks_tenant_assigned ON tasks (tenant_id, assigned_to);
Análise de performance com EXPLAIN ANALYZE:
-- Configurar tenant
SELECT set_current_tenant(42);
-- Analisar consulta com RLS
EXPLAIN ANALYZE
SELECT * FROM tasks WHERE status = 'pending';
O plano de execução mostrará um Filter adicional aplicado pelo RLS. Em tabelas com milhões de registros e muitos tenants, o índice em tenant_id é essencial para evitar scans completos.
Particionamento por tenant pode ser combinado com RLS:
-- Tabela particionada por tenant (range)
CREATE TABLE tasks_partitioned (
id SERIAL,
tenant_id INTEGER NOT NULL,
title TEXT
) PARTITION BY RANGE (tenant_id);
CREATE TABLE tasks_tenant_1_1000 PARTITION OF tasks_partitioned
FOR VALUES FROM (1) TO (1000);
-- Habilitar RLS na tabela particionada
ALTER TABLE tasks_partitioned ENABLE ROW LEVEL SECURITY;
6. Migração e Manutenção de Dados com RLS
Migração de um sistema sem RLS para RLS:
-- 1. Adicionar coluna tenant_id em tabelas existentes
ALTER TABLE tasks ADD COLUMN tenant_id INTEGER;
-- 2. Atualizar registros existentes (mapear usuários para tenants)
UPDATE tasks t
SET tenant_id = u.tenant_id
FROM users u
WHERE t.assigned_to = u.username;
-- 3. Tornar tenant_id NOT NULL
ALTER TABLE tasks ALTER COLUMN tenant_id SET NOT NULL;
-- 4. Habilitar RLS e criar políticas
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
CREATE POLICY migration_policy ON tasks
USING (tenant_id = current_tenant_id());
Para atualizar políticas sem interrupção:
BEGIN;
-- Remover política antiga
DROP POLICY IF EXISTS old_policy ON tasks;
-- Criar nova política
CREATE POLICY new_policy ON tasks
USING (tenant_id = current_tenant_id() OR is_auditor());
COMMIT;
Backup seletivo por tenant:
-- Exportar dados de um tenant específico
COPY (
SELECT * FROM tasks
WHERE tenant_id = 42
) TO '/tmp/tenant_42_backup.csv' CSV HEADER;
7. Monitoramento, Auditoria e Troubleshooting
Para auditar tentativas de acesso entre tenants:
-- Criar tabela de log de auditoria
CREATE TABLE access_log (
id SERIAL PRIMARY KEY,
user_id INTEGER,
attempted_tenant_id INTEGER,
actual_tenant_id INTEGER,
action TEXT,
timestamp TIMESTAMP DEFAULT NOW()
);
-- Função trigger para log
CREATE OR REPLACE FUNCTION log_access_attempt()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.tenant_id != current_tenant_id() THEN
INSERT INTO access_log (user_id, attempted_tenant_id, actual_tenant_id, action)
VALUES (current_setting('app.user_id')::INTEGER, NEW.tenant_id, current_tenant_id(), TG_OP);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Verificação de políticas ativas:
-- Listar todas as políticas do banco
SELECT * FROM pg_policies
WHERE tablename IN ('tasks', 'users');
-- Verificar se RLS está habilitado
SELECT relname, relrowsecurity
FROM pg_class
WHERE relname IN ('tasks', 'users');
8. Considerações Finais e Casos de Uso Avançados
O RLS não é uma bala de prata. Suas limitações incluem: overhead em operações bulk (INSERT/UPDATE/DELETE em lote), dificuldade com subconsultas complexas em políticas (que podem degradar performance) e incompatibilidade com algumas funcionalidades (como replicação lógica em versões antigas).
Para alta concorrência, combine RLS com cache por tenant (Redis) e sharding horizontal quando um único tenant atingir volumes massivos. Em cenários de SaaS com milhares de tenants pequenos, o RLS com tabela compartilhada é ideal. Para tenants que exigem isolamento físico (dados sensíveis), considere database por tenant.
Exemplo completo de aplicação em produção:
-- Configuração inicial da sessão
SELECT set_current_tenant(42);
SELECT set_config('app.user_id', '123', false);
SELECT set_config('app.user_role', 'member', false);
-- Operações seguras (RLS garante isolamento)
INSERT INTO tasks (tenant_id, title, assigned_to)
VALUES (42, 'Implementar relatório mensal', 'joao@exemplo.com');
SELECT * FROM tasks WHERE status = 'pending';
UPDATE tasks SET status = 'completed'
WHERE id = 5 AND tenant_id = 42;
DELETE FROM tasks WHERE id = 10;
O RLS no PostgreSQL oferece uma camada de segurança robusta e declarativa, reduzindo significativamente o risco de vazamento de dados em arquiteturas multi-tenant. Quando combinado com boas práticas de indexação, monitoramento e design de aplicação, torna-se uma solução elegante e escalável para SaaS.
Referências
-
Documentação Oficial do PostgreSQL: Row Security Policies — Documentação completa sobre criação e gerenciamento de políticas de segurança a nível de linha no PostgreSQL.
-
PostgreSQL Wiki: Row Level Security — Guia prático com exemplos adicionais e casos de uso avançados para RLS.
-
Citus Blog: Multi-tenant SaaS with PostgreSQL Row Level Security — Artigo técnico da equipe Citus (Microsoft) sobre implementação de multitenancy com RLS em escala.
-
Crunchy Data: Implementing Multi-Tenancy with PostgreSQL — Tutorial completo abordando estratégias de multitenancy, incluindo RLS, particionamento e performance.
-
Cybertec PostgreSQL: Row Level Security Best Practices — Artigo com dicas práticas e armadilhas comuns ao implementar RLS em produção.
-
PostgreSQL Tutorial: Row Level Security Explained — Tutorial introdutório com exemplos passo a passo para iniciantes em RLS.