Ein Dashboard einbetten
Einbetten ist der Punkt, an dem ein Dashboard aufhört, etwas zu sein, das Ihr Team öffnet, und Teil eines Produkts wird, das Ihre Kunden nutzen. Vibedasher bietet zwei Wege dafür, und der Unterschied ist nicht kosmetisch — er entscheidet, wer die Daten lesen kann.
| Öffentliche Spur | Spur mit signiertem Token | |
|---|---|---|
Dashboard-view_mode | public | protected |
| Wer sehen kann | jeder mit der URL | nur ein Betrachter, für den Ihr Backend ein Token ausgestellt hat |
| Filterung pro Betrachter auf Zeilenebene | nein | ja, serverseitig durchgesetzt |
| Was Sie einbetten | eine URL | eine URL plus ein kurzlebiges Token |
Ein private-Dashboard ist über keine der beiden Spuren einbettbar. Beide Spuren
erfordern, dass das Dashboard zuerst veröffentlicht ist — siehe
Veröffentlichen und teilen.
Einbetten legt unser gerendertes Dashboard in ein Rechteck auf Ihrer Seite. Die andere Antwort ist Eject — Sie nehmen den React- und SQL-Quellcode und rendern ihn selbst, nativ, mit uns außerhalb des Render-Pfads und nur der Daten-Engine darunter. Welche der beiden Sie wollen, entscheiden vor allem zwei Fragen: Müssen verschiedene Betrachter verschiedene Zeilen sehen (Einbetten ist die einzige Spur, die das kann), und muss das Dashboard aussehen und sich verhalten wie der Rest Ihrer App (nur Auswerfen bringt Ihnen das). Die Abwägungen stehen nebeneinander in Einbetten oder auswerfen.
Die öffentliche Spur
Wenn das Dashboard mit view_mode: public veröffentlicht ist, ist ein Iframe die gesamte
Integration:
<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 wirkt neben allow-scripts alarmierend, ist hier aber gerade deshalb
sicher, weil der Frame cross-origin zu Ihrer Seite ist: Es gewährt dem Renderer seine
eigene Origin, nicht Ihre. allow-top-navigation fehlt bewusst, sodass der Frame Ihre
Seite nicht wegnavigieren kann. allow-downloads ist das, was den PNG-Export von
Diagrammen ermöglicht.
Das Snippet funktioniert von jeder Seite, die Sie kontrollieren — auf unserer Seite muss zuvor nichts konfiguriert werden. Die Zielgruppe, unter der Sie veröffentlichen, ist hier die entscheidende Kontrolle, und Die Grenze des Rahmens legt diese Wahl dar.
Der Freigabedialog in der App liefert Ihnen dieses Snippet vorausgefüllt. Holen Sie es von dort, statt die URL selbst zusammenzubauen.
Die Spur mit signiertem Token
Das ist die, die es sich zu verstehen lohnt, weil sie der einzige Ort ist, an dem Vibedasher zeilenbasierte Sicherheit umsetzt.
Ihr Backend stellt das Token aus — Sie signieren nichts selbst
Vibedasher signiert das Token selbst, ES256, mit seinem eigenen rotierenden Schlüssel. Ihr Backend fordert eines mit seinem API-Schlüssel an:
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,
});
Sie halten kein Signaturmaterial und brauchen keines — stellen Sie Tokens wie hier gezeigt aus.
Oder mit reinem HTTP, aus jeder Sprache:
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}
Der Anfragekörper nimmt:
| Feld | Erforderlich | Hinweise |
|---|---|---|
vizId | ja | Genau ein Dashboard. Ein Token kann keinen Ordner oder ein Konto abdecken. |
filters | nein | Die Zeilenfilter. Siehe unten. |
viewerId | nein | Eine undurchsichtige Sitz-ID. Nur für Audit und Messung — kein Eingabewert für die Zugriffskontrolle. Verlassen Sie sich nicht darauf, um irgendetwas einzuschränken. |
ttlSeconds | nein | Standard 600. Untergrenze 60, harte Obergrenze 86 400 (24 h). Werte außerhalb des Bereichs werden beim Ausstellen begrenzt und bei der Verifizierung erneut geprüft. Die Verifizierung erlaubt einen kleinen Zeitversatz-Spielraum über exp hinaus, begrenzt auf 120 s und auf die Hälfte der Lebensdauer des Tokens selbst — die tatsächlich durchgesetzte Lebensdauer beträgt damit nie mehr als das 1,5-Fache von expires_in, und ab einer TTL von 240 s ist der Spielraum die feste Obergrenze von 120 s. |
apiKeyId | nein | Nur nötig, wenn aus einer Browser-Sitzung heraus ausgestellt wird, um das Token an die Domain-Zulassungsliste und den Widerruf eines Schlüssels zu binden. |
Die account_id wird aus Ihrem Zugangsdatum entnommen und nie aus dem Körper gelesen.
Das Ausstellen schlägt mit einer Meldung fehl, die die gescheiterte Prüfung benennt — Sie stellen ein Token aus, Sie präsentieren keines, also sagt Ihnen der Fehler, was am Dashboard oder am Schlüssel zu ändern ist:
| Status | Detail | Abhilfe |
|---|---|---|
403 | die Visualisierung gehört einem anderen Konto | mit einem API-Schlüssel des besitzenden Kontos ausstellen |
403 | die Visualisierung kann nicht per Token eingebettet werden: ihre Sichtbarkeit muss protected sein | die Sichtbarkeit des Dashboards im Freigabedialog auf Geschützt stellen |
404 | die Visualisierung ist nicht veröffentlicht | zuerst veröffentlichen |
403 | kein verwendbarer API-Schlüssel, an den das Token gebunden werden kann | der Schlüssel fehlt, ist inaktiv, abgelaufen oder gehört einem anderen Konto |
404 | Viz nicht gefunden | vizId prüfen |
Ein public-Dashboard wird von dieser Spur abgelehnt — es hat bereits die anonyme
Spur, und beide zu mischen wäre ein Weg, Zeilenfilter zu vermuten, wo keine sind.
Zeilenfilter
filters ist eine Zuordnung von Spaltenname zu einem Skalar oder einer Liste. Ein Skalar
wird zu =, eine Liste zu IN, und mehrere Schlüssel werden mit UND verknüpft. Die
Schlüssel sind der sqlName der Spalte, wie ihn GET /v1/datasets/{id} meldet — der Name,
den die SQL-Sicht offenlegt, nicht der Anzeigename.
Die Plattform hängt keine WHERE-Klausel an Ihre Abfrage an; sie schreibt jede
Datensatz-Referenz in eine gefilterte Relation um und behält den ursprünglichen Alias:
FROM orders
-> FROM (SELECT * FROM "orders" WHERE "tenant" = 'acme') AS orders
Das macht den Geltungsbereich nicht erweiterbar und nicht bloß vorhanden. Was die Seite
anfordert, greift außerhalb der gefilterten Relation, kann sie also nur weiter einengen —
WHERE tenant = 'globex' OR 1=1 sieht weiterhin genau einen Mandanten, eine umschließende
Unterabfrage umschließt die gefilterte Relation, beide Zweige eines UNION werden
umgeschrieben, und eine CTE, die den Alias überdeckt, eine schemaqualifizierte Referenz
sowie jede dateilesende Funktion werden rundweg abgelehnt.
Drei Eigenschaften machen das prüfbar:
- Die Filter stammen ausschließlich aus dem verifizierten Token. Sie werden nie aus dem Request-Body gelesen, also kann ein Betrachter, der im Netzwerk-Tab stöbert, seinen eigenen Geltungsbereich nicht erweitern.
- Ein Filterschlüssel, der keine Spalte der Datensätze des Dashboards benennt, wird bei der
Ausstellung abgelehnt —
403, unter Nennung des Schlüssels und Auflistung der tatsächlich vorhandenen Spaltennamen. Es wird kein Token signiert. Dasselbe gilt für einen Wert, der sich nicht in den Typ seiner Spalte überführen lässt. Ein Filter, der stillschweigend auf alles passt, wäre ein Datenleck im Gewand einer Sicherheitskontrolle — er wird abgelehnt, bevor er zu einem Link werden kann, den Sie einem Kunden aushändigen. - Die Ausstellung sagt Ihnen, was das Token erlaubt, im Feld
scopeihrer Antwort. Lesen Sie es; nehmen Sie es nicht an.
Lesen Sie scope.fullyScoped — es heißt nicht „die Filter wurden akzeptiert"
Ein Dashboard kann mehr als einen Datensatz lesen, und ein Filter kann nur auf einen
Datensatz wirken, der diese Spalte besitzt — man kann nicht nach einer Spalte filtern,
die eine Tabelle nicht hat. Ein Dashboard über orders und price_list mit dem
Geltungsbereich {"tenant": "acme"} filtert also orders und liest price_list
vollständig. Die Seite lädt, jede Kachel zeichnet, jede Zahl ist plausibel, und an der
Anfrage wirkt nichts falsch.
Das ist richtig und beabsichtigt, wenn der zweite Datensatz eine gemeinsam genutzte
Nachschlagetabelle ist — eine Preisliste, ein Tarifkatalog, ein Satz Bezeichnungen. Es ist
nicht das, was Sie wollen, wenn der zweite Datensatz kundenbezogene Zeilen unter einer
anders benannten Spalte führt (tenant_id statt tenant, customer, org). In der Antwort
der Ausstellung erfahren Sie, in welchem der beiden Fälle Sie sind:
{
"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. ..."
}
}
| Feld | Was damit zu tun ist |
|---|---|
fullyScoped | Das Feld, auf das man prüft. true nur, wenn jeder Datensatz, den das Dashboard liest, durch diese Filter eingegrenzt ist. Alles andere ist false. |
filtered[].predicate | Das exakte SQL, das auf diesen Datensatz angewendet wird. Das ist der Satz, den man einem Sicherheitsprüfer vorlegt. |
unfiltered[] | Datensätze, die dieses Token vollständig liest. Entscheiden Sie je Datensatz, ob das eine gemeinsame Nachschlagetabelle ist oder eine Lücke. |
unresolvedDatasetIds | Datensätze, deren Schema nicht gelesen werden konnte. Nicht dasselbe wie ungefiltert: „liest alles" und „wir wissen nicht, was dies liest" sind verschiedene Antworten, und keine wird in die andere überführt. |
Eine vernünftige Produktionsprüfung in Ihrer Ausstellungs-Route ist, das Token nicht
zurückzugeben, wenn scope.fullyScoped false ist und Sie true erwartet haben — Sie
wissen, welche Ihrer Dashboards kundenbezogen sein sollen, die Plattform nicht.
Das Token präsentieren
Der Frame kann keinen Header setzen, also reist das Token in der URL mit:
<iframe
src="https://cloud.vibedasher.com/v/<slug>?embed_token=<token>"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allowfullscreen
></iframe>
Embed-Antworten werden mit Referrer-Policy: no-referrer gesendet, damit das Token nicht
über einen Referer-Header bei Unteranfragen durchsickert. Es bleibt dennoch ein
Bearer-Zugangsdatum in einer URL, was der eigentliche Grund ist, warum die Standard-TTL
zehn Minuten beträgt: pro Seitenaufruf ausstellen, kurz halten und aktualisieren, statt die
TTL zu erhöhen.
Ihre Domain-Zulassungsliste einrichten
Ein API-Schlüssel kann eine Liste von Domains tragen, von denen aus seine Embed-Tokens
respektiert werden. Eine leere Liste bedeutet Alles-erlaubt. Das ist eine bewusste
Entscheidung, damit die Funktion in dem Moment funktioniert, in dem Sie sie ausprobieren,
und es bedeutet, dass ein nicht konfigurierter Schlüssel auf Origin-Ebene nichts schützt.
Setzen Sie allowed_domains auf dem Schlüssel, bevor Sie in Produktion gehen.
Die Token-Aktualisierungsschleife
Ein Zehn-Minuten-Token auf einem Dashboard, das jemand den ganzen Nachmittag offen lässt, muss aktualisiert werden, und der Renderer sagt Ihnen, wann:
- Der Frame sendet
EMBED_TOKEN_EXPIRED(oderEMBED_TOKEN_INVALID) an Ihre Seite. - Ihre Seite fragt Ihr Backend nach einem frischen Token.
- Ihre Seite sendet
UPDATE_TOKENin den 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');
}
});
Prüfen Sie immer e.origin. UPDATE_TOKEN ist die einzige Nachricht, die ein
Drittanbieter-Host nach innen senden darf — alles andere wird ignoriert.
Was Ihre Seite tatsächlich empfängt
Die Nachrichten, die ein Drittanbieter-Host erhält, sind eine feste Zulassungsliste von dreizehn. Das ist die vollständige Liste:
| Nachricht | Wann |
|---|---|
READY | Der Renderer ist gestartet. |
LOADED | Das Dashboard hat das Laden abgeschlossen. |
RESIZE | Die Inhaltshöhe hat sich geändert — verwenden Sie es, um das Iframe zu dimensionieren. |
BUNDLE_STARTED / BUNDLE_COMPLETED / BUNDLE_FAILED / BUNDLE_RETRY | Build-Lebenszyklus des Dashboard-Codes. |
RUNTIME_ERROR | Der Dashboard-Code hat einen Fehler geworfen. |
EMPTY_DATA | Eine Abfrage hat keine Zeilen zurückgegeben. |
RENDER_HEALTH | Render-Gesundheit pro Kachel. |
EMBED_TOKEN_EXPIRED / EMBED_TOKEN_INVALID / EMBED_UNAVAILABLE | Token-Lebenszyklus — siehe oben. |
Alles andere, das Sie referenziert sehen, ist intern. Insbesondere wird eine Reihe von
Nachrichten explizit nie an einen Drittanbieter-Host weitergeleitet — VIZ_DATA,
FILES, UPDATE_TOKEN ausgehend, SQL_ERROR, CONSOLE_ERROR,
REQUEST_NEW_AUTH_TOKEN, ELEMENT_SELECTED, FILTER_STATE_CHANGED — weil sie entweder
den Quellcode Ihres Dashboards oder dessen rohen Abfragezustand tragen.
Die Grenze des Rahmens
Zwei Eigenschaften entscheiden, worauf eine Integration aufbauen kann, und beide sollten Sie lesen, bevor Sie um den Rahmen herum entwerfen, nicht danach.
Der Rahmen besitzt seinen eigenen Filterzustand. Die Nachrichten, die ein Drittanbieter-Host empfängt, sind die oben aufgeführten; der Filterzustand des Dashboards gehört in keiner Richtung dazu. Der Rahmen ist eine in sich geschlossene Ansicht: Steuern Sie ihn über die URL und das Token, das Sie ausstellen, und lassen Sie ihn besitzen, was innen passiert. Wenn Ihre Oberfläche eine Auswahl mit dem Dashboard teilen muss, legen Sie diese Auswahl in das Filterprädikat des Tokens, das die Plattform jeder von ihr autorisierten Abfrage anhängt — serverseitig und mit derselben Antwort für jede Kachel.
Wählen Sie die Zielgruppe mit dem Framing im Blick. Ein als public veröffentlichtes
Dashboard ist eine öffentliche URL, mit allem, was daraus folgt; die Spur mit signiertem
Token ist die richtige, wenn es darauf ankommt, wer schaut und von wo, und die
Domain-Erlaubnisliste eines API-Schlüssels bindet seine Antworten an Ihren Ursprung.
Die Abfragespur mit signiertem Token läuft in eu-central-1. Dort lebt die
Abfrage-Laufzeit für geschützte Embeds. Liegen die Daten Ihres Kontos in einer anderen
Region, sprechen Sie mit uns, bevor Sie auf dieser Spur aufbauen.
Nächste Schritte
- Wer sieht was — wo zeilenbasierte Filterung in das breitere Berechtigungsmodell passt, und warum sie nur hier existiert.
- Veröffentlichen und teilen — ein Dashboard auf
publishedbringen und seinenview_modewählen. - API Reference — die Operation
embed-tokensin voller Länge.