Passa al contenuto principale

Incorporare una dashboard

Incorporare è il momento in cui una dashboard smette di essere qualcosa che il tuo team apre e diventa parte di un prodotto che i tuoi clienti usano. Vibedasher ha due modi per farlo, e la differenza non è cosmetica — decide chi può leggere i dati.

Corsia pubblicaCorsia a token firmato
view_mode della dashboardpublicprotected
Chi può visualizzarechiunque abbia l'URLsolo un visualizzatore per cui il tuo backend ha generato un token
Filtraggio per riga per singolo visualizzatorenosì, applicato lato server
Cosa incorporiun URLun URL più un token di breve durata

Una dashboard private non è incorporabile da nessuna delle due corsie. Entrambe le corsie richiedono che la dashboard sia prima pubblicata — vedi Pubblica e condividi.

Incorporare è una delle due risposte

Incorporare mette la nostra dashboard renderizzata dentro un rettangolo della tua pagina. L'altra risposta è espellere — prendere i sorgenti React e SQL e renderizzarli tu, in modo nativo, con noi fuori dal percorso di rendering e solo il motore dati sottostante. Quale delle due vuoi lo decidono soprattutto due domande: visualizzatori diversi devono vedere righe diverse (incorporare è la sola corsia che lo fa), e la dashboard deve avere l'aspetto e il comportamento del resto della tua app (solo l'espulsione te lo dà). I compromessi sono messi a confronto in Incorporare o espellere.

La corsia pubblica​

Se la dashboard è pubblicata con view_mode: public, un iframe è l'intera integrazione:

<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 sembra allarmante accanto ad allow-scripts ed è sicuro qui specificamente perché il frame è cross-origin rispetto alla tua pagina: concede al renderer la propria origine, non la tua. allow-top-navigation è deliberatamente assente, quindi il frame non può far navigare via la tua pagina. allow-downloads è ciò che fa funzionare l'esportazione PNG dei grafici.

Lo snippet funziona da qualunque pagina tu controlli — dalla nostra parte non c'è nulla da configurare prima. L'audience sotto cui pubblichi è il controllo che conta qui, e Il confine del frame è dove quella scelta viene esposta.

La finestra di dialogo Condividi nell'app ti fornisce questo snippet già precompilato. Prendilo da lì invece di assemblare l'URL a mano.

La corsia a token firmato​

Questa è quella che vale la pena capire, perché è l'unico posto in cui Vibedasher applica la sicurezza per riga.

Il tuo backend genera il token — tu non firmi nulla​

Vibedasher firma il token stesso, ES256, con una propria chiave rotante. Il tuo backend ne richiede uno con la sua chiave 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,
});
Non esiste alcun "embed secret"

Non possiedi alcun materiale di firma e non ne hai bisogno — genera i token come mostrato qui.

Oppure in HTTP semplice, da qualsiasi linguaggio:

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}

Il corpo della richiesta accetta:

CampoObbligatorioNote
vizIdsìEsattamente una dashboard. Un token non può coprire una cartella o un account.
filtersnoI filtri per riga. Vedi sotto.
viewerIdnoUn id di posto opaco. Solo per audit e metering — non è un input di controllo degli accessi. Non farci affidamento per delimitare nulla.
ttlSecondsnoPredefinito 600. Minimo 60, massimo rigido 86 400 (24 h). I valori fuori intervallo vengono limitati alla generazione e riverificati alla verifica. La verifica ammette un piccolo margine di scarto dell'orologio oltre exp, limitato a 120 s e alla metà della durata di vita del token stesso: la durata effettivamente applicata non supera mai 1,5× expires_in e, da un TTL di 240 s in su, il margine è il valore fisso di 120 s.
apiKeyIdnoNecessario solo quando si genera da una sessione browser, per vincolare il token alla lista consentita di domini di una chiave e alla sua revoca.

L'account_id viene impresso dalla tua credenziale e non viene mai letto dal corpo.

La generazione fallisce con un messaggio che nomina il controllo fallito — stai generando un token, non presentandolo, quindi l'errore ti dice cosa cambiare nella dashboard o nella chiave:

