Hub externo — autenticação e conexões#
Use este padrão quando outro sistema já é responsável por autenticar usuários e/ou gerenciar credenciais de banco por tenant. O Sulfite se integra a qualquer hub que implemente os contratos descritos aqui — a linguagem ou stack do hub não importa.
Quando usar#
- Seu sistema já autentica usuários e emite JWT — você não quer duplicar login no Sulfite.
- Cada tenant tem seu próprio banco, cujas credenciais ficam gerenciadas centralmente.
- O Sulfite deve executar relatórios sem armazenar senhas dos bancos dos tenants.
- Múltiplas réplicas do
sulfite_serverprecisam funcionar sem estado local compartilhado.
Arquitetura#
App cliente
│
│ 1. login(usuario, senha, tenant_id)
▼
Hub de autenticação (qualquer stack)
│
│ 2. emite JWT RS256 com tenant_id e permissões
▼
App cliente
│
│ 3. POST /api/v1/reports/:id/generate
│ Authorization: Bearer <jwt>
▼
sulfite_server
│
├─ valida JWT via JWKS do hub
├─ verifica tenant e permissões
├─ carrega o relatório no storage do Sulfite
├─ resolve connectionRef no hub de conexões
└─ executa datasource e renderiza PDF/HTML/CSV/Excel
O Sulfite mantém relatórios e metadados no seu próprio storage. Os dados de negócio ficam nos bancos dos tenants, acessados com credenciais fornecidas pelo hub em runtime.
Configuração#
# Auth externo via JWT RS256/JWKS
AUTH_JWKS_URL=https://auth.your-system.example.com/.well-known/jwks.json
AUTH_ISSUER=https://auth.your-system.example.com
AUTH_AUDIENCE=sulfite-api
AUTH_TENANT_CLAIM=tenant_id
# Conexões resolvidas pelo hub
CONNECTION_REGISTRY_BACKEND=remote
CONNECTION_HUB_URL=https://hub.your-system.internal
CONNECTION_HUB_TOKEN=<credencial-de-servico>
CONNECTION_HUB_TIMEOUT_MS=3000
CONNECTION_HUB_CACHE_TTL_SECONDS=60
CONNECTION_ROUTES_ENABLED=false
# Multi-tenancy e storage do Sulfite
MULTI_TENANT_MODE=true
STORAGE_BACKEND=postgres
DATABASE_URL=postgresql://sulfite:secret@postgres:5432/sulfite_reports
# Recomendado em produção
SULFITE_CLUSTER_MODE=true
ENCRYPTION_KEY=<openssl rand -base64 32>
| Variável | Obrigatória | Descrição |
|---|---|---|
AUTH_JWKS_URL |
Sim | URL pública do JWKS do hub de autenticação |
AUTH_ISSUER |
Não | Valor esperado no claim iss |
AUTH_AUDIENCE |
Não | Valor esperado no claim aud |
AUTH_JWKS_CACHE_TTL_SECONDS |
Não | TTL do cache de chaves JWKS. Default: 300 |
AUTH_JWKS_FETCH_TIMEOUT_SECONDS |
Não | Timeout para buscar JWKS. Default: 10 |
AUTH_TENANT_CLAIM |
Não | Claim que contém o tenant. Default: tenant_id |
CONNECTION_REGISTRY_BACKEND |
Sim | Use remote |
CONNECTION_HUB_URL |
Sim | URL interna do hub de conexões |
CONNECTION_HUB_TOKEN |
Sim | Credencial de serviço usada pelo Sulfite |
CONNECTION_HUB_TIMEOUT_MS |
Não | Timeout para chamadas ao hub. Default: 3000 |
CONNECTION_HUB_CACHE_TTL_SECONDS |
Não | TTL do cache de conexões. Default: 60 |
CONNECTION_ROUTES_ENABLED |
Não | Use false para desabilitar /connections |
Contrato do JWT#
O hub de autenticação deve emitir JWT assinado com RS256 e publicar as chaves públicas em um endpoint JWKS. O header do JWT precisa conter
kid.
Payload esperado:
{
"sub": "usuario@example.com",
"email": "usuario@example.com",
"iss": "https://auth.your-system.example.com",
"aud": "sulfite-api",
"exp": 1777065600,
"tenant_id": "tenant-a-uuid",
"permissions": {
"reports": {
"list": true,
"generate": true,
"create": false,
"edit": false,
"delete": false,
"settings": false,
"share": false,
"edit_global": false,
"admin": false
}
}
}
Mapeamento de claims para o token interno do Sulfite:
| Claim do hub | Campo no Sulfite |
|---|---|
sub | AuthToken.subject |
email | AuthToken.email |
tenant_id ou tnt | AuthToken.tenantId |
permissions.reports.list | canList |
permissions.reports.generate | canGenerate |
permissions.reports.create | canInsert |
permissions.reports.edit | canEdit |
permissions.reports.delete | canDelete |
permissions.reports.settings | canSettings |
permissions.reports.share | canShare |
permissions.reports.edit_global | canEditGlobal |
permissions.reports.admin | canAdmin |
Se o token for inválido, expirado, assinado por uma chave desconhecida ou tiver iss/aud
divergente, a API retorna 401.
Contrato do endpoint de conexões#
Quando um relatório usa um datasource com connectionRef, o Sulfite chama:
GET /internal/tenants/{tenantId}/connections/{connectionRef}
Authorization: Bearer <CONNECTION_HUB_TOKEN>
Resposta para conexão de banco:
{
"type": "database",
"name": "erp_main",
"host": "db.tenant-a.internal",
"port": 5432,
"database": "erp",
"username": "readonly",
"password": "secret",
"ssl": true
}
Resposta para conexão HTTP:
{
"type": "http",
"name": "api_fiscal",
"baseUrl": "https://api-fiscal.tenant-a.internal",
"defaultHeaders": { "X-Api-Key": "secret" }
}
| Status do hub | Resultado no Sulfite |
|---|---|
200 | Conexão cacheada e usada na geração |
404 | resolveAsync retorna null |
403 |
Geração retorna 403 connection_forbidden |
401 |
Geração retorna 503 connection_unavailable |
timeout / 5xx |
Geração retorna 503 connection_unavailable |
Implementando o endpoint no hub#
Os parâmetros tenantId e connectionRef chegam como segmentos de path URL-encoded, prevenindo path traversal. O hub só precisa:
- Verificar o token
Bearerno headerAuthorization - Buscar as credenciais pelo
tenantId+name - Retornar o JSON no formato acima, ou
404se não encontrado
GET /internal/tenants/tenant-a-uuid/connections/erp_main
Authorization: Bearer <CONNECTION_HUB_TOKEN>
Cache e aquecimento#
O RemoteConnectionRegistry mantém cache em memória por tenantId/connectionRef. Antes de executar o relatório, o
GenerateHandler aquece as conexões usadas por:
DataSource.connectionRef;DbLookupConfig.connectionRefem parâmetros de relatório.
Esse aquecimento é necessário porque os resolvers de datasource usam a API síncrona ConnectionRegistry.resolve(). O fluxo:
generate request
├─ parse report definition
├─ coleta connectionRef dos datasources e lookups
├─ await registry.resolveAsync(tenantId, connectionRef) ← chamada ao hub
├─ cache preenchido
└─ engine.generate() usa registry.resolve(connectionRef) ← lê do cache
Em jobs assíncronos (Prefer: respond-async), o aquecimento é repetido dentro do executor do job, porque o cache da request HTTP original pode expirar antes da execução.
Desabilitando o CRUD de conexões#
Quando o hub é a única fonte de verdade para credenciais, desabilite as rotas de conexão do Sulfite:
CONNECTION_ROUTES_ENABLED=false
Isso remove do roteador:
GET /api/v1/connections
POST /api/v1/connections
GET /api/v1/connections/:name
PUT /api/v1/connections/:name
DELETE /api/v1/connections/:name
POST /api/v1/connections/:name/test
Mesmo com as rotas desligadas, mantenha permissions.reports.settings=false nos JWTs de usuários finais.
Testando a integração#
# Valide o JWKS do hub
curl https://auth.your-system.example.com/.well-known/jwks.json
# Gere um relatório com JWT do hub
curl -X POST "https://sulfite.example.com/api/v1/reports/<id>/generate?format=pdf" \
-H "Authorization: Bearer $HUB_JWT" \
-o report.pdf
# Confirme que o CRUD de conexões está desabilitado
curl -i https://sulfite.example.com/api/v1/connections \
-H "Authorization: Bearer $HUB_JWT"
# Esperado: 404
Checklist de produção#
- Publique JWKS com rotação de chaves mantendo as antigas até todos os tokens expirarem.
- Use
expcurto nos JWTs do hub. - Configure
AUTH_ISSUEReAUTH_AUDIENCEem produção. - Mantenha
/internal/*acessível apenas aosulfite_server. - Use
CONNECTION_HUB_TOKENcomo credencial de serviço, nunca o token do usuário final. -
Desabilite
CONNECTION_ROUTES_ENABLEDquando conexões forem gerenciadas exclusivamente pelo hub. -
Use
STORAGE_BACKEND=postgreseSULFITE_CLUSTER_MODE=trueem múltiplas réplicas. - Nunca registre senha de conexão ou token de serviço em logs.
Ver também#
- Autenticação & Tokens — modos de auth, HMAC e JWKS
- Conexões — tipos de conexão e criptografia at-rest
- Configuração — todas as env vars
- Cluster — backends distribuídos
- Multi-tenancy