Partager via


Qu’est-ce que l’incorporation pour les utilisateurs externes ?

Important

Cette fonctionnalité est disponible en préversion publique.

Cette page décrit le fonctionnement de l’incorporation pour les utilisateurs externes, la configuration de votre espace de travail Azure Databricks pour le partage sécurisé de tableaux de bord incorporés et l’utilisation d’exemples d’applications pour commencer. L’incorporation pour les utilisateurs externes utilise un principal de service et des jetons d’accès étendus pour authentifier et autoriser l’accès aux tableaux de bord incorporés. Cette approche vous permet de partager des tableaux de bord avec des visionneuses en dehors de votre organisation, tels que des partenaires et des clients, sans provisionner des comptes Azure Databricks pour ces utilisateurs.

Pour en savoir plus sur d’autres options d’incorporation, notamment l’incorporation de tableaux de bord pour les utilisateurs au sein de votre organisation, consultez Incorporer un tableau de bord.

Fonctionnement de l’incorporation pour les utilisateurs externes

Le diagramme et les étapes numérotées qui suivent expliquent comment les utilisateurs sont authentifiés et les tableaux de bord sont remplis avec des résultats délimités par l’utilisateur lorsque vous incorporez un tableau de bord pour les utilisateurs externes.

Graphique de flux montrant les échanges de jetons nécessaires entre votre application et l’espace de travail Databricks.

  1. Authentification et demande de l’utilisateur : L’utilisateur se connecte à votre application. Le serveur frontal de votre application envoie une demande authentifiée à votre serveur pour un jeton d’accès au tableau de bord.
  2. Authentification du principal de service : Votre serveur utilise le secret du principal de service pour demander et recevoir un jeton OAuth du serveur Databricks. Il s’agit d’un jeton largement étendu qui peut appeler toutes les API de tableau de bord auxquelles Azure Databricks a accès pour le compte du principal de service. Votre serveur appelle le point de terminaison à l’aide /tokeninfo de ce jeton, en passant des informations utilisateur de base, telles que external_viewer_id et external_value. Voir Présenter en toute sécurité des tableaux de bord à des utilisateurs individuels.
  3. Génération de jetons délimité par l’utilisateur : À l’aide de la /tokeninfo réponse du point de terminaison et du point de terminaison Databricks OpenID Connect (OIDC), votre serveur génère un nouveau jeton étroitement étendu qui encode les informations utilisateur que vous avez passées.
  4. Rendu du tableau de bord et filtrage des données : La page de l’application instancie DatabricksDashboard@databricks/aibi-client et transmet le jeton délimité par l’utilisateur pendant la construction. Le tableau de bord s’affiche avec le contexte de l’utilisateur. Ce jeton autorise l’accès, prend en charge l’audit avec external_viewer_idet effectue le external_value filtrage des données. Les requêtes dans les jeux de données de tableau de bord peuvent référencer __aibi_external_value pour appliquer des filtres par utilisateur, ce qui garantit que chaque visionneuse voit uniquement les données qu’elles sont autorisées à afficher.

Présenter en toute sécurité des tableaux de bord à des utilisateurs individuels

Configurez votre serveur d’applications pour générer un jeton d’étendue utilisateur unique pour chaque utilisateur en fonction de leur external_viewer_id. Cela vous permet de suivre les vues et l’utilisation du tableau de bord via les journaux d’audit. L’objet external_viewer_id est associé à une external_valuevariable globale qui peut être insérée dans des requêtes SQL utilisées dans des jeux de données de tableau de bord. Cela vous permet de filtrer les données affichées sur le tableau de bord pour chaque utilisateur.

external_viewer_id est transmis aux journaux d’audit de votre tableau de bord et ne doit pas inclure d’informations d’identification personnelle. Cette valeur doit également être unique par utilisateur.

external_value est utilisé dans le traitement des requêtes et peut inclure des informations d’identification personnelle.

L’exemple suivant montre comment utiliser la valeur externe comme filtre dans les requêtes de jeu de données :

SELECT *
FROM sales
WHERE region = __aibi_external_value

Vue d’ensemble de l’installation

Cette section inclut une vue d’ensemble conceptuelle générale des étapes que vous devez effectuer pour configurer l’incorporation d’un tableau de bord dans un emplacement externe.

Pour incorporer un tableau de bord dans une application externe, vous créez d’abord un principal de service dans Azure Databricks et générez un secret. Le principal de service doit disposer d’un accès en lecture au tableau de bord et à ses données sous-jacentes. Votre serveur utilise le secret du principal de service pour récupérer un jeton qui peut accéder aux API de tableau de bord pour le compte du principal de service. Avec ce jeton, le serveur appelle le /tokeninfo point de terminaison d’API, un point de terminaison OpenID Connect (OIDC) qui retourne des informations de profil utilisateur de base, y compris les valeurs et external_value les external_viewer_id valeurs. Ces valeurs vous permettent d’associer des requêtes à des utilisateurs individuels.

À l’aide du jeton obtenu à partir du principal de service, votre serveur génère un nouveau jeton étendu à l’utilisateur spécifique qui accède au tableau de bord. Ce jeton d’étendue utilisateur est transmis à la page d’application, où l’application instancie l’objet DatabricksDashboard à partir de la @databricks/aibi-client bibliothèque. Le jeton contient des informations spécifiques à l’utilisateur qui prennent en charge l’audit et applique le filtrage afin que chaque utilisateur voit uniquement les données auxquelles il est autorisé à accéder. Du point de vue de l’utilisateur, la connexion à l’application permet automatiquement d’accéder au tableau de bord incorporé avec la visibilité correcte des données.

Limites de débit et considérations relatives aux performances

L’incorporation externe a une limite de débit de 20 charges de tableau de bord par seconde. Vous pouvez ouvrir plus de 20 tableaux de bord simultanément, mais pas plus de 20 peuvent commencer à charger simultanément.

Prerequisites

Pour implémenter l’incorporation externe, veillez à respecter les conditions préalables suivantes :

Étape 1 : Créer un principal de service

Créez un principal de service pour agir comme l’identité de votre application externe dans Azure Databricks. Ce principal de service authentifie les demandes pour le compte de votre application.

Pour créer un principal de service :

  1. En tant qu’administrateur d’espace de travail, connectez-vous à l’espace de travail Azure Databricks.
  2. Cliquez sur votre nom d’utilisateur dans la barre supérieure de l’espace de travail Azure Databricks, puis sélectionnez Paramètres.
  3. Cliquez sur Identité et accès dans le volet gauche.
  4. À côté de Principaux de service, cliquez sur Gérer.
  5. Cliquez sur Ajouter un principal de service.
  6. Cliquez sur Ajouter nouveau.
  7. Entrez un nom descriptif pour le principal de service.
  8. Cliquez sur Ajouter.
  9. Ouvrez le principal de service que vous venez de créer à partir de la page de référencement des principaux de service . Utilisez le champ d’entrée de texte Filtrer pour le rechercher par nom, si nécessaire.
  10. Dans la page détails du principal du service , enregistrez l’ID d’application. Vérifiez que les cases à cocher Accès SQL Databricks et Espace de travail sont cochées.

Étape 2 : Créer un secret OAuth

Générez un secret pour le principal de service et collectez les valeurs de configuration suivantes, dont vous aurez besoin pour votre application externe :

  • Identifiant du principal de service (client)
  • Clé secrète client

Le principal de service utilise un secret OAuth pour vérifier son identité lors de la demande d’un jeton d’accès à partir de votre application externe.

Pour générer un secret :

  1. Cliquez sur Secrets dans la page de détails du principal du service .
  2. Cliquez sur Générer un secret.
  3. Entrez une valeur de durée de vie pour le nouveau secret en jours (par exemple, entre 1 et 730 jours).
  4. Copiez immédiatement le secret. Vous ne pouvez plus afficher ce secret après avoir quitté cet écran.

Étape 3 : Attribuer des autorisations à votre principal de service

Le principal de service que vous avez créé agit comme l’identité qui fournit un accès au tableau de bord via votre application. Ses autorisations s’appliquent uniquement si le tableau de bord n’est pas publié avec des autorisations de données partagées. Si des autorisations de données partagées sont utilisées, les informations d’identification de l’éditeur accèdent aux données. Pour plus d’informations et de recommandations, consultez les approches d’authentification d’incorporation.

  1. Cliquez sur Tableaux de bord dans la barre latérale de l’espace de travail pour ouvrir la page de description du tableau de bord.
  2. Cliquez sur le nom du tableau de bord que vous souhaitez incorporer. Le tableau de bord publié s’ouvre.
  3. Cliquez sur Partager.
  4. Utilisez le champ d’entrée de texte dans la boîte de dialogue Partage pour rechercher votre principal de service, puis cliquez dessus. Définissez le niveau d’autorisation sur CAN RUN. Cliquez ensuite sur Ajouter.
  5. Enregistrez l’ID du tableau de bord. Vous trouverez l’ID du tableau de bord dans l’URL du tableau de bord (par exemple https://<your-workspace-url>/dashboards/<dashboard-id>). Consultez les détails de l’espace de travail Databricks.

Note

Si vous publiez un tableau de bord avec des autorisations de données individuelles, vous devez accorder à votre principal de service l’accès aux données utilisées dans le tableau de bord. L’accès au calcul utilise toujours les informations d’identification de l’éditeur. Vous n’avez donc pas besoin d’accorder des autorisations de calcul au principal du service.

Pour lire et afficher des données, le principal de service doit disposer d’au moins SELECT des privilèges sur les tables et vues référencées dans le tableau de bord. Voir Qui peut gérer les privilèges ?.

Étape 4 : Utiliser l’exemple d’application pour authentifier et générer des jetons

Utilisez un exemple d’application pour pratiquer l’incorporation externe de votre tableau de bord. Les applications incluent des instructions et du code qui initie l’échange de jetons nécessaire pour générer des jetons délimités. Les blocs de code suivants n’ont aucune dépendance. Copiez et enregistrez l’une des applications suivantes.

Python

Copiez et enregistrez-le dans un fichier nommé example.py.

#!/usr/bin/env python3

import os
import sys
import json
import base64
import urllib.request
import urllib.parse
from http.server import HTTPServer, BaseHTTPRequestHandler

# -----------------------------------------------------------------------------
# Config
# -----------------------------------------------------------------------------
CONFIG = {
    "instance_url": os.environ.get("INSTANCE_URL"),
    "dashboard_id": os.environ.get("DASHBOARD_ID"),
    "service_principal_id": os.environ.get("SERVICE_PRINCIPAL_ID"),
    "service_principal_secret": os.environ.get("SERVICE_PRINCIPAL_SECRET"),
    "external_viewer_id": os.environ.get("EXTERNAL_VIEWER_ID"),
    "external_value": os.environ.get("EXTERNAL_VALUE"),
    "workspace_id": os.environ.get("WORKSPACE_ID"),
    "port": int(os.environ.get("PORT", 3000)),
}

basic_auth = base64.b64encode(
    f"{CONFIG['service_principal_id']}:{CONFIG['service_principal_secret']}".encode()
).decode()

# -----------------------------------------------------------------------------
# HTTP Request Helper
# -----------------------------------------------------------------------------
def http_request(url, method="GET", headers=None, body=None):
    headers = headers or {}
    if body is not None and not isinstance(body, (bytes, str)):
        raise ValueError("Body must be bytes or str")

    req = urllib.request.Request(url, method=method, headers=headers)
    if body is not None:
        if isinstance(body, str):
            body = body.encode()
        req.data = body

    try:
        with urllib.request.urlopen(req) as resp:
            data = resp.read().decode()
            try:
                return {"data": json.loads(data)}
            except json.JSONDecodeError:
                return {"data": data}
    except urllib.error.HTTPError as e:
        raise RuntimeError(f"HTTP {e.code}: {e.read().decode()}") from None

# -----------------------------------------------------------------------------
# Token logic
# -----------------------------------------------------------------------------
def get_scoped_token():
    # 1. Get all-api token
    oidc_res = http_request(
        f"{CONFIG['instance_url']}/oidc/v1/token",
        method="POST",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": f"Basic {basic_auth}",
        },
        body=urllib.parse.urlencode({
            "grant_type": "client_credentials",
            "scope": "all-apis"
        })
    )
    oidc_token = oidc_res["data"]["access_token"]

    # 2. Get token info
    token_info_url = (
        f"{CONFIG['instance_url']}/api/2.0/lakeview/dashboards/"
        f"{CONFIG['dashboard_id']}/published/tokeninfo"
        f"?external_viewer_id={urllib.parse.quote(CONFIG['external_viewer_id'])}"
        f"&external_value={urllib.parse.quote(CONFIG['external_value'])}"
    )
    token_info = http_request(
        token_info_url,
        headers={"Authorization": f"Bearer {oidc_token}"}
    )["data"]

    # 3. Generate scoped token
    params = token_info.copy()
    authorization_details = params.pop("authorization_details", None)
    params.update({
        "grant_type": "client_credentials",
        "authorization_details": json.dumps(authorization_details)
    })

    scoped_res = http_request(
        f"{CONFIG['instance_url']}/oidc/v1/token",
        method="POST",
        headers={
            "Content-Type": "application/x-www-form-urlencoded",
            "Authorization": f"Basic {basic_auth}",
        },
        body=urllib.parse.urlencode(params)
    )
    return scoped_res["data"]["access_token"]

# -----------------------------------------------------------------------------
# HTML generator
# -----------------------------------------------------------------------------
def generate_html(token):
    return f"""<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Demo</title>
    <style>
        body {{ font-family: system-ui; margin: 0; padding: 20px; background: #f5f5f5; }}
        .container {{ max-width: 1200px; margin: 0 auto; height:calc(100vh - 40px) }}
    </style>
</head>
<body>
    <div id="dashboard-content" class="container"></div>
    <script type="module">
        import {{ DatabricksDashboard }} from "https://cdn.jsdelivr.net/npm/@databricks/aibi-client@0.0.0-alpha.7/+esm";
        const dashboard = new DatabricksDashboard({{
            instanceUrl: "{CONFIG['instance_url']}",
            workspaceId: "{CONFIG['workspace_id']}",
            dashboardId: "{CONFIG['dashboard_id']}",
            token: "{token}",
            container: document.getElementById("dashboard-content")
        }});
        dashboard.initialize();
    </script>
</body>
</html>"""

# -----------------------------------------------------------------------------
# HTTP server
# -----------------------------------------------------------------------------
class RequestHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path != "/":
            self.send_response(404)
            self.send_header("Content-Type", "text/plain")
            self.end_headers()
            self.wfile.write(b"Not Found")
            return

        try:
            token = get_scoped_token()
            html = generate_html(token)
            status = 200
        except Exception as e:
            html = f"<h1>Error</h1><p>{e}</p>"
            status = 500

        self.send_response(status)
        self.send_header("Content-Type", "text/html")
        self.end_headers()
        self.wfile.write(html.encode())

def start_server():
    missing = [k for k, v in CONFIG.items() if not v]
    if missing:
        print(f"Missing: {', '.join(missing)}", file=sys.stderr)
        sys.exit(1)

    server = HTTPServer(("localhost", CONFIG["port"]), RequestHandler)
    print(f":rocket: Server running on http://localhost:{CONFIG['port']}")
    try:
        server.serve_forever()
    except KeyboardInterrupt:
        sys.exit(0)

if __name__ == "__main__":
    start_server()

JavaScript

Copiez et enregistrez-le dans un fichier nommé example.js.

#!/usr/bin/env node

const http = require('http');
const https = require('https');
const { URL, URLSearchParams } = require('url');

// This constant is just a mapping of environment variables to their respective
// values.
const CONFIG = {
  instanceUrl: process.env.INSTANCE_URL,
  dashboardId: process.env.DASHBOARD_ID,
  servicePrincipalId: process.env.SERVICE_PRINCIPAL_ID,
  servicePrincipalSecret: process.env.SERVICE_PRINCIPAL_SECRET,
  externalViewerId: process.env.EXTERNAL_VIEWER_ID,
  externalValue: process.env.EXTERNAL_VALUE,
  workspaceId: process.env.WORKSPACE_ID,
  port: process.env.PORT || 3000,
};

const basicAuth = Buffer.from(`${CONFIG.servicePrincipalId}:${CONFIG.servicePrincipalSecret}`).toString('base64');

// ------------------------------------------------------------------------------------------------
// Main
// ------------------------------------------------------------------------------------------------

function startServer() {
  const missing = Object.keys(CONFIG).filter((key) => !CONFIG[key]);
  if (missing.length > 0) throw new Error(`Missing: ${missing.join(', ')}`);

  const server = http.createServer(async (req, res) => {
    // This is a demo server, we only support GET requests to the root URL.
    if (req.method !== 'GET' || req.url !== '/') {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('Not Found');
      return;
    }

    let html = '';
    let status = 200;

    try {
      const token = await getScopedToken();
      html = generateHTML(token);
    } catch (error) {
      html = `<h1>Error</h1><p>${error.message}</p>`;
      status = 500;
    } finally {
      res.writeHead(status, { 'Content-Type': 'text/html' });
      res.end(html);
    }
  });

  server.listen(CONFIG.port, () => {
    console.log(`🚀 Server running on http://localhost:${CONFIG.port}`);
  });

  process.on('SIGINT', () => process.exit(0));
  process.on('SIGTERM', () => process.exit(0));
}

async function getScopedToken() {
  // 1. Get all-api token. This will allow you to access the /tokeninfo
  // endpoint, which contains the information required to generate a scoped token
  const {
    data: { access_token: oidcToken },
  } = await httpRequest(`${CONFIG.instanceUrl}/oidc/v1/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${basicAuth}`,
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      scope: 'all-apis',
    }),
  });

  // 2. Get token info. This information is **required** for generating a token that is correctly downscoped.
  // A correctly downscoped token will only have access to a handful of APIs, and within those APIs, only
  // a the specific resources required to render the dashboard.
  //
  // This is essential to prevent leaking a privileged token.
  //
  // At the time of writing, OAuth tokens in Databricks are valid for 1 hour.
  const tokenInfoUrl = new URL(
    `${CONFIG.instanceUrl}/api/2.0/lakeview/dashboards/${CONFIG.dashboardId}/published/tokeninfo`,
  );
  tokenInfoUrl.searchParams.set('external_viewer_id', CONFIG.externalViewerId);
  tokenInfoUrl.searchParams.set('external_value', CONFIG.externalValue);

  const { data: tokenInfo } = await httpRequest(tokenInfoUrl.toString(), {
    headers: { Authorization: `Bearer ${oidcToken}` },
  });

  // 3. Generate scoped token. This call is very similar to what was issued before, but now we are providing the scoping to make the generated token
  // safe to pass to a browser.
  const { authorization_details, ...params } = tokenInfo;
  const {
    data: { access_token },
  } = await httpRequest(`${CONFIG.instanceUrl}/oidc/v1/token`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      Authorization: `Basic ${basicAuth}`,
    },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      ...params,
      authorization_details: JSON.stringify(authorization_details),
    }),
  });

  return access_token;
}

startServer();

// ------------------------------------------------------------------------------------------------
// Helper functions
// ------------------------------------------------------------------------------------------------

/**
 * Helper function to create HTTP requests.
 * @param {string} url - The URL to make the request to.
 * @param {Object} options - The options for the request.
 * @param {string} options.method - The HTTP method to use.
 * @param {Object} options.headers - The headers to include in the request.
 * @param {Object} options.body - The body to include in the request.
 * @returns {Promise<Object>} A promise that resolves to the response data.
 */
function httpRequest(url, { method = 'GET', headers = {}, body } = {}) {
  return new Promise((resolve, reject) => {
    const isHttps = url.startsWith('https://');
    const lib = isHttps ? https : http;
    const options = new URL(url);
    options.method = method;
    options.headers = headers;

    const req = lib.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => {
        if (res.statusCode >= 200 && res.statusCode < 300) {
          try {
            resolve({ data: JSON.parse(data) });
          } catch {
            resolve({ data });
          }
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${data}`));
        }
      });
    });

    req.on('error', reject);

    if (body) {
      if (typeof body === 'string' || Buffer.isBuffer(body)) {
        req.write(body);
      } else if (body instanceof URLSearchParams) {
        req.write(body.toString());
      } else {
        req.write(JSON.stringify(body));
      }
    }
    req.end();
  });
}

function generateHTML(token) {
  return `<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Demo</title>
    <style>
        body { font-family: system-ui; margin: 0; padding: 20px; background: #f5f5f5; }
        .container { max-width: 1200px; margin: 0 auto; height:calc(100vh - 40px) }
    </style>
</head>
<body>
    <div id="dashboard-content" class="container"></div>
    <script type="module">
        /**
         * We recommend bundling the dependency instead of using a CDN. However, for demonstration purposes,
         * we are just using a CDN.
         * 
         * We do not recommend one CDN over another and encourage decoupling the dependency from third-party code.
         */
        import { DatabricksDashboard } from "https://cdn.jsdelivr.net/npm/@databricks/aibi-client@0.0.0-alpha.7/+esm";
    
        const dashboard = new DatabricksDashboard({
            instanceUrl: "${CONFIG.instanceUrl}",
            workspaceId: "${CONFIG.workspaceId}",
            dashboardId: "${CONFIG.dashboardId}",
            token: "${token}",
            container: document.getElementById("dashboard-content")
        });
        
        dashboard.initialize();
    </script>
</body>
</html>`;
}

Étape 5 : Exécuter l’exemple d’application

Remplacez les valeurs suivantes, puis exécutez le bloc de code de votre terminal. Vos valeurs ne doivent pas être entourées de crochets () :< >

  • Utilisez l’URL de l’espace de travail pour rechercher et remplacer les valeurs suivantes :
    • <your-instance>
    • <workspace_id>
    • <dashboard_id>
  • Remplacez les valeurs suivantes par les valeurs que vous avez créées lors de la création du principal de service (étape 2) :
    • <service_principal_id>
    • <service_principal_secret> (clé secrète client)
  • Remplacez les valeurs suivantes par les identificateurs associés aux utilisateurs de l’application externe :
    • <some-external-viewer>
    • <some-external-value>
  • Remplacez </path/to/example> par le chemin d’accès au .py fichier ou .js au fichier que vous avez créé à l’étape précédente. Incluez l’extension de fichier.

Note

N’incluez aucune information personnellement identifiable (PII) dans la EXTERNAL_VIEWER_ID valeur.


INSTANCE_URL='https://<your-instance>.databricks.com' \
WORKSPACE_ID='<workspace_id>' \
DASHBOARD_ID='<dashboard_id>' \
SERVICE_PRINCIPAL_ID='<service-principal-id>' \
SERVICE_PRINCIPAL_SECRET='<service-principal_secret>' \
EXTERNAL_VIEWER_ID='<some-external-viewer>' \
EXTERNAL_VALUE='<some-external-value>' \
~</path/to/example>

# Terminal will output: :rocket: Server running on http://localhost:3000