StatoDettaglioRimedio
403la visualizzazione appartiene a un altro accountgenera con una chiave API dell'account proprietario
403la visualizzazione non è incorporabile con un token: la sua visibilità deve essere protectedimposta la visibilità della dashboard su Protetta nella finestra Condividi
404la visualizzazione non è pubblicatapubblicala prima
403nessuna chiave API utilizzabile a cui legare il tokenla chiave manca, è inattiva, scaduta o appartiene a un altro account
404Viz non trovatacontrolla vizId

Una dashboard public viene rifiutata da questa corsia — ha già la corsia anonima, e mescolare le due sarebbe un modo per credere di avere filtri per riga quando non è così.

Filtri per riga​

filters è una mappa da nome di colonna a uno scalare o una lista. Uno scalare diventa =, una lista diventa IN, e più chiavi sono combinate con AND. Le chiavi sono il sqlName della colonna così come lo riporta GET /v1/datasets/{id} — il nome che la vista SQL espone, non il nome visualizzato.

La piattaforma non aggiunge una clausola WHERE alla tua query; riscrive ogni riferimento a un dataset in una relazione filtrata, mantenendo l'alias originale:

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

È questo che rende l'ambito non allargabile, e non semplicemente presente. Ciò che la pagina chiede si applica all'esterno della relazione filtrata, quindi può solo restringerla ulteriormente — WHERE tenant = 'globex' OR 1=1 vede comunque un solo tenant, una sottoquery avvolgente avvolge la relazione filtrata, entrambi i rami di una UNION vengono riscritti, e una CTE che maschera l'alias, un riferimento qualificato da schema e qualsiasi funzione che legga file sono rifiutati senza appello.

Tre proprietà lo rendono verificabile:

  • I filtri provengono solo dal token verificato. Non vengono mai letti dal corpo della richiesta, quindi uno spettatore che curiosa nella scheda di rete non può allargare il proprio ambito.
  • Una chiave di filtro che non nomina alcuna colonna dei dataset della dashboard viene rifiutata all'emissione — 403, nominando la chiave ed elencando i nomi di colonna che esistono davvero. Nessun token viene firmato. Lo stesso vale per un valore che non può essere convertito nel tipo della sua colonna. Un filtro che corrispondesse silenziosamente a tutto sarebbe una fuga di dati travestita da controllo di sicurezza: viene quindi rifiutato prima di poter diventare un link che consegni a un cliente.
  • L'emissione ti dice cosa permette il token, nel campo scope della sua risposta. Leggilo; non darlo per scontato.

Leggi scope.fullyScoped — non significa «i filtri sono stati accettati»​

Una dashboard può leggere più di un dataset, e un filtro può applicarsi solo a un dataset che possiede quella colonna — non si può filtrare su una colonna che una tabella non ha. Così una dashboard su orders e price_list, con ambito {"tenant": "acme"}, filtra orders e legge price_list per intero. La pagina si carica, ogni riquadro si disegna, ogni numero è plausibile, e nella richiesta non c'è nulla che appaia sbagliato.

È corretto e voluto quando il secondo dataset è una tabella di lookup condivisa — un listino prezzi, un catalogo di piani, un insieme di etichette. Non è ciò che vuoi quando il secondo dataset contiene righe per cliente sotto una colonna con un nome diverso (tenant_id invece di tenant, customer, org). La risposta dell'emissione è dove scopri in quale dei due casi ti trovi:

{
"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. ..."
}
}
CampoCosa farne
fullyScopedQuello su cui asserire. true solo quando ogni dataset che la dashboard legge è delimitato da questi filtri. Tutto il resto è false.
filtered[].predicateL'SQL esatto applicato a quel dataset. È la frase da mettere davanti a un revisore di sicurezza.
unfiltered[]Dataset che questo token legge per intero. Decidi, per ciascuno, se sia una tabella condivisa o una falla.
unresolvedDatasetIdsDataset il cui schema non è stato leggibile. Non è la stessa cosa di «non filtrato»: «legge tutto» e «non sappiamo cosa legga» sono risposte diverse, e nessuna viene fusa nell'altra.

Un controllo di produzione ragionevole nella tua route di emissione è rifiutare di restituire il token quando scope.fullyScoped è false e ti aspettavi true — tu sai quali delle tue dashboard debbano essere per cliente, la piattaforma no.

Presenta il token​

Il frame non può impostare un header, quindi il token viaggia sull'URL:

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

Le risposte di embed vengono inviate con Referrer-Policy: no-referrer in modo che il token non trapeli in un header Referer sulle sotto-richieste. Resta comunque una credenziale bearer in un URL, che è la vera ragione per cui il TTL predefinito è di dieci minuti: genera un token per ogni caricamento di pagina, tienilo breve, e rinnovalo invece di alzare il TTL.

Configura la tua lista consentita di domini​

Una chiave API può portare con sé un elenco di domini da cui i suoi token di embed saranno onorati. Un elenco vuoto significa consenti-tutto. È una decisione deliberata affinché la funzionalità funzioni nel momento in cui la provi, e significa che una chiave non configurata non protegge nulla a livello di origine. Imposta allowed_domains sulla chiave prima di andare in produzione.

Il ciclo di rinnovo del token​

Un token di dieci minuti su una dashboard che qualcuno lascia aperta tutto il pomeriggio deve essere rinnovato, e il renderer ti dice quando:

  1. Il frame invia EMBED_TOKEN_EXPIRED (o EMBED_TOKEN_INVALID) alla tua pagina.
  2. La tua pagina chiede al tuo backend un nuovo token.
  3. La tua pagina invia UPDATE_TOKEN nel 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');
}
});

Controlla sempre e.origin. UPDATE_TOKEN è l'unico messaggio che un host di terze parti può inviare verso l'interno — tutto il resto viene ignorato.

Cosa riceve realmente la tua pagina​

I messaggi che un host di terze parti riceve sono un elenco fisso e consentito di tredici. Questo è l'elenco completo:

MessaggioQuando
READYIl renderer si è avviato.
LOADEDLa dashboard ha finito di caricarsi.
RESIZEL'altezza del contenuto è cambiata — usalo per dimensionare l'iframe.
BUNDLE_STARTED / BUNDLE_COMPLETED / BUNDLE_FAILED / BUNDLE_RETRYCiclo di vita della build del codice della dashboard.
RUNTIME_ERRORIl codice della dashboard ha generato un'eccezione.
EMPTY_DATAUna query non ha restituito righe.
RENDER_HEALTHStato di salute del rendering per singolo tile.
EMBED_TOKEN_EXPIRED / EMBED_TOKEN_INVALID / EMBED_UNAVAILABLECiclo di vita del token — vedi sopra.

Qualsiasi altra cosa tu possa vedere menzionata è interna. In particolare un insieme di messaggi non viene esplicitamente mai inoltrato a un host di terze parti — VIZ_DATA, FILES, UPDATE_TOKEN in uscita, SQL_ERROR, CONSOLE_ERROR, REQUEST_NEW_AUTH_TOKEN, ELEMENT_SELECTED, FILTER_STATE_CHANGED — perché portano o il sorgente della tua dashboard o il suo stato grezzo di query.

Il confine del frame​

Due proprietà decidono su cosa può essere costruita un'integrazione, ed entrambe vale la pena leggerle prima di progettare attorno al frame, non dopo.

Il frame possiede il proprio stato di filtraggio. I messaggi che un host terzo riceve sono quelli elencati sopra; lo stato di filtraggio della dashboard non è fra loro, in nessuna delle due direzioni. Il frame è una vista autonoma: guidalo con l'URL e il token che emetti, e lascia che possieda ciò che accade all'interno. Se la tua interfaccia deve condividere una selezione con la dashboard, metti quella selezione nel predicato di filtraggio del token, che la piattaforma aggiunge a ogni query che autorizza — lato server, e con la stessa risposta per ogni tile.

Scegli l'audience pensando al framing. Una dashboard pubblicata come public è un URL pubblico, con tutto quello che ne segue; la corsia a token firmato è quella da usare quando conta chi guarda e da dove, e l'elenco dei domini consentiti di una chiave API è ciò che lega le sue risposte alla tua origine.

La corsia di query a token firmato gira in eu-central-1. È lì che vive il runtime di query per gli embed protetti. Se i dati del tuo account sono in un'altra regione, parlane con noi prima di costruire su questa corsia.

Passi successivi​

  • Chi può vedere cosa — dove si inserisce il filtraggio per riga nel modello di permessi più ampio, e perché esiste solo qui.
  • Pubblica e condividi — portare una dashboard a published e scegliere il suo view_mode.
  • Riferimento API — l'operazione embed-tokens per intero.