Pular para o conteúdo principal

Incorporando um painel

Incorporar é o ponto em que um painel deixa de ser algo que sua equipe abre e passa a fazer parte de um produto que seus clientes usam. A Vibedasher tem duas formas de fazer isso, e a diferença não é cosmética — ela decide quem pode ler os dados.

Via públicaVia de token assinado
view_mode do painelpublicprotected
Quem pode visualizarqualquer pessoa com a URLsomente um visualizador para quem seu backend gerou um token
Filtragem por linha por visualizadornãosim, aplicada no servidor
O que você incorporauma URLuma URL mais um token de curta duração

Um painel private não é incorporável por nenhuma das duas vias. Ambas as vias exigem que o painel esteja publicado primeiro — veja Publique e compartilhe.

Incorporar é uma das duas respostas

Incorporar coloca o nosso painel renderizado dentro de um retângulo na sua página. A outra resposta é ejetar — pegar o código-fonte React e SQL e renderizá-lo você mesmo, nativamente, com a gente fora do caminho de renderização e apenas o motor de dados por baixo. Qual das duas você quer é decidido principalmente por duas perguntas: visualizadores diferentes precisam ver linhas diferentes (incorporar é a única via que faz isso), e o painel precisa se parecer e se comportar como o resto do seu app (só ejetar te dá isso). As contrapartidas estão lado a lado em Incorporar ou ejetar.

A via pública​

Se o painel é publicado com view_mode: public, um iframe é toda a integração:

<iframe
src="https://cloud.vibedasher.com/v/<slug>"
width="100%"
height="800"
style="border: 0;"
title="Vibedasher dashboard"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
loading="lazy"
></iframe>

allow-same-origin parece alarmante ao lado de allow-scripts e é seguro aqui especificamente porque o frame é cross-origin em relação à sua página: ele concede ao renderizador sua própria origem, não a sua. allow-top-navigation está deliberadamente ausente, então o frame não pode navegar sua página para outro lugar. allow-downloads é o que faz a exportação de gráfico em PNG funcionar.

O trecho funciona a partir de qualquer página que você controle — nada precisa ser configurado do nosso lado antes. A audiência sob a qual você publica é o controle que importa aqui, e A fronteira do frame é onde essa escolha é apresentada.

O diálogo de compartilhamento no app te dá esse trecho já preenchido. Pegue-o de lá em vez de montar a URL manualmente.

A via de token assinado​

Essa é a que vale a pena entender, porque é o único lugar onde a Vibedasher faz segurança por linha.

Seu backend gera o token — você não assina nada​

A Vibedasher assina o token ela mesma, ES256, com sua própria chave rotativa. Seu backend pede um usando sua chave de API:

from vibedasher.models import EmbedTokenRequest, EmbedTokenRequestFilters

resp = client.embed_tokens.create_embed(
EmbedTokenRequest(
viz_id=1234,
filters=EmbedTokenRequestFilters.from_dict({"region": "EMEA"}),
ttl_seconds=600,
)
)
print(resp.token, resp.expires_in)
const resp = await client.embedTokens.createEmbed({
vizId: 1234,
filters: { region: 'EMEA' },
ttlSeconds: 600,
});
Não existe "segredo de incorporação"

Você não guarda nenhum material de assinatura e não precisa de nenhum — emita tokens como mostrado aqui.

Ou com HTTP puro, a partir de qualquer linguagem:

curl -X POST https://api.vibedasher.com/api/v1/embed/token \
-H "X-Api-Key: $VIBEDASHER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"vizId": 1234, "ttlSeconds": 600, "filters": {"region": "EMEA"}}'
# → {"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600}

O corpo da requisição aceita:

CampoObrigatórioNotas
vizIdsimExatamente um painel. Um token não pode cobrir uma pasta ou uma conta.
filtersnãoOs filtros de linha. Veja abaixo.
viewerIdnãoUm id de assento opaco. Apenas auditoria e medição — não é uma entrada de controle de acesso. Não confie nele para delimitar o escopo de nada.
ttlSecondsnãoPadrão 600. Piso 60, teto rígido 86 400 (24 h). Valores fora do intervalo são limitados na geração e reverificados na validação. A validação admite uma pequena margem de desvio de relógio acima de exp, limitada a 120 s e à metade do tempo de vida do próprio token — assim o tempo de vida efetivamente aplicado nunca passa de 1,5× expires_in e, a partir de um TTL de 240 s, a margem é o valor fixo de 120 s.
apiKeyIdnãoNecessário apenas ao gerar a partir de uma sessão de navegador, para vincular o token à lista de domínios permitidos e à revogação de uma chave.

O account_id é carimbado a partir da sua credencial e nunca é lido do corpo.

A geração falha com uma mensagem que nomeia a verificação que falhou — você está emitindo um token, não apresentando um, então o erro diz o que mudar no painel ou na chave:

StatusDetalheCorreção
403a visualização pertence a outra contaemita com uma chave de API da conta proprietária
403a visualização não pode ser incorporada com um token: sua visibilidade precisa ser protectedmude a visibilidade do painel para Protegido no diálogo de compartilhamento
404a visualização não está publicadapublique-a primeiro
403nenhuma chave de API utilizável para vincular o tokena chave está ausente, inativa, expirada ou pertence a outra conta
404Viz não encontradaconfira o vizId

Um painel public é rejeitado por esta via — ele já tem a via anônima, e misturar as duas seria um jeito de achar que você tem filtros de linha quando não tem.

Filtros de linha​

filters é um mapa de nome de coluna para um escalar ou uma lista. Um escalar vira =, uma lista vira IN, e várias chaves são combinadas com E. As chaves são o sqlName da coluna como GET /v1/datasets/{id} o informa — o nome que a view SQL expõe, não o nome de exibição.

A plataforma não acrescenta uma cláusula WHERE à sua consulta; ela reescreve cada referência a um conjunto de dados como uma relação filtrada, preservando o alias original:

FROM orders
-> FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders

É isso que torna o escopo não ampliável, e não apenas presente. O que a página pedir se aplica fora da relação filtrada, então só pode estreitá-la ainda mais — WHERE tenant = 'globex' OR 1=1 continua vendo um único inquilino, uma subconsulta envolvente envolve a relação filtrada, os dois braços de um UNION são reescritos, e uma CTE que sombreia o alias, uma referência qualificada por esquema e qualquer função que leia arquivos são recusadas de imediato.

Três propriedades tornam isso auditável:

  • Os filtros vêm apenas do token verificado. Nunca são lidos do corpo da requisição, então um visualizador fuçando na aba de rede não consegue ampliar o próprio escopo.
  • Uma chave de filtro que não nomeia nenhuma coluna dos conjuntos de dados do painel é recusada na emissão — 403, nomeando a chave e listando os nomes de coluna que existem. Nenhum token é assinado. O mesmo vale para um valor que não possa ser convertido para o tipo da sua coluna. Um filtro que silenciosamente correspondesse a tudo seria um vazamento de dados vestido de controle de segurança, então é recusado antes de virar um link que você entrega a um cliente.
  • A emissão diz o que o token permite, no campo scope da resposta. Leia; não presuma.

Leia scope.fullyScoped — não é «os filtros foram aceitos»​

Um painel pode ler mais de um conjunto de dados, e um filtro só pode se aplicar a um conjunto que tenha aquela coluna — não se filtra por uma coluna que a tabela não tem. Assim, um painel sobre orders e price_list, com escopo {"tenant": "acme"}, filtra orders e lê price_list inteiro. A página carrega, cada bloco desenha, cada número é plausível, e nada na requisição parece errado.

Isso é correto e desejado quando o segundo conjunto é uma tabela de consulta compartilhada — uma tabela de preços, um catálogo de planos, um conjunto de rótulos. Não é o que você quer quando o segundo conjunto guarda linhas por cliente sob uma coluna com outro nome (tenant_id em vez de tenant, customer, org). A resposta da emissão é onde você descobre em qual dos casos está:

{
"token": "<ES256 JWT>", "token_type": "embed", "expires_in": 600,
"scope": {
"vizId": 1234,
"filtered": [{"datasetId": 7, "datasetName": "orders",
"predicate": "\"tenant\" = 'acme'", "columns": ["tenant"]}],
"unfiltered": [{"datasetId": 8, "datasetName": "price_list",
"predicate": null, "columns": []}],
"unresolvedDatasetIds": [],
"fullyScoped": false,
"meaning": "PARTIALLY SCOPED — read this before handing the link out. ..."
}
}
CampoO que fazer com ele
fullyScopedO campo em que se deve asseverar. true somente quando todo conjunto de dados que o painel lê está delimitado por esses filtros. Qualquer outra coisa é false.
filtered[].predicateO SQL exato aplicado àquele conjunto. É a frase para colocar diante de um revisor de segurança.
unfiltered[]Conjuntos que este token lê inteiros. Decida, para cada um, se é uma tabela compartilhada ou uma brecha.
unresolvedDatasetIdsConjuntos cujo esquema não pôde ser lido. Não é o mesmo que não filtrado: «lê tudo» e «não sabemos o que isto lê» são respostas diferentes, e nenhuma é fundida na outra.

