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