Logosulfite.app
rafagazani/sulfite 999999

Edição Protegida

Controle de acesso ao Studio com verificação de identidade via OTP

Edição Protegida#

A EditPolicy permite restringir quem pode abrir o designer e salvar alterações em relatórios. Quando ativa, o Studio solicita um código OTP por email antes de liberar a edição e exige um segundo código OTP para confirmar o salvamento.

┌──────────┐  POST /edit/verify      ┌──────────────┐   email   ┌──────────┐
│  Studio   │────────────────────────▶│  Server       │──────────▶│  Email   │
│ (edit)   │◀── { status: sent }     │  (gera OTP)   │           │  (OTP)   │
│           │                         └──────────────┘           └──────────┘
│           │  POST /edit/confirm
│           │  { email, reportId, code, intent: "edit" }
│           │────────────────────────▶┌──────────────┐
│           │◀── { token, expiresAt } │  Server       │
│           │                         │  (verifica +  │
│           │  (usa token para editar)│   emite token)│
│           │                         └──────────────┘
│ (save)   │  POST /edit/verify (intent: "save")
│           │────────────────────────▶ (mesmo fluxo)
│           │◀── saveToken
│           │
│           │  PUT /api/v1/reports/:id
│           │  X-Edit-Token: <saveToken>
│           │────────────────────────▶┌──────────────┐
│           │◀── { report atualizado }│  Server       │
└──────────┘                          └──────────────┘

Dois intents distintos:

IntentQuandoTTL do token
editAo abrir o designer1 hora (configurável)
saveAo confirmar salvamento5 minutos (configurável)

Os dois tokens são assinados com HMAC-SHA256, ligados ao reportId e ao email verificado.

Endpoints#

Política — GET /api/v1/edit/policy#

Retorna a configuração de EditPolicy do servidor. O cliente usa isso para decidir se deve exibir o fluxo OTP.

Response 200:

{
  "enabled": true,
  "editTokenExpirationSeconds": 3600,
  "saveTokenExpirationSeconds": 300,
  "otpExpirationSeconds": 300,
  "identityHints": ["@acme.com"]
}

Solicitar OTP — POST /api/v1/edit/verify#

Gera e envia um código OTP para o email informado.

Body:

{
  "email": "user@acme.com",
  "reportId": "uuid-do-relatorio",
  "intent": "edit"
}
CampoObrigatórioValores
emailSimemail válido
reportIdSimID do relatório
intent Não "edit" (default) ou "save"

Response 200:

{
  "status": "sent",
  "expiresIn": 300
}

Erros:

StatusErroQuando
400invalid_emailEmail inválido
400 validation_error reportId ausente
400 invalid_intent intent diferente de "edit" ou "save"
400 policy_disabled EditPolicy não habilitada no servidor
403 identity_not_allowed Email/domínio não permitido por EDIT_ALLOWED_IDENTITIES
404not_foundRelatório não encontrado
429rate_limitedCódigo já enviado recentemente
503 not_configured OTP service não configurado (sem email provider)
503 service_unavailable Falha no envio do email ou OTP store cheio

Confirmar OTP — POST /api/v1/edit/confirm#

Verifica o código OTP e retorna um token assinado.

Body:

{
  "email": "user@acme.com",
  "reportId": "uuid-do-relatorio",
  "code": "123456",
  "intent": "edit"
}

Response 200:

{
  "token": "<base64url(payload)>.<hmac>",
  "expiresAt": "2026-04-11T18:00:00.000Z"
}

Erros:

StatusErroQuando
400invalid_codeCódigo incorreto
400 otp_not_found Nenhum código pendente para este email/relatório
400policy_disabledEditPolicy não habilitada
403 identity_not_allowed Email/domínio não permitido
404not_foundRelatório não encontrado
410expiredCódigo expirado
423 max_attempts Tentativas esgotadas — solicitar novo código
503not_configuredOTP service não configurado

Salvar com token — PUT /api/v1/reports/:id#

Quando a EditPolicy está ativa, o endpoint de atualização exige o header X-Edit-Token com um token de intent save.

Headers:

X-Edit-Token: <saveToken>

Erros específicos da EditPolicy:

