Skip to content

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:

json
{
  "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:

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

Response 200:

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

Erros:

StatusErroQuando
400invalid_emailEmail inválido
400validation_errorreportId ausente
400invalid_intentintent diferente de "edit" ou "save"
400policy_disabledEditPolicy não habilitada no servidor
403identity_not_allowedEmail/domínio não permitido por EDIT_ALLOWED_IDENTITIES
404not_foundRelatório não encontrado
429rate_limitedCódigo já enviado recentemente
503not_configuredOTP service não configurado (sem email provider)
503service_unavailableFalha 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:

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

Response 200:

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

Erros:

StatusErroQuando
400invalid_codeCódigo incorreto
400otp_not_foundNenhum código pendente para este email/relatório
400policy_disabledEditPolicy não habilitada
403identity_not_allowedEmail/domínio não permitido
404not_foundRelatório não encontrado
410expiredCódigo expirado
423max_attemptsTentativas 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
401save_token_requiredHeader X-Edit-Token ausente
403save_token_invalidToken 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_EXPIRATION3600TTL do token de edição em segundos
SAVE_TOKEN_EXPIRATION300TTL do token de salvamento em segundos
EDIT_OTP_EXPIRATION300Expiração do código OTP em segundos
EDIT_OTP_MAX_ATTEMPTS3Tentativas máximas antes de invalidar
EDIT_OTP_LENGTH6Comprimento do código OTP numérico

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

Exemplo de configuração

bash
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

bash
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

bash
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

bash
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":

bash
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

bash
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:

json
{
  "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órioreportId no payload — token de um relatório não serve para outro
Token bound ao intentToken edit não aceito em PUT /reports/:id
Expiração curta para saveDefault 5 minutos — reduz janela de uso indevido
Segredo persistenteConfigurar EDIT_TOKEN_SECRET para que tokens sobrevivam a reinícios
AuditoriaEventos edit_verify_requested, edit_identity_rejected, edit_otp_sent, edit_otp_failed (ver Segurança → Auditoria)

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro