Condividi tramite


Che cos'è l'incorporamento per gli utenti esterni?

Importante

Questa funzionalità è in Anteprima Pubblica.

Questa pagina descrive il funzionamento dell'incorporamento per gli utenti esterni, come configurare l'area di lavoro di Azure Databricks per la condivisione sicura dei dashboard incorporati e come usare applicazioni di esempio per iniziare. L'incorporamento per gli utenti esterni usa un'entità servizio e token di accesso con ambito per autenticare e autorizzare l'accesso ai dashboard incorporati. Questo approccio consente di condividere dashboard con visualizzatori esterni all'organizzazione, ad esempio partner e clienti, senza effettuare il provisioning degli account Azure Databricks per tali utenti.

Per informazioni su altre opzioni di incorporamento, inclusi i dashboard di incorporamento per gli utenti all'interno dell'organizzazione, vedere Incorporare un dashboard.

Funzionamento dell'incorporamento per utenti esterni

Il diagramma e i passaggi numerati che seguono illustrano come gli utenti vengono autenticati e i dashboard vengono popolati con risultati con ambito utente quando si incorpora un dashboard per gli utenti esterni.

Diagramma di flusso che mostra gli scambi di token necessari nell'applicazione e nell'area di lavoro Databricks.

  1. Autenticazione utente e richiesta: L'utente accede all'applicazione. Il front-end dell'applicazione invia una richiesta autenticata al server per un token di accesso al dashboard.
  2. Autenticazione dell'entità servizio: Il server usa il segreto dell'entità servizio per richiedere e ricevere un token OAuth dal server Databricks. Si tratta di un token con ambito generale che può chiamare tutte le API del dashboard a cui Azure Databricks può accedere per conto dell'entità servizio. Il server chiama l'endpoint /tokeninfo usando questo token, passando informazioni utente di base, ad esempio external_viewer_id e external_value. Vedere Presentare in modo sicuro i dashboard ai singoli utenti.
  3. Generazione di token con ambito utente: Usando la risposta dall'endpoint e dall'endpoint /tokeninfo OIDC (Databricks OpenID Connect), il server genera un nuovo token con ambito stretto che codifica le informazioni utente passate.
  4. Rendering del dashboard e filtro dei dati: La pagina dell'applicazione crea un'istanza DatabricksDashboard da @databricks/aibi-client e passa il token con ambito utente durante la costruzione. Il rendering del dashboard viene eseguito con il contesto dell'utente. Questo token autorizza l'accesso, supporta il controllo con external_viewer_ide esegue il external_value filtro dei dati. Le query nei set di dati del dashboard possono fare riferimento __aibi_external_value per applicare filtri per utente, assicurando che ogni visualizzatore veda solo i dati che possono visualizzare.

Presentare in modo sicuro i dashboard ai singoli utenti

Configurare il server applicazioni per generare un token univoco con ambito utente per ogni utente in base al relativo external_viewer_id. In questo modo è possibile tenere traccia delle visualizzazioni del dashboard e dell'utilizzo tramite i log di controllo. è external_viewer_id associato a un external_valueoggetto , che funge da variabile globale che può essere inserita nelle query SQL usate nei set di dati del dashboard. In questo modo è possibile filtrare i dati visualizzati nel dashboard per ogni utente.

external_viewer_id viene passato ai log di controllo del dashboard e non deve includere informazioni personali. Questo valore deve anche essere univoco per utente.

external_value viene usato nell'elaborazione delle query e può includere informazioni personali.

L'esempio seguente illustra come usare il valore esterno come filtro nelle query del set di dati:

SELECT *
FROM sales
WHERE region = __aibi_external_value

Panoramica dell'installazione

Questa sezione include una panoramica concettuale generale dei passaggi da eseguire per configurare per l'incorporamento di un dashboard in una posizione esterna.

Per incorporare un dashboard in un'applicazione esterna, creare prima un'entità servizio in Azure Databricks e generare un segreto. All'entità servizio deve essere concesso l'accesso in lettura al dashboard e ai relativi dati sottostanti. Il server usa il segreto dell'entità servizio per recuperare un token che può accedere alle API del dashboard per conto dell'entità servizio. Con questo token, il server chiama l'endpoint /tokeninfo API, un endpoint OpenID Connect (OIDC) che restituisce informazioni di base sul profilo utente, inclusi i external_value valori e external_viewer_id . Questi valori consentono di associare le richieste ai singoli utenti.

Usando il token ottenuto dall'entità servizio, il server genera un nuovo token con ambito per l'utente specifico che accede al dashboard. Questo token con ambito utente viene passato alla pagina dell'applicazione, in cui l'applicazione crea un'istanza dell'oggetto DatabricksDashboard dalla @databricks/aibi-client libreria. Il token contiene informazioni specifiche dell'utente che supportano il controllo e applicano il filtro in modo che ogni utente veda solo i dati a cui è autorizzato ad accedere. Dal punto di vista dell'utente, l'accesso all'applicazione fornisce automaticamente l'accesso al dashboard incorporato con la visibilità corretta dei dati.

Considerazioni sulle prestazioni e i limiti di frequenza

L'incorporamento esterno prevede un limite di frequenza di 20 caricamenti del dashboard al secondo. È possibile aprire più di 20 dashboard contemporaneamente, ma non più di 20 possono iniziare il caricamento contemporaneamente.

Prerequisiti

Per implementare l'incorporamento esterno, assicurarsi di soddisfare i prerequisiti seguenti:

Passaggio 1: Creare un'entità servizio

Creare un'entità servizio per fungere da identità per l'applicazione esterna in Azure Databricks. Questa entità servizio autentica le richieste per conto dell'applicazione.

Per creare un'entità servizio:

  1. In quanto amministratore dell'area di lavoro, accedere all'area di lavoro di Azure Databricks.
  2. Fare clic sul nome utente nella barra superiore dell'area di lavoro di Azure Databricks e selezionare Impostazioni.
  3. Fare clic su Identità e accesso nel riquadro sinistro.
  4. Accanto a Oggetti principali del servizio, cliccare su Gestisci.
  5. Fare clic su Aggiungi entità servizio.
  6. Fare clic su Aggiungi nuovo.
  7. Immettere un nome descrittivo per l'entità servizio.
  8. Fare clic su Aggiungi.
  9. Aprire l'entità servizio appena creata dalla pagina Elenco entità servizio . Usare il campo Filtra voce di testo per cercarlo in base al nome, se necessario.
  10. Nella pagina Dettagli entità servizio registrare l'ID applicazione. Verificare che le caselle di controllo Accesso a Databricks SQL e Accesso all'area di lavoro siano selezionate.

Passaggio 2: Creare un segreto OAuth

Generare un segreto per l'entità servizio e raccogliere i valori di configurazione seguenti, necessari per l'applicazione esterna:

  • ID entità servizio (client)
  • Segreto del cliente

L'entità servizio usa un segreto OAuth per verificarne l'identità quando si richiede un token di accesso dall'applicazione esterna.

Per generare un segreto:

  1. Fare clic su Segreti nella pagina dei dettagli dell'entità servizio .
  2. Fare clic su Genera segreto.
  3. Immettere un valore di durata per il nuovo segreto in giorni ,ad esempio tra 1 e 730 giorni.
  4. Copiare immediatamente il segreto. Non è possibile visualizzare di nuovo il segreto dopo aver lasciato questa schermata.

Passaggio 3: Assegnare le autorizzazioni all'entità servizio

L'entità servizio creata funge da identità che fornisce l'accesso al dashboard tramite l'applicazione. Le autorizzazioni si applicano solo se il dashboard non viene pubblicato con autorizzazioni per i dati condivisi. Se vengono usate autorizzazioni per i dati condivisi, le credenziali dell'editore accedono ai dati. Per altri dettagli e consigli, vedere Incorporamento degli approcci di autenticazione.

  1. Fare clic su Dashboard nella barra laterale dell'area di lavoro per aprire la pagina di presentazione del dashboard.
  2. Fare clic sul nome del dashboard da incorporare. Verrà aperto il dashboard pubblicato.
  3. Fare clic su Condividi.
  4. Usare il campo della voce di testo nella finestra di dialogo Condivisione per trovare l'entità servizio e quindi fare clic su di esso. Impostare il livello di autorizzazione su CAN RUN. Fare quindi clic su Aggiungi.
  5. Registrare l'ID dashboard. È possibile trovare l'ID del dashboard nell'URL del dashboard , ad esempio https://<your-workspace-url>/dashboards/<dashboard-id>. Vedere Dettagli dell'area di lavoro di Databricks.

Annotazioni

Se si pubblica un dashboard con autorizzazioni di dati individuali, è necessario concedere all'entità servizio l'accesso ai dati usati nel dashboard. L'accesso di calcolo usa sempre le credenziali dell'editore, pertanto non è necessario concedere autorizzazioni di calcolo all'entità servizio.

Per leggere e visualizzare i dati, l'entità servizio deve disporre almeno SELECT dei privilegi per le tabelle e le viste a cui si fa riferimento nel dashboard. Vedere Chi può gestire i privilegi?

Passaggio 4: Usare l'app di esempio per autenticare e generare token

Usare un'applicazione di esempio per praticare l'incorporamento esterno del dashboard. Le applicazioni includono istruzioni e codice che avvia lo scambio di token necessario per generare token con ambito. I blocchi di codice seguenti non hanno dipendenze. Copiare e salvare una delle applicazioni seguenti.

Pitone

Copiare e salvare il file in un file denominato 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

Copiare e salvare il file in un file denominato 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>`;
}

Passaggio 5: Eseguire l'applicazione di esempio

Sostituire i valori seguenti e quindi eseguire il blocco di codice dal terminale. I valori non devono essere racchiusi tra parentesi angolari (< >):

  • Usare l'URL dell'area di lavoro per trovare e sostituire i valori seguenti:
    • <your-instance>
    • <workspace_id>
    • <dashboard_id>
  • Sostituire i valori seguenti con i valori creati durante la creazione dell'entità servizio (passaggio 2):
    • <service_principal_id>
    • <service_principal_secret> (segreto client)
  • Sostituire i valori seguenti con gli identificatori associati agli utenti dell'applicazione esterna:
    • <some-external-viewer>
    • <some-external-value>
  • Sostituire </path/to/example> con il percorso del .py file o .js creato nel passaggio precedente. Includere l'estensione del file.

Annotazioni

Non includere informazioni personali nel EXTERNAL_VIEWER_ID valore .


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