StatusErroQuando
401 save_token_required Header X-Edit-Token ausente
403 save_token_invalid Token inválido, expirado, intent errado ou reportId diferente

Configuração#

Variáveis de ambiente#

VariávelDefaultDescrição
EDIT_POLICY_ENABLEDfalseHabilita a EditPolicy
EDIT_ALLOWED_IDENTITIES — (todos) Emails ou domínios autorizados, separados por vírgula
EDIT_TOKEN_SECRET — (efêmero) Segredo HMAC para assinar tokens. Sem essa variável, tokens expiram ao reiniciar o servidor
EDIT_TOKEN_EXPIRATION 3600 TTL do token de edição em segundos
SAVE_TOKEN_EXPIRATION 300 TTL do token de salvamento em segundos
EDIT_OTP_EXPIRATION 300 Expiração do código OTP em segundos
EDIT_OTP_MAX_ATTEMPTS 3 Tentativas máximas antes de invalidar
EDIT_OTP_LENGTH 6 Comprimento do código OTP numérico

Também é necessário configurar um email provider. Veja Configuração → Email.

Exemplo de configuração#

export EDIT_POLICY_ENABLED=true
export EDIT_ALLOWED_IDENTITIES="acme.com,admin@parceiro.com"
export EDIT_TOKEN_SECRET="segredo-hmac-longo-e-aleatorio"
export RESEND_API_TOKEN="re_..."
export RESEND_FROM_EMAIL="noreply@acme.com"

Fluxo completo#

1. Cliente detecta a política#

GET /api/v1/edit/policy
# → { "enabled": true, "editTokenExpirationSeconds": 3600, ... }

Se enabled: true, o Studio exibe o fluxo OTP ao abrir o designer.

2. Solicitar OTP de edição#

curl -X POST https://server.com/api/v1/edit/verify \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@acme.com", "reportId": "abc123", "intent": "edit" }'

# Response: { "status": "sent", "expiresIn": 300 }

3. Confirmar e obter token de edição#

curl -X POST https://server.com/api/v1/edit/confirm \
  -H "Content-Type: application/json" \
  -d '{ "email": "user@acme.com", "reportId": "abc123", "code": "482910", "intent": "edit" }'

# Response: { "token": "eyJ...<payload>.<sig>", "expiresAt": "..." }

O Studio armazena o token e libera a interface de edição pelo tempo configurado em EDIT_TOKEN_EXPIRATION.

4. Solicitar OTP de salvamento#

Ao salvar, o Studio repete o fluxo com intent: "save":

curl -X POST https://server.com/api/v1/edit/verify \
  -d '{ ..., "intent": "save" }'

curl -X POST https://server.com/api/v1/edit/confirm \
  -d '{ ..., "intent": "save" }'
# → saveToken com TTL de SAVE_TOKEN_EXPIRATION segundos

5. Salvar o relatório#

curl -X PUT https://server.com/api/v1/reports/abc123 \
  -H "X-Edit-Token: <saveToken>" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Nome", "definitionJson": "{...}" }'

Formato do token#

O token é composto por dois segmentos separados por .:

base64url(payload).hmac_sha256

O payload é um JSON com:

{
  "reportId": "uuid-do-relatorio",
  "email": "user@acme.com",
  "intent": "edit",
  "exp": 1713456000000
}

exp é um timestamp em milissegundos (não segundos). O servidor rejeita tokens expirados, com signature inválida, intent diferente de "save" (no PUT) ou reportId diferente do recurso acessado.


Segurança#

ProteçãoDetalhe
OTP hashingSHA-256 — código nunca armazenado em plaintext
Comparação de tokensTiming-safe (evita timing attacks)
Token bound ao relatório reportId no payload — token de um relatório não serve para outro
Token bound ao intent Token edit não aceito em PUT /reports/:id
Expiração curta para saveDefault 5 minutos — reduz janela de uso indevido
Segredo persistente Configurar EDIT_TOKEN_SECRET para que tokens sobrevivam a reinícios
Auditoria Eventos edit_verify_requested , edit_identity_rejected , edit_otp_sent , edit_otp_failed (ver Segurança → Auditoria )