Pular para conteúdo

0017 — Metabase signed embedding (supersede o "BI = só link" da 0014)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-06 · Decidido em: 2026-07-06

Contexto

A decisão 0014 §6 fixou "BI = só link": o auth devolve metabase_dashboard_id e a Central abre o dashboard numa aba — sem embutir. O motivo era evitar superfície: signed embedding exige um embedding secret no env do auth e um iframe no painel.

O trabalho de padronização da Central (authui, .ia/003, L9) pediu o dashboard embutido no detalhe do app (melhor UX que pular pra outra aba). Isso reverte o §6 da 0014 — uma decisão explícita (REGRA Nº 3), registrada aqui.

Decisão

Adotar signed embedding do Metabase, mantendo o link como fallback. Três peças no auth:

  1. MetabaseProvider habilita embedding no dashboard (A2): ao provisionar/reusar, GET o dashboard e, se enable_embedding não é true, faz PUT /api/dashboard/:id <!-- checa-rotas: ignorar --> {enable_embedding:true} (idempotente, atualização parcial). Sem o flag, o Metabase recusa /embed/dashboard/<jwt> com 400. Best-effort e por último (depois dos cards): se o Static Embedding está off na instância (o default do A0), o PUT 400a — a falha é engolida com WARN, não derruba a provisão. Degrada pra "dashboard com cards, sem embed" (a Central usa o link simples), nunca "dashboard vazio". Antes o enable_embedding rodava antes dos cards e era fatal → numa instância sem A0 a provisão estourava com o dashboard já criado e vazio.
  2. MetabaseEmbedSigner minta a URL de embed assinada (A2): JWT HS256 com o METABASE_EMBEDDING_SECRET (payload {resource:{dashboard:<id>}, params:{}, exp}), no mesmo padrão do token forjado do Aptabase; a URL é <metabase>/embed/dashboard/<jwt>#bordered=true&titled=true, mintada em read-time (TTL curto, default 10 min) pelo AppCatalogController e exposta em AppLinks.metabase_embed, só quando o provider metabase está ligada.
  3. Links contextuais resolvidos server-side (A1): o AppCatalogController também resolve AppLinks.glitchtip (<glitchtip>/<org>/<slug>) e AppLinks.metabase (<metabase>/dashboard/<id>) dos refs persistidos — a UI não deriva URL de slug/repo (o DSN do GlitchTip não carrega o slug).

Graceful sem A0: enquanto o METABASE_EMBEDDING_SECRET não estiver no env, o signer devolve empty → metabase_embed é null → a Central cai no link simples. Ou seja, este código é deployável antes do A0 (habilitar signed embedding no Metabase + prover o secret): acende sozinho quando o secret aparecer.

Justificativa da superfície (que a 0014 evitava)

  • Embedding secret no env do auth: é do mesmo tipo do METABASE_API_KEY e do APTABASE_AUTH_SECRET que o auth já guarda (secrets de provider, cifrados no Coolify) — não é uma classe nova de risco. O JWT de embed é read-only, escopado a um dashboard, com TTL curto (≠ o api-key de escrita).
  • Iframe no painel admin: o embed roda só no painel admin (/api/admin/apps, atrás do AdminAuthFilter + JWT xadm_admin), não numa superfície pública. O app do usuário final não embeda (continua no link).
  • Só-web: o HtmlElementView (iframe) é do lado authui, condicional a kIsWeb (fallback link fora do web). O auth só entrega a URL.

Consequências / riscos

  • Verificação real pendente do A0. O enable_embedding PUT e a URL assinada não foram exercitados contra um Metabase real (exige o A0: signed embedding habilitado + o secret). O contrato do payload de embed (resource/params/exp, HS256) segue a doc do Metabase; reconferir ao habilitar o A0. Cobertura atual: unit test do signer (assinatura + graceful sem secret) + WireMock do enable_embedding PUT (habilita quando ausente e degrada não-fatal quando a instância recusa com 400, com os cards presentes) + teste de resolução de links (A1).
  • Rotação do embedding secret invalida as URLs em voo (TTL curto → impacto pequeno).
  • Versão do Metabase: signed embedding é estável há muitas versões; o guard de versão da 0014 (API keys ≥ 0.49) já cobre o mínimo.

Operar (config no recurso auth)

Além dos envs da 0014:

Variável Valor Secret?
METABASE_EMBEDDING_SECRET Admin → Settings → Embedding → Static embedding → Embedding secret key sim ✅
METABASE_EMBED_TTL_SECONDS TTL da URL assinada (default 600) não

A0 (infra, uma vez): Admin → Settings → Embedding → habilitar Static embedding e copiar a secret key pro env do auth. Sem isso, a Central usa o link simples (graceful).

Alternativas consideradas

  • Manter "só link" (0014): pula pra outra aba — UX pior; o pedido L9 é embutir.
  • Public embedding (dashboard público, sem assinatura): expõe os dados sem controle — recusado.
  • Interactive / SSO embedding: fora de escopo (mais integração; o static embedding basta).
  • Mintar a URL no provision (persistida): o JWT tem exp — persistir venceria. Mintar em read-time resolve.