Relacionamento Digitalcom o Cidadão
Parte IV — Módulos
Parte IV — MódulosCapítulo 30

Capítulo 30 — Gestão Multi-Tenant

Este capítulo detalha a arquitetura de gestão multi-tenant da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como múltiplos órgãos públicos (tenants) compartilham a mesma infraestrutura e codebase sem co…

30.1 Objetivo do Capítulo

Este capítulo detalha a arquitetura de gestão multi-tenant da Plataforma de Relacionamento Digital com o Cidadão, descrevendo como múltiplos órgãos públicos (tenants) compartilham a mesma infraestrutura e codebase sem comprometer o isolamento de dados, o cumprimento de conformidades específicas ou a personalização de cada organização.

O modelo multi-tenant é uma das características diferenciadores da plataforma. Permite que a PRODEMGE e seus parceiros escalem a solução para dezenas de órgãos (secretarias, prefeituras, autarquias) mantendo um custo operacional controlado, enquanto cada tenant experimenta a plataforma como se fosse uma instância dedicada sob seu controle de governança.

Este capítulo descreve o modelo de segregação lógica, os mecanismos de isolamento de contexto, a personalização por tenant, a governança de dados, a conformidade com LGPD tenant-aware e as decisões arquiteturais que permitem que o sistema escale de um para cem tenants sem degradação de segurança ou desempenho.


30.2 Contexto e Motivação

30.2.1 Cenário Sem Multi-Tenant

Historicamente, a oportunidade de negócio foi concebida com cada órgão operando em instância separada: uma secretaria = um banco de dados = um cluster Kubernetes = uma fatura de infraestrutura. Esse modelo oferecia máxima segurança por isolamento físico, mas criava ineficiências operacionais:

  • duplicação de código e configuração em múltiplas instâncias;
  • custo infraestrutural proporcional ao número de tenants (não escalável);
  • impossibilidade de aproveitar economia de escala;
  • dificuldade de orquestração de atualizações (cada instância atualiza em seu ritmo).

30.2.2 Oportunidade Multi-Tenant

A opção por multi-tenant com segregação lógica reconhece que o valor estratégico está em escala: uma plataforma que atende a 50 órgãos simultaneamente com custo controlado é mais viável economicamente do que 50 instâncias isoladas.

A segregação lógica garante que o isolamento é tão robusto quanto o isolamento físico — do ponto de vista de segurança e dados — mas permite compartilhamento de infraestrutura, versionamento unificado, operação centralizada e custos proporcionais ao valor gerado, não ao número de tenants.

30.2.3 Confirmação Regulatória

Os esclarecimentos da PRODEMGE (Montreal, Valtech) confirmam que instâncias lógicas segregadas atendem ao requisito de multi-tenancy do edital. Não é exigida instância física separada por tenant.


30.3 Definições Fundamentais

30.3.1 O que é um Tenant

Um tenant é uma unidade organizacional isolada logicamente que representa uma organização (órgão público, secretaria, autarquia) com:

  • identidade única e imutável;
  • namespace próprio para dados;
  • configurações específicas (políticas de senha, sessão, auditoria);
  • usuários e perfis próprios;
  • permissões de acesso exclusivas;
  • isolamento completo de dados de outros tenants.

Um tenant é a unidade primária de segregação. Toda requisição na plataforma passa pelo Tenant Context — o identificador do tenant resolvido a partir da identidade do usuário autenticado.

30.3.2 Tenant Context

O Tenant Context é a dimensão de segurança que responde a pergunta: "Em qual tenant estou operando nesta requisição?".

O Tenant Context é derivado da identidade autenticada no Capítulo 28. O usuário autenticado possui um ou mais vínculos com tenants (associação de usuário, tenant, perfil, unidade). A escolha de qual tenant opera é feita durante a autenticação ou alternada durante a sessão (com step-up quando requerido).

Uma vez estabelecido o Tenant Context, toda operação subsequente (leitura, escrita, busca, integração) é constrangida por esse contexto — nenhuma operação atravessa a fronteira de tenant.

30.3.3 Segregação Lógica vs. Física

Segregação física: cada tenant em sua própria instância, banco de dados separado, cluster Kubernetes separado, rede separada. Máximo isolamento, custo máximo.

Segregação lógica: todos os tenants em mesma instância, mesma base de dados, mesma rede, mas dados identificados por tenant e isolados em acesso. Custo controlado, isolamento através de controle de acesso.

A plataforma adota segregação lógica. O isolamento é garantido não pela infraestrutura, mas pelos seguintes mecanismos:

  1. Identificação de todos os dados com tenant_id;
  2. Filtros obrigatórios em toda query por tenant_id;
  3. Verificação de Tenant Context antes de toda operação;
  4. Isolamento de sessões, tokens e cache por tenant;
  5. Encriptação de dados sensíveis (independente de tenant);
  6. Auditoria que registra tenant em cada evento.

30.4 Modelo Arquitetural de Multi-Tenant

30.4.1 Camadas de Isolamento

A segregação multi-tenant é implementada em múltiplas camadas:

Camada de Identidade (Cap. 28)
  │
  └─→ Tenant Context resolvido a partir do usuário autenticado
      │
      ├─ Autorização (Cap. 29): permissões filtradas por tenant
      │
      ├─ Aplicação (Microsserviços)
      │  │
      │  ├─ Interceptor: Tenant Context injected em todo request
      │  ├─ Validação: tenant_id verificado em todo resource ID
      │  └─ Query: filtro por tenant_id adicionado automaticamente
      │
      ├─ Persistência (Banco de Dados)
      │  │
      │  ├─ Schema compartilhado, tabelas com coluna tenant_id
      │  ├─ Índices multi-coluna (tenant_id, entity_id, ...)
      │  ├─ Triggers que garantem tenant_id em INSERT/UPDATE
      │  └─ Row-level security (quando disponível no BD)
      │
      ├─ Cache Distribuído (Redis)
      │  │
      │  ├─ Chave de cache: "{tenant_id}:{entity_type}:{entity_id}"
      │  ├─ Invalidação por tenant quando políticas mudam
      │  └─ TTL por tenant_id configurável
      │
      ├─ Busca e Indexação (Elasticsearch)
      │  │
      │  └─ Índices segregados por tenant (one index per tenant)
      │     ou índice compartilhado com field tenant_id e query filter
      │
      ├─ Fila de Mensagens (RabbitMQ)
      │  │
      │  └─ Topics: "{tenant_id}.{event_type}"
      │     ou exchange + routing_key que inclui tenant_id
      │
      └─ Armazenamento de Objetos (S3/blob storage)
         │
         └─ Prefixo de chave: "{tenant_id}/{document_uuid}/{file}"
            ou bucket por tenant quando necessário

Cada camada implementa sua própria verificação de Tenant Context. Nenhuma camada confia na anterior — todas verificam independentemente.

30.4.2 Identificação de Dados

Toda entidade que contém ou referencia dados de negócio inclui a coluna/campo tenant_id:

User
  id                (UUID)
  tenantId          (UUID — qual tenant este usuário pertence)
  email
  ...

Request (Solicitação)
  id                (UUID)
  tenantId          (UUID — qual tenant esta solicitação pertence)
  citizenId         (UUID — cidadão que abriu a solicitação)
  status
  ...

Document (Documento)
  id                (UUID)
  tenantId          (UUID)
  requestId         (UUID)
  filePath          (string)
  ...

Interaction (Atendimento)
  id                (UUID)
  tenantId          (UUID)
  citizenId         (UUID)
  channel           (EMAIL, SMS, WHATSAPP)
  ...

tenantId é imutável após criação. Não existe "transferência de entidade entre tenants" — isso seria violação de isolamento.


30.5 Ciclo de Vida de um Tenant

30.5.1 Provisioning (Criação)

Quando um novo órgão (tenant) é incorporado à plataforma:

  1. Pedido de Onboarding: órgão solicita integração (descrito no Capítulo 48);
  2. Validação: PRODEMGE valida viabilidade legal, conformidade, volume estimado;
  3. Criação do Tenant:
    • Gera UUID único para identificação permanente;
    • Registra dados meta (nome, CNPJ, órgão responsável, contato);
    • Cria namespace no IAM;
    • Inicializa configurações padrão (políticas de senha, timeout de sessão, retenção de dados);
  4. Ambiente Inicial:
    • Cria usuário administrador inicial (Break Glass Account, Capítulo 29);
    • Configura domínio/URL para acesso ao portal (ex: educacao.meu.gov.br);
    • Ativa integração com Gov.br se requerido;
  5. Ativação: tenant passa para status ACTIVE e começa aceitar usuários e dados.

30.5.2 Operação (Uso Normal)

Durante a operação:

  • Usuários do tenant autenticam e trabalham;
  • Dados são criados, modificados, consultados — sempre constrangidos por tenant_id;
  • Eventos são registrados com tenant_id na trilha de auditoria;
  • Backups incluem tenant_id na partição de backup;
  • Relatórios e analytics respeitam fronteira de tenant;
  • Integrações com sistemas externos (Gov.br, SEI!MG) passam por camada de adaptação que valida tenant.

30.5.3 Suspensão (Temporária)

Tenant pode ser suspenso por:

  • inadimplência (modelo de negócio);
  • investigação de segurança;
  • requisição do órgão;
  • violação de conformidade.

Efeito da suspensão:

  • usuários não conseguem autenticar;
  • APIs rejeitam requisições;
  • operações em background (atualizações, sincronizações) param;
  • dados permanecem em banco (preservados para auditoria);
  • backups continuam sendo feitos.

Status: SUSPENDED. Pode ser revertido para ACTIVE.

30.5.4 Ofboarding (Exclusão)

Quando um tenant é desativado permanentemente:

  1. Notificação: órgão recebe aviso prévio (em conformidade com contrato);
  2. Período de Grace: tempo configurável (ex: 60 dias) para exportação de dados;
  3. Exportação: órgão pode solicitar dump completo de seus dados;
  4. Purga de Dados:
    • Dados pessoais de cidadãos: anonimizados ou deletados conforme LGPD;
    • Dados operacionais (solicitações, documentos): podem ser arquivados ou deletados conforme política de retenção;
    • Dados de auditoria: preservados por período legal (LGPD art. 16 — máximo 6 meses por padrão, extensível);
  5. Status Final: DELETED. Tenant ID preservado em registros históricos, mas inacessível para operações.

30.5.5 Estados do Tenant

                       ┌─────────────┐
                       │   PENDING   │
                       │ (provisioning)
                       └──────┬──────┘
                              │
                       ┌──────▼──────┐
                       │   ACTIVE    │◄──────┐
                       │  (operando)  │      │
                       └──┬───────┬──┘      │
                          │       │       reativation
                    suspend│       │        │
                          │   activation   │
                          ▼       ▼        │
                       ┌──────────────┐    │
                       │  SUSPENDED   │────┘
                       └──────┬───────┘
                              │
                        offboarding
                              │
                       ┌──────▼───────┐
                       │   DELETED    │
                       │ (archived)   │
                       └──────────────┘

Transições permitidas:

  • PENDING → ACTIVE (ativação bem-sucedida)
  • PENDING → DELETED (falha no onboarding)
  • ACTIVE → SUSPENDED (suspensão administrativa)
  • SUSPENDED → ACTIVE (reativação)
  • ACTIVE ou SUSPENDED → DELETED (ofboarding)

30.6 Isolamento de Contexto na Aplicação

30.6.1 Tenant Context Injection

Toda requisição HTTP chega ao backend com credenciais de um usuário autenticado. O primeiro passo após autenticação é derivar o Tenant Context:

HTTP Request
    │
    ├─ Headers: Authorization: Bearer <token>
    │
    ▼
AuthenticationFilter
    │
    ├─ Desserializa token
    ├─ Valida assinatura
    ├─ Extrai userId
    │
    ▼
TenantContextFilter
    │
    ├─ Busca userId no IAM
    ├─ Retorna lista de tenants associados
    ├─ Se um único tenant: usa automaticamente
    ├─ Se múltiplos tenants: busca seleção no header (X-Tenant-Id)
    ├─ Valida que o tenant selecionado é válido para o usuário
    │
    ▼
ThreadLocal<TenantContext>
    │
    ├─ Armazena tenant_id na thread-local
    ├─ Disponível para toda lógica de negócio
    └─ Acessível via TenantContext.getCurrent()

O TenantContext é thread-local (ou request-scoped em frameworks como Spring). Isso garante que:

  • Cada requisição tem exatamente um tenant ativo;
  • Nenhuma requisição pode "vazar" tenant de outro usuário;
  • Múltiplas requisições paralelas de diferentes tenants não se interferem;
  • Testes unitários podem usar um tenant de teste sem afetar outros testes.

30.6.2 Validação de Tenant em Operações

Toda operação que toca um recurso identificado valida tenant:

// Exemplo: consultar solicitação
public Request getRequest(UUID requestId) {
    UUID currentTenant = TenantContext.getCurrent().getTenantId();
    
    // Busca a solicitação
    Request request = requestRepository.findById(requestId);
    
    // Valida que a solicitação pertence ao tenant atual
    if (!request.getTenantId().equals(currentTenant)) {
        throw new ForbiddenException(
            "Solicitação não pertence ao seu tenant"
        );
    }
    
    return request;
}

Essa validação ocorre em toda operação:

  • Leitura: antes de retornar o dado;
  • Escrita: antes de permitir modificação;
  • Exclusão: antes de permitir deleção;
  • Busca: em cada resultado retornado;
  • Integração: ao enviar/receber dados de sistemas externos.

Nenhuma operação confia no filtro da query sozinha — a validação é dupla:

  1. Filtro na query (defesa em profundidade);
  2. Verificação do resultado contra tenant (segurança explícita).

30.6.3 Propagação para Serviços Internos

A plataforma é baseada em microsserviços. Quando um serviço chama outro serviço internamente, o Tenant Context é propagado:

RequestService
    │
    ├─ currentTenant = TenantContext.getCurrent()
    │
    └─▶ documentService.uploadDocument(file)
          │
          ├─ Tenant Context propagado via header (X-Tenant-Id: <uuid>)
          │
          └─▶ DocumentService interceptor
              ├─ Extrai X-Tenant-Id do header
              ├─ Valida que matches header Authorization
              ├─ Estabelece novo TenantContext localmente
              └─ Processa upload com tenant context ativo

O header X-Tenant-Id é interno (comunicação entre serviços em private network). Ele não é aceito de requisições do cliente externo — o tenant é sempre derivado da autenticação.

Isso previne que um cliente tente "forjar" acesso a outro tenant passando um header fake.


30.7 Isolamento de Dados na Persistência

30.7.1 Schema do Banco de Dados

O banco de dados compartilhado usa schema único com tenant_id em todas as tabelas:

-- Tabela de usuários
CREATE TABLE public.users (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    email             VARCHAR(255) NOT NULL,
    name              VARCHAR(255),
    password_hash     VARCHAR(255),
    status            VARCHAR(50),
    created_at        TIMESTAMP,
    updated_at        TIMESTAMP,
    
    -- Índice composto para queries por tenant
    UNIQUE(tenant_id, email),
    INDEX idx_users_tenant (tenant_id),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- Tabela de solicitações
CREATE TABLE public.requests (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    citizen_id        UUID NOT NULL,
    service_id        UUID NOT NULL,
    status            VARCHAR(50),
    created_at        TIMESTAMP,
    updated_at        TIMESTAMP,
    
    INDEX idx_requests_tenant (tenant_id),
    INDEX idx_requests_citizen_tenant (citizen_id, tenant_id),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id)
);

-- Tabela de documentos
CREATE TABLE public.documents (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    request_id        UUID NOT NULL,
    file_path         VARCHAR(1024),
    file_size         BIGINT,
    content_type      VARCHAR(100),
    created_at        TIMESTAMP,
    
    INDEX idx_documents_tenant (tenant_id),
    INDEX idx_documents_request_tenant (request_id, tenant_id),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (request_id) REFERENCES requests(id)
);

Princípios:

  1. tenant_id em toda tabela de negócio;
  2. Índices sempre incluem tenant_id (consultas são sempre filtradas por tenant);
  3. Constraints garantem que chaves estrangeiras respeitem tenant (ex: um usuário de tenant A não pode ter solicitação em tenant B);
  4. Chaves únicas combinam tenant (ex: email é único por tenant, não globalmente).

30.7.2 Queries Seguras

Toda query produzida pelos microsserviços é interceptada e validada:

// Specification Pattern com verificação de tenant
public class RequestSpecifications {
    
    public static Specification<Request> forTenant(UUID tenantId) {
        return (root, query, cb) -> 
            cb.equal(root.get("tenantId"), tenantId);
    }
    
    public static Specification<Request> withStatus(String status) {
        return (root, query, cb) -> 
            cb.equal(root.get("status"), status);
    }
    
    // Uso na aplicação
    UUID currentTenant = TenantContext.getCurrent().getTenantId();
    Specification<Request> spec = 
        Specification
            .where(RequestSpecifications.forTenant(currentTenant))
            .and(RequestSpecifications.withStatus("OPEN"));
    
    List<Request> requests = requestRepository.findAll(spec);
}

O padrão garante que:

  • forTenant(tenantId) é obrigatória em toda query;
  • Se desenvolvedor esquecer, teste unitário falha (spec incompleta);
  • ORM (Hibernate) adiciona filtro tenant automaticamente;
  • Query gerada inclui sempre WHERE tenant_id = ?.

30.7.3 Triggers no Banco de Dados (Camada Defensiva)

Como camada defensiva adicional, triggers garantem integridade de tenant no nível do banco:

-- Trigger: previne INSERT sem tenant_id
CREATE TRIGGER trigger_requests_insert BEFORE INSERT ON public.requests
FOR EACH ROW
BEGIN
    IF NEW.tenant_id IS NULL THEN
        RAISE EXCEPTION 'tenant_id cannot be NULL';
    END IF;
END;

-- Trigger: previne UPDATE que mude tenant_id (imutável)
CREATE TRIGGER trigger_requests_update BEFORE UPDATE ON public.requests
FOR EACH ROW
BEGIN
    IF NEW.tenant_id != OLD.tenant_id THEN
        RAISE EXCEPTION 'tenant_id is immutable';
    END IF;
END;

30.8 Isolamento de Cache Distribuído

30.8.1 Strategy de Cache

O Redis é usado para cache de leitura frequente. O isolamento no cache é feito via chave namespaceada:

Sem isolamento (INSEGURO):
  KEY: "user:alice@example.com"
  VALUE: {...user_data...}
  PROBLEMA: dois tenants com mesmo email conflitam

Com isolamento (SEGURO):
  KEY: "tenant:{tenantId}:user:alice@example.com"
  VALUE: {...user_data...}

A aplicação nunca forma chave de cache sem incluir tenant:

String cacheKey = String.format(
    "tenant:%s:user:%s",
    TenantContext.getCurrent().getTenantId(),
    userId
);

User cachedUser = redisTemplate.opsForValue().get(cacheKey);
if (cachedUser == null) {
    // Busca do banco
    User user = userRepository.findById(userId);
    
    // Armazena em cache com tenant
    redisTemplate.opsForValue().set(
        cacheKey, 
        user, 
        Duration.ofHours(1)
    );
}

30.8.2 Invalidação de Cache por Tenant

Quando configuração de um tenant muda (ex: política de senha), cache daquele tenant é invalidado:

public void updateTenantPolicy(TenantPolicy policy) {
    UUID tenantId = policy.getTenantId();
    
    // Atualiza no banco
    tenantPolicyRepository.save(policy);
    
    // Invalida cache do tenant (com padrão)
    String pattern = String.format("tenant:%s:*", tenantId);
    redisTemplate.delete(
        redisTemplate.keys(pattern)
    );
}

Isso garante que não há cache "venenado" quando uma mudança é feita.


30.9 Isolamento de Filas e Mensageria

30.9.1 Topics e Routing

RabbitMQ é usado para comunicação assíncrona. O isolamento é feito via topic/routing key que inclui tenant:

Exchange: "platformEvents"

Routing keys por tipo de evento:
  - "tenant:{tenantId}.request.created"
  - "tenant:{tenantId}.request.approved"
  - "tenant:{tenantId}.document.uploaded"
  - "tenant:{tenantId}.interaction.closed"
  - "tenant:{tenantId}.campaign.executed"

Queue subscription:
  Q: "DocumentClassificationWorker_{tenantId}"
  Binding: exchange="platformEvents" 
           routing_key="tenant:{tenantId}.document.*"

Quando um evento é publicado, o publisher inclui tenant:

public void publishRequestCreated(Request request) {
    String routingKey = String.format(
        "tenant:%s.request.created",
        request.getTenantId()
    );
    
    rabbitTemplate.convertAndSend(
        "platformEvents",
        routingKey,
        new RequestCreatedEvent(request)
    );
}

Subscribers escutam apenas seu tenant:

@RabbitListener(
    bindings = @QueueBinding(
        value = @Queue(name = "DocumentClassificationWorker_#{tenantId}"),
        exchange = @Exchange(name = "platformEvents", type = "topic"),
        key = "tenant:#{tenantId}.document.*"
    )
)
public void onDocumentEvent(DocumentUploadedEvent event) {
    // event.getTenantId() == ThreadLocal tenant automaticamente
    documentClassificationService.classify(event);
}

30.9.2 Dead Letter Queues (DLQ) por Tenant

Quando um evento falha, é enviado para DLQ. A DLQ também é segregada por tenant:

Queue: "DocumentClassificationWorker_tenant1_dlq"
Queue: "DocumentClassificationWorker_tenant2_dlq"

Isso previne que falha de um tenant bloqueie fila de outro.


30.10 Isolamento de Busca e Indexação

30.10.1 Elasticsearch: Uma Estratégia

Elasticsearch é usado para busca semântica e full-text (Capítulo 36). Duas estratégias de isolamento:

Estratégia A: Um índice por tenant (recomendado para poucos tenants)

Index: "requests_tenant_<uuid_tenant1>"
Index: "requests_tenant_<uuid_tenant2>"
Index: "requests_tenant_<uuid_tenant3>"

Vantagem: isolamento físico, políticas de retenção por tenant, facilmente deletável. Desvantagem: muitos índices se houver muitos tenants.

Estratégia B: Índice compartilhado com filtro de tenant (recomendado para muitos tenants)

Index: "requests"
  Document: {
    "id": "req-123",
    "tenant_id": "tenant-1",
    "content": "Solicitação de habilitação...",
    "status": "OPEN"
  }

Toda query adiciona filtro de tenant:

SearchRequest searchRequest = new SearchRequest("requests");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

// Query do usuário
sourceBuilder.query(
    QueryBuilders.boolQuery()
        .must(QueryBuilders.multiMatchQuery(
            userSearchTerm, 
            "content", "title"
        ))
        .filter(QueryBuilders.termQuery(
            "tenant_id", 
            currentTenantId.toString()
        ))
);

searchRequest.source(sourceBuilder);

30.11 Isolamento de Armazenamento de Objetos

30.11.1 Namespace de Prefixos (S3 ou Blob Storage)

Documentos, imagens, anexos são armazenados em objeto storage (S3, Azure Blob, etc.). O isolamento é feito via prefixo:

s3://prodemge-documents/
  ├─ tenant-1/
  │  ├─ request-abc/
  │  │  ├─ documento-habilitacao.pdf
  │  │  └─ foto-rosto.jpg
  │  └─ request-def/
  │     └─ comprovante.pdf
  │
  ├─ tenant-2/
  │  └─ request-xyz/
  │     └─ requerimento.docx
  │
  └─ tenant-3/
     ├─ ...

Chave completa: s3://prodemge-documents/{tenant_id}/{entity_type}/{entity_id}/{filename}

Acesso é feito via presigned URL gerada pelo backend:

public String getPresignedDownloadUrl(UUID documentId) {
    Document doc = documentRepository.findById(documentId);
    
    // Valida que documento pertence ao tenant
    if (!doc.getTenantId().equals(TenantContext.getCurrent().getTenantId())) {
        throw new ForbiddenException();
    }
    
    // Gera URL assinada (válida por 15 minutos)
    String presignedUrl = s3Client.generatePresignedUrl(
        bucket,
        String.format(
            "%s/%s/%s/%s",
            doc.getTenantId(),
            "requests",
            doc.getRequestId(),
            doc.getFilename()
        ),
        Duration.ofMinutes(15)
    );
    
    return presignedUrl;
}

Políticas de bucket garantem que sem header de autenticação válido (gerado pelo backend), objetos não são acessíveis diretamente — o cliente deve passar pelo backend.


30.12 Isolamento de Auditoria

30.12.1 Trilha de Auditoria Multi-Tenant

Auditoria é registrada em tabela centralizada, mas com tenant_id:

CREATE TABLE audit_log (
    id                BIGINT PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    user_id           UUID NOT NULL,
    action            VARCHAR(50),
    resource_type     VARCHAR(50),
    resource_id       UUID,
    changes           JSONB,
    ip_address        VARCHAR(45),
    user_agent        VARCHAR(1024),
    timestamp         TIMESTAMP,
    
    INDEX idx_audit_tenant (tenant_id),
    INDEX idx_audit_tenant_user (tenant_id, user_id),
    INDEX idx_audit_tenant_timestamp (tenant_id, timestamp)
);

Toda ação registra tenant_id:

tenant_id: "tenant-1"
timestamp: 2026-07-16 14:32:15
user_id: "user-alice"
action: "REQUEST_APPROVED"
resource_id: "req-12345"
changes: {"status": "OPEN" → "APPROVED", "approved_by": "alice"}

Consultas de auditoria filtram sempre por tenant:

List<AuditLog> getAuditTrail(UUID tenantId, UUID userId) {
    return auditRepository.findAll(
        Specification
            .where((root, query, cb) -> 
                cb.equal(root.get("tenantId"), tenantId)
            )
            .and((root, query, cb) -> 
                cb.equal(root.get("userId"), userId)
            )
    );
}

30.13 Isolamento de Comunicação e Notificações

30.13.1 Segregação de Canais

Campanhas de comunicação, templates e notificações são segregados por tenant:

CREATE TABLE communication_templates (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    name              VARCHAR(255),
    channel           VARCHAR(50),  -- EMAIL, SMS, WHATSAPP
    subject           VARCHAR(255),
    body              TEXT,
    created_by        UUID,
    created_at        TIMESTAMP,
    
    INDEX idx_templates_tenant (tenant_id),
    UNIQUE(tenant_id, name, channel)
);

CREATE TABLE campaigns (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    name              VARCHAR(255),
    template_id       UUID NOT NULL,
    segment_id        UUID,
    status            VARCHAR(50),
    scheduled_at      TIMESTAMP,
    executed_at       TIMESTAMP,
    
    INDEX idx_campaigns_tenant (tenant_id),
    FOREIGN KEY (tenant_id) REFERENCES tenants(id),
    FOREIGN KEY (template_id) REFERENCES communication_templates(id)
);

Quando uma campanha é executada, o serviço de comunicação valida tenant:

public void executeCampaign(UUID campaignId) {
    Campaign campaign = campaignRepository.findById(campaignId);
    
    // Valida tenant
    if (!campaign.getTenantId().equals(TenantContext.getCurrent().getTenantId())) {
        throw new ForbiddenException();
    }
    
    // Busca template
    CommunicationTemplate template = 
        templateRepository.findById(campaign.getTemplateId());
    
    // Valida que template pertence ao mesmo tenant
    if (!template.getTenantId().equals(campaign.getTenantId())) {
        throw new DataIntegrityException();
    }
    
    // Carrega segmento de cidadãos
    List<Citizen> recipients = segmentService.getCitizens(
        campaign.getSegmentId(), 
        campaign.getTenantId()
    );
    
    // Enfileira notificações por canal
    for (Citizen citizen : recipients) {
        notificationQueue.enqueue(
            new NotificationTask(
                campaign.getTenantId(),
                template,
                citizen,
                campaign.getId()
            )
        );
    }
}

30.13.2 Consentimento Segregado

Consentimentos do cidadão para comunicação são armazenados por tenant:

CREATE TABLE citizen_consents (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    citizen_id        UUID NOT NULL,
    consent_type      VARCHAR(50),  -- EMAIL, SMS, WHATSAPP, MARKETING
    granted           BOOLEAN,
    granted_at        TIMESTAMP,
    revoked_at        TIMESTAMP,
    
    INDEX idx_consents_tenant_citizen (tenant_id, citizen_id),
    UNIQUE(tenant_id, citizen_id, consent_type)
);

Um cidadão pode ter diferentes consentimentos em diferentes tenants:

Tenant A (Secretaria de Educação):
  - citizen-123: EMAIL=true, SMS=false, MARKETING=true

Tenant B (Secretaria de Saúde):
  - citizen-123: EMAIL=true, SMS=true, MARKETING=false

Antes de enviar notificação, o serviço valida consentimento para o tenant correto:

boolean canSendEmail(UUID tenantId, UUID citizenId) {
    CitizenConsent consent = consentRepository.findOne(
        Specification
            .where((root, query, cb) -> 
                cb.and(
                    cb.equal(root.get("tenantId"), tenantId),
                    cb.equal(root.get("citizenId"), citizenId),
                    cb.equal(root.get("consentType"), "EMAIL")
                )
            )
    );
    
    return consent != null && consent.isGranted();
}

30.14 Isolamento de Integrações Externas

30.14.1 Credenciais de Integração por Tenant

Cada tenant pode ter integrações próprias com sistemas externos (SEI!MG, sistemas de trânsito, etc.). Credenciais são armazenadas segregadas:

CREATE TABLE integration_credentials (
    id                UUID PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    integration_name  VARCHAR(100),  -- "SEI", "TRAFFIC_SYSTEM", "PROBPMS"
    api_key           VARCHAR(512),  -- encrypted
    api_secret        VARCHAR(512),  -- encrypted
    endpoint_url      VARCHAR(1024),
    status            VARCHAR(50),
    last_validated_at TIMESTAMP,
    
    INDEX idx_integrations_tenant (tenant_id),
    UNIQUE(tenant_id, integration_name)
);

Credenciais são criptografadas em repouso:

public void storeIntegrationCredential(
    UUID tenantId, 
    String integrationName, 
    String apiKey, 
    String apiSecret
) {
    IntegrationCredential credential = new IntegrationCredential(
        tenantId,
        integrationName,
        encryptionService.encrypt(apiKey),
        encryptionService.encrypt(apiSecret)
    );
    
    credentialRepository.save(credential);
}

Quando um serviço faz chamada para integração externa, valida tenant e recupera credencial:

public Response callExternalAPI(
    String integrationName, 
    String endpoint, 
    String payload
) {
    UUID currentTenant = TenantContext.getCurrent().getTenantId();
    
    IntegrationCredential credential = 
        credentialRepository.findOne(
            Specification
                .where((root, query, cb) -> 
                    cb.and(
                        cb.equal(root.get("tenantId"), currentTenant),
                        cb.equal(root.get("integrationName"), integrationName)
                    )
                )
        );
    
    if (credential == null) {
        throw new NotFoundException("Integration not configured for tenant");
    }
    
    String decryptedKey = encryptionService.decrypt(credential.getApiKey());
    
    return externalApiClient.call(
        credential.getEndpointUrl(),
        endpoint,
        decryptedKey,
        payload
    );
}

30.14.2 Logs de Integração por Tenant

Toda chamada a integração externa é registrada em log segregado:

CREATE TABLE integration_logs (
    id                BIGINT PRIMARY KEY,
    tenant_id         UUID NOT NULL,
    integration_name  VARCHAR(100),
    request_at        TIMESTAMP,
    request_payload   TEXT,  -- primeiros 4KB por segurança
    response_status   INT,
    response_payload  TEXT,  -- primeiros 4KB
    duration_ms       INT,
    error_message     TEXT,
    
    INDEX idx_integration_logs_tenant (tenant_id),
    INDEX idx_integration_logs_tenant_integration (tenant_id, integration_name)
);

Logs podem ser consultados por tenant sem cross-contamination:

List<IntegrationLog> getIntegrationLogs(
    UUID tenantId, 
    String integrationName, 
    LocalDateTime from, 
    LocalDateTime to
) {
    return logRepository.findAll(
        Specification
            .where((root, query, cb) -> 
                cb.and(
                    cb.equal(root.get("tenantId"), tenantId),
                    cb.equal(root.get("integrationName"), integrationName),
                    cb.between(root.get("requestAt"), from, to)
                )
            ),
        Sort.by("requestAt").descending()
    );
}

30.15 Isolamento na Camada de Negócio

30.15.1 Agregados DDD com Tenant

No DDD (Domain-Driven Design), agregados são construídos com tenant como parte da identidade:

@Entity
@Table(name = "requests")
public class Request {
    
    @EmbeddedId
    private RequestId id;  // Contém UUID + tenantId
    
    @Embedded
    private TenantId tenantId;
    
    @Embedded
    private CitizenId citizenId;
    
    @Embedded
    private ServiceId serviceId;
    
    private String status;
    private LocalDateTime createdAt;
    
    // Constructor valida tenant
    public Request(
        RequestId id,
        TenantId tenantId,
        CitizenId citizenId,
        ServiceId serviceId
    ) {
        this.id = id;
        this.tenantId = tenantId;
        this.citizenId = citizenId;
        this.serviceId = serviceId;
        this.createdAt = LocalDateTime.now();
    }
    
    // Métodos de negócio validam tenant implicitamente
    public void approve() {
        if (!this.status.equals("IN_ANALYSIS")) {
            throw new InvalidStateException();
        }
        this.status = "APPROVED";
    }
}

30.15.2 Value Objects com Tenant

Value objects imutáveis encapsulam tenant e previnem comparações cross-tenant:

@Embeddable
public class RequestId implements ValueObject {
    
    @Column(name = "id")
    private UUID value;
    
    @Column(name = "tenant_id")
    private UUID tenantId;
    
    // Constructor privado
    private RequestId() {}
    
    public RequestId(UUID value, UUID tenantId) {
        Objects.requireNonNull(value);
        Objects.requireNonNull(tenantId);
        this.value = value;
        this.tenantId = tenantId;
    }
    
    // Equals considera tenant
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof RequestId)) return false;
        RequestId that = (RequestId) o;
        return this.value.equals(that.value) && 
               this.tenantId.equals(that.tenantId);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(value, tenantId);
    }
}

30.16 Teste e Validação de Isolamento

30.16.1 Testes Automáticos

Testes de isolamento são obrigatórios antes de merge em main:

@Test
public void testCannotAccessRequestFromOtherTenant() {
    // Setup
    Tenant tenant1 = tenantRepository.save(new Tenant("Tenant 1"));
    Tenant tenant2 = tenantRepository.save(new Tenant("Tenant 2"));
    
    User user1 = createUser(tenant1);
    
    Request request2 = requestRepository.save(
        new Request(UUID.randomUUID(), tenant2.getId(), "...")
    );
    
    // Attempt
    TenantContext.setCurrent(tenant1.getId());
    
    // Assert
    assertThrows(
        ForbiddenException.class,
        () -> requestService.getRequest(request2.getId())
    );
}

@Test
public void testCacheRespectsTenantBoundary() {
    // Tenant 1 busca usuário
    TenantContext.setCurrent(tenant1.getId());
    User user1 = userService.getUser(userId);
    
    // Tenant 2 busca usuário com mesmo ID
    TenantContext.setCurrent(tenant2.getId());
    User user2 = userService.getUser(userId);
    
    // Devem ser usuários diferentes
    assertNotEquals(user1, user2);
}

@Test
public void testDatabaseConstraintEnforcesTenant() {
    // Tenta inserir documento sem tenant_id
    assertThrows(
        DataIntegrityViolationException.class,
        () -> {
            Document doc = new Document();
            doc.setId(UUID.randomUUID());
            doc.setTenantId(null);  // Violação
            documentRepository.save(doc);
        }
    );
}

30.16.2 Verificação de Cross-Tenant Vulnerability

Testes de segurança checam padrões comuns de vulnerabilidade:

@Test
public void testQueryParameterTenantBypassBlocked() {
    // Tenant 1 tenta acessar via query param fake
    TenantContext.setCurrent(tenant1.getId());
    
    // Mesmo que envie X-Tenant-Id=tenant2, será ignorado
    String result = mvc.perform(
        get("/api/requests/req-123")
            .header("X-Tenant-Id", tenant2.getId().toString())
    )
    .andExpect(status().isForbidden());
}

@Test
public void testRawSQLInjectionRespectsTenant() {
    // Tenta SQL injection para contornar filtro de tenant
    TenantContext.setCurrent(tenant1.getId());
    
    assertThrows(
        Exception.class,
        () -> requestRepository.findAllRaw(
            "SELECT * FROM requests WHERE id = '" + 
            "req-123' OR tenant_id IS NOT NULL --'"
        )
    );
}

30.17 Riscos e Mitigações

RiscoConsequênciaMitigação
TenantContext não inicializadoOperação executa sem tenant, pode vazar dadosSempre inicializar em filter, falhar se não definido
Desenvolvedor esquece de filtrar por tenantQuery retorna dados de outro tenantSpecification obrigatória em repository pattern
Cache compartilhado sem namespaceCidadão acessa dados de outro tenantChave de cache sempre inclui tenant_id
Trigger de banco não validadoINSERT sem tenant_id não é bloqueadoNOT NULL constraint + trigger BEFORE INSERT
JWT do token forjadoAcesso não autorizado a tenantSempre validar assinatura de token
Rate limiting global sem tenantAtaque a um tenant degrada outroRate limiting por tenant, não global
Backup com dados misturadosRestauração contamina tenantsBackup por tenant, segregado fisicamente
Log de auditoria centralizado sem tenantAuditor de um tenant acessa logs de outroÍndice sempre (tenant_id, timestamp), filtro obrigatório
Integração externa sem validação de tenantCredencial de tenant A usada por tenant BSempre buscar credencial validando tenant
Elasticsearch com query mal formadaBusca vaza dados de outro tenantFiltro termo de tenant obrigatório em toda query

30.18 Benefícios do Modelo Multi-Tenant Isolado

  • Escalabilidade operacional — novo tenant é onboarded em minutos, não em semanas;
  • Custo-benefício — compartilhamento físico de recursos (servidor, banco, cache) reduz custo operacional;
  • Isolamento forte — múltiplas camadas de validação (aplicação, banco, cache) garantem segurança;
  • Governança por tenant — políticas de senha, sessão, MFA configuráveis independentemente;
  • Auditoria desagregada — cada tenant visualiza apenas suas ações;
  • Conformidade LGPD — exclusão de dados de um tenant não afeta outro;
  • Recuperação granular — restauração de backup de um tenant isolado não impacta outros;
  • Equipes operacionais independentes — admin de tenant A não acessa tenant B;
  • Modelo econômico flexível — fácil ajustar pricing, features e SLA por tenant;
  • Testabilidade — testes de isolamento automatizados garantem integridade contínua.

30.19 Decisões Arquiteturais

ADRTema
ADR-101Modelo de isolamento lógico vs. físico — segregação lógica em banco único confirmada como suficiente para PRODEMGE
ADR-102TenantContext em variável ThreadLocal — seguro em ambiente de container com thread pool limitado
ADR-103Iniciador de TenantContext no HTTP Filter — abordagem recomendada, valida tenant antes de business logic
ADR-104Specification pattern com Spring Data JPA — garante que nenhuma query escapa do filtro de tenant
ADR-105Cache distribuído com namespace por tenant — Redis com chave de tenant obrigatória
ADR-106Políticas de retenção de dados por tenant — cada tenant configura ciclo de vida de seus dados
ADR-107Índices de banco de dados — (tenant_id, recurso_id) como chave composta primária
ADR-108Elasticsearch — filtro de tenant em toda query, sem exceção
ADR-109Arquivos e documentos — armazenamento na nuvem com prefix de tenant_id
ADR-110Fila de mensagens — RabbitMQ com exchange por tenant, sem mixing de mensagens

30.20 Considerações Finais

O modelo multi-tenant não é apenas uma característica de escalabilidade — é um compromisso arquitetural fundamental que afeta decisões em cada camada, desde o desenho de tabelas até a forma como logs são consultados.

A força deste modelo reside em três pilares interligados:

  1. Defesa em profundidade — não há uma única "barreira de segurança" que, se contornada, exponha dados de outro tenant. A validação ocorre na API, na camada de persistência, no cache, na fila de mensagens, na integração com sistemas externos. Um desenvolvedor que esquece de filtrar por tenant em um lugar não compromete o sistema porque o banco o impede na sequência.

  2. Simplicidade operacional — a ausência de infraestrutura separada por tenant significa que onboarding de um novo órgão é uma ação administrativo, não um projeto de meses. Um novo tenant é criado em minutos, com schemas e dados iniciais provisionados automaticamente.

  3. Conformidade por design — LGPD exige direito ao esquecimento (exclusão de dados pessoais). Em modelo multi-tenant bem implementado, exclusão dos dados de um tenant é operação isolada, sem risco de afetar outros. Auditoria, backup, recuperação — tudo é desagregável por tenant.

O risco residual é sempre o mesmo em arquiteturas multi-tenant: um desenvolvedor que não compreende a criticidade do isolamento e implementa um atalho. Por isso, o modelo exige:

  • Cultura de segurança — toda pessoa na equipe entende que tenant é a primeira questão, não a última;
  • Automação de testes — testes de isolamento executam em todo commit, bloqueando merge se falharem;
  • Code review orientado a tenant — revisor sempre pergunta: "onde está filtrado por tenant?";
  • Documentação periódica — riscos e mitigações são revisados trimestralmente, não apenas durante design inicial.

O Capítulo 31 detalha a implementação do Backend em Java e Spring Boot.


30.21 Controle de Versão

CampoValor
DocumentoDocumento Mestre — Plataforma de Relacionamento Digital com o Cidadão
Capítulo30 — Gestão Multi-Tenant
Versão1.0
SituaçãoConcluído
Última atualização16/07/2026
Status de AprovaçãoAprovado

30.22 Rastreabilidade PRODEMGE

Edital CP 001/2026:

  • Item 3.1.3 — Arquitetura flexível, multi-tenant, operando em nuvem híbrida, nuvem pública ou on-premise
  • Item 3.2.2 — Isolamento lógico de dados por tenant

Plano de Negócio (Anexo I):

  • Seção 3.1.2 — Arquitetura modular orientada a serviços
  • Seção 3.1.10 — Multi-tenancy com segregação completa
  • Seção 3.1.7 — Segurança da informação e isolamento de dados
  • Seção 3.3.5 — Conformidade LGPD e proteção de dados pessoais

Funcionalidades (Anexo III) — 60% pontuação:

  • Bloco 1 — Integração e Orquestração: Item 1.1 (plataforma integrada com suporte a múltiplos órgãos = multi-tenant)
  • Bloco 6 — Segurança, Governança, Acesso: Item 6.2 (controle de acesso com RBAC por tenant), Item 6.4 (configurabilidade por tenant)

Capacidades (Anexo IV) — 30% pontuação:

  • Item 3.1.10 — Plataforma multi-tenant com segregação lógica completa de dados
  • Item 3.1.6 — Escalabilidade horizontal para suportar múltiplos órgãos em instância única
  • Item 3.1.8 — Isolamento de dados e conformidade LGPD por tenant
  • Item 3.3.2 — Cache distribuído com segregação por tenant
  • Item 3.3.4 — Fila de mensagens com segregação de tópicos por tenant

Sustentabilidade (Anexo V) — 10% pontuação:

  • Item 2.2 — Isolamento de dados pessoais conforme LGPD
  • Item 2.3 — Proteção de dados pessoais e direitos do titular (exclusão, portabilidade) por tenant
  • Item 2.4 — Auditoria desagregada por tenant

Esclarecimentos pertinentes:

  • Montreal (02/07/2026), Bloco 1, item 1 — Multi-tenant confirmado como segregação lógica; instâncias lógicas segregadas atendem ao requisito
  • Valtech (03/07/2026), item 3.1.10 — Multi-tenancy operando em nuvem é suficiente para pontuação
  • Madrona (08/07/2026) — Documentação funcional e de arquitetura aceita como evidência de multi-tenancy

Erratas:

  • Errata nº 002 (10/07/2026) — Não aplicável a este capítulo

30.23 Próximo Capítulo

O Capítulo 31 — Backend: Java e Spring Boot detalha a implementação do serviço backend, incluindo arquitetura hexagonal, padrões de design, framework Spring Boot, tratamento de exceções, validação, serialização e integração com os componentes de identidade, autorização, isolamento multi-tenant descritos neste capítulo e no Capítulo 29.


Fim do Capítulo 30 — Gestão Multi-Tenant

Nesta página

30.1 Objetivo do Capítulo30.2 Contexto e Motivação30.2.1 Cenário Sem Multi-Tenant30.2.2 Oportunidade Multi-Tenant30.2.3 Confirmação Regulatória30.3 Definições Fundamentais30.3.1 O que é um Tenant30.3.2 Tenant Context30.3.3 Segregação Lógica vs. Física30.4 Modelo Arquitetural de Multi-Tenant30.4.1 Camadas de Isolamento30.4.2 Identificação de Dados30.5 Ciclo de Vida de um Tenant30.5.1 Provisioning (Criação)30.5.2 Operação (Uso Normal)30.5.3 Suspensão (Temporária)30.5.4 Ofboarding (Exclusão)30.5.5 Estados do Tenant30.6 Isolamento de Contexto na Aplicação30.6.1 Tenant Context Injection30.6.2 Validação de Tenant em Operações30.6.3 Propagação para Serviços Internos30.7 Isolamento de Dados na Persistência30.7.1 Schema do Banco de Dados30.7.2 Queries Seguras30.7.3 Triggers no Banco de Dados (Camada Defensiva)30.8 Isolamento de Cache Distribuído30.8.1 Strategy de Cache30.8.2 Invalidação de Cache por Tenant30.9 Isolamento de Filas e Mensageria30.9.1 Topics e Routing30.9.2 Dead Letter Queues (DLQ) por Tenant30.10 Isolamento de Busca e Indexação30.10.1 Elasticsearch: Uma Estratégia30.11 Isolamento de Armazenamento de Objetos30.11.1 Namespace de Prefixos (S3 ou Blob Storage)30.12 Isolamento de Auditoria30.12.1 Trilha de Auditoria Multi-Tenant30.13 Isolamento de Comunicação e Notificações30.13.1 Segregação de Canais30.13.2 Consentimento Segregado30.14 Isolamento de Integrações Externas30.14.1 Credenciais de Integração por Tenant30.14.2 Logs de Integração por Tenant30.15 Isolamento na Camada de Negócio30.15.1 Agregados DDD com Tenant30.15.2 Value Objects com Tenant30.16 Teste e Validação de Isolamento30.16.1 Testes Automáticos30.16.2 Verificação de Cross-Tenant Vulnerability30.17 Riscos e Mitigações30.18 Benefícios do Modelo Multi-Tenant Isolado30.19 Decisões Arquiteturais30.20 Considerações Finais30.21 Controle de Versão30.22 Rastreabilidade PRODEMGE30.23 Próximo Capítulo