Uma verificação de produção razoável na sua rota de emissão é recusar devolver o token quando scope.fullyScoped for false e você esperava true — você sabe quais dos seus painéis devem ser por cliente, e a plataforma não.

Apresente o token​

O frame não pode definir um cabeçalho, então o token viaja na URL:

<iframe
src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
></iframe>

Respostas de incorporação são enviadas com Referrer-Policy: no-referrer para que o token não vaze em um cabeçalho Referer em sub-requisições. Ainda assim é uma credencial bearer em uma URL, que é a razão real pela qual o TTL padrão é de dez minutos: gere um por carregamento de página, mantenha-o curto, e renove-o em vez de aumentar o TTL.

Configure sua lista de domínios permitidos​

Uma chave de API pode carregar uma lista de domínios a partir dos quais seus tokens de incorporação serão honrados. Uma lista vazia significa permitir tudo. Essa é uma decisão deliberada para que o recurso funcione no momento em que você o testa, e significa que uma chave não configurada não protege nada no nível de origem. Configure allowed_domains na chave antes de ir para produção.

O ciclo de renovação de token​

Um token de dez minutos em um painel que alguém deixa aberto a tarde toda precisa ser renovado, e o renderizador te avisa quando:

  1. O frame envia EMBED_TOKEN_EXPIRED (ou EMBED_TOKEN_INVALID) para sua página.
  2. Sua página pede ao seu backend um token novo.
  3. Sua página envia UPDATE_TOKEN para dentro do frame: { type: 'UPDATE_TOKEN', token: '<new>' }.
window.addEventListener('message', async (e) => {
if (e.origin !== 'https://cloud.vibedasher.com') return;
if (e.data?.type === 'EMBED_TOKEN_EXPIRED' || e.data?.type === 'EMBED_TOKEN_INVALID') {
const token = await fetch('/api/vibedasher-embed-token').then((r) => r.text());
frame.contentWindow.postMessage({ type: 'UPDATE_TOKEN', token }, 'https://cloud.vibedasher.com');
}
});

Sempre verifique e.origin. UPDATE_TOKEN é a única mensagem que um host terceiro pode enviar para dentro — tudo o mais é ignorado.

O que sua página realmente recebe​

As mensagens que um host terceiro recebe são uma lista fixa e permitida de treze. Esta é a lista completa:

MensagemQuando
READYO renderizador inicializou.
LOADEDO painel terminou de carregar.
RESIZEA altura do conteúdo mudou — use para dimensionar o iframe.
BUNDLE_STARTED / BUNDLE_COMPLETED / BUNDLE_FAILED / BUNDLE_RETRYCiclo de vida do build do código do painel.
RUNTIME_ERRORO código do painel lançou um erro.
EMPTY_DATAUma consulta não retornou linhas.
RENDER_HEALTHSaúde de renderização por tile.
EMBED_TOKEN_EXPIRED / EMBED_TOKEN_INVALID / EMBED_UNAVAILABLECiclo de vida do token — veja acima.

Qualquer outra coisa que você veja referenciada é interna. Em particular, um conjunto de mensagens é explicitamente nunca encaminhado para um host terceiro — VIZ_DATA, FILES, UPDATE_TOKEN de saída, SQL_ERROR, CONSOLE_ERROR, REQUEST_NEW_AUTH_TOKEN, ELEMENT_SELECTED, FILTER_STATE_CHANGED — porque carregam ou o código-fonte do seu painel ou seu estado bruto de consulta.

A fronteira do frame​

Duas propriedades decidem sobre o que uma integração pode ser construída, e vale a pena ler as duas antes de projetar em torno do frame, não depois.

O frame é dono do seu próprio estado de filtro. As mensagens que um host externo recebe são as listadas acima; o estado de filtro do painel não está entre elas, em nenhuma das direções. O frame é uma visão autossuficiente: conduza-o pela URL e pelo token que você emite, e deixe que ele seja dono do que acontece lá dentro. Se a sua interface precisa compartilhar uma seleção com o painel, coloque essa seleção no predicado de filtro do token, que a plataforma anexa a toda consulta que autoriza — no servidor, e com a mesma resposta para cada tile.

Escolha a audiência pensando no enquadramento. Um painel publicado como public é uma URL pública, com tudo o que decorre disso; a via de token assinado é a que se usa quando importa quem está olhando e de onde, e a lista de domínios permitidos de uma chave de API é o que amarra suas respostas à sua origem.

A via de consulta com token assinado roda em eu-central-1. É lá que vive o runtime de consulta dos embeds protegidos. Se os dados da sua conta estão em outra região, fale com a gente antes de construir nessa via.

Próximos passos​