Sdílet prostřednictvím


Co je vkládání pro externí uživatele?

Důležité

Tato funkce je ve verzi Public Preview.

Tato stránka popisuje, jak funguje vkládání externích uživatelů, jak nakonfigurovat pracovní prostor Azure Databricks pro zabezpečené sdílení vložených řídicích panelů a jak začít používat ukázkové aplikace. Vkládání externích uživatelů používá instanční objekt a přístupové tokeny s vymezeným oborem k ověřování a autorizaci přístupu k vloženým řídicím panelům. Tento přístup umožňuje sdílet řídicí panely s diváky mimo vaši organizaci, jako jsou partneři a zákazníci, aniž byste pro tyto uživatele zřídili účty Azure Databricks.

Další možnosti vkládání, včetně vkládání řídicích panelů pro uživatele ve vaší organizaci, najdete v tématu Vložení řídicího panelu.

Jak funguje vkládání pro externí uživatele

Postup diagramu a číslovaný postup vysvětlující, jak se uživatelé ověřují, a řídicí panely se při vkládání řídicího panelu pro externí uživatele naplní výsledky s vymezeným uživatelem.

Vývojový diagram znázorňující potřebné výměny tokenů napříč vaší aplikací a pracovním prostorem Databricks

  1. Ověřování a žádost uživatele: Uživatel se přihlásí k vaší aplikaci. Front-end vaší aplikace odešle na server ověřený požadavek na přístupový token řídicího panelu.
  2. Ověřování instančního objektu: Váš server používá tajný klíč instančního objektu k vyžádání a přijetí tokenu OAuth ze serveru Databricks. Jedná se o široce vymezený token, který může volat všechna rozhraní API řídicího panelu, ke kterým má Azure Databricks přístup jménem instančního objektu. Váš server volá /tokeninfo koncový bod pomocí tohoto tokenu a předává základní informace o uživateli, například external_viewer_id a external_value. Zobrazení bezpečné prezentace řídicích panelů jednotlivým uživatelům
  3. Generování tokenů v oboru uživatele: Pomocí odpovědi z koncového /tokeninfo bodu a koncového bodu Databricks OpenID Connect (OIDC) váš server vygeneruje nový pevně vymezený token, který kóduje informace o uživateli, které jste předali.
  4. Vykreslování řídicího panelu a filtrování dat: Stránka aplikace vytvoří instanci DatabricksDashboard@databricks/aibi-client tokenu v oboru uživatele a předá ho během sestavování. Řídicí panel se vykreslí s kontextem uživatele. Tento token autorizuje přístup, podporuje auditování a external_viewer_idprovádí external_value filtrování dat. Dotazy v datových sadách řídicích panelů můžou odkazovat __aibi_external_value na použití filtrů pro jednotlivé uživatele a zajistit tak, aby každý prohlížeč viděl jenom data, která smí zobrazit.

Bezpečné prezentování řídicích panelů jednotlivým uživatelům

Nakonfigurujte aplikační server tak, aby vygeneroval jedinečný token v oboru uživatele pro každého uživatele na základě jejich external_viewer_id. Díky tomu můžete sledovat zobrazení řídicích panelů a využití prostřednictvím protokolů auditu. Spáruje external_viewer_id se s proměnnou external_value, která funguje jako globální proměnná, která se dá vložit do dotazů SQL používaných v datových sadách řídicích panelů. To umožňuje filtrovat data zobrazená na řídicím panelu pro každého uživatele.

external_viewer_id se předává do protokolů auditu řídicího panelu a nesmí obsahovat identifikovatelné osobní údaje. Tato hodnota by měla být také jedinečná pro jednotlivé uživatele.

external_value se používá při zpracování dotazů a může obsahovat identifikovatelné osobní údaje.

Následující příklad ukazuje, jak použít externí hodnotu jako filtr v dotazech datové sady:

SELECT *
FROM sales
WHERE region = __aibi_external_value

Přehled nastavení

Tato část obsahuje základní koncepční přehled kroků, které je potřeba provést, abyste mohli řídicí panel vložit do externího umístění.

Pokud chcete vložit řídicí panel do externí aplikace, nejprve vytvoříte instanční objekt v Azure Databricks a vygenerujete tajný klíč. Instančnímu objektu musí být udělen přístup pro čtení k řídicímu panelu a jeho podkladovým datům. Váš server používá tajný kód instančního objektu k načtení tokenu, který má přístup k rozhraním API řídicího panelu jménem instančního objektu. Pomocí tohoto tokenu /tokeninfo server volá koncový bod rozhraní API, koncový bod OpenID Connect (OIDC), který vrací základní informace o profilu uživatele, včetně external_value hodnot a external_viewer_id hodnot. Tyto hodnoty umožňují přidružit žádosti k jednotlivým uživatelům.

Pomocí tokenu získaného z instančního objektu vygeneruje váš server nový token s vymezeným oborem pro konkrétního uživatele, který přistupuje k řídicímu panelu. Tento token s oborem uživatele se předává na stránku aplikace, kde aplikace vytvoří DatabricksDashboard instanci objektu @databricks/aibi-client z knihovny. Token obsahuje informace specifické pro uživatele, které podporují auditování a vynucují filtrování, aby každý uživatel viděl pouze data, ke kterým má oprávnění přistupovat. Z pohledu uživatele poskytuje přihlášení k aplikaci automaticky přístup k vloženého řídicího panelu se správnou viditelností dat.

Aspekty omezení rychlosti a výkonu

Externí vkládání má limit rychlosti 20 načtení řídicího panelu za sekundu. Můžete otevřít více než 20 řídicích panelů najednou, ale ne více než 20 může začít načítat současně.

Požadavky

Pokud chcete implementovat externí vkládání, ujistěte se, že splňujete následující požadavky:

Krok 1: Vytvoření instančního objektu

Vytvořte instanční objekt, který bude fungovat jako identita vaší externí aplikace v rámci Azure Databricks. Tento instanční objekt ověřuje požadavky jménem vaší aplikace.

Vytvoření instančního objektu:

  1. Jako správce pracovního prostoru se přihlaste k pracovnímu prostoru Azure Databricks.
  2. Klikněte na své uživatelské jméno v horním panelu pracovního prostoru Azure Databricks a vyberte Nastavení.
  3. V levém podokně klikněte na Identita a přístup .
  4. Vedle poskytovatelů služeb klikněte na Spravovat.
  5. Klikněte na Přidat instanční objekt.
  6. Klikněte na Přidat nový.
  7. Zadejte popisný název instančního objektu.
  8. Klikněte na tlačítko Přidat.
  9. Otevřete instanční objekt, který jste právě vytvořili ze stránky výpisu instančních objektů . V případě potřeby ho vyhledejte podle názvu pomocí pole Filtrovat textové položky.
  10. Na stránce podrobností instančního objektu si poznamenejte ID aplikace. Ověřte, že jsou zaškrtnutá políčka přístup k Sql Databricks a přístup k pracovnímu prostoru .

Krok 2: Vytvoření tajného klíče OAuth

Vygenerujte tajný kód instančního objektu a shromážděte následující hodnoty konfigurace, které budete potřebovat pro vaši externí aplikaci:

  • ID instančního objektu (klienta)
  • Klientské tajemství

Instanční objekt používá tajný klíč OAuth k ověření své identity při vyžádání přístupového tokenu z externí aplikace.

Vygenerování tajného kódu:

  1. Na stránce podrobností instančního objektu klikněte na Tajné kódy.
  2. Klikněte na Vygenerovat tajný klíč.
  3. Zadejte hodnotu životnosti nového tajného kódu ve dnech (např. mezi 1 a 730 dny).
  4. Zkopírujte tajný kód okamžitě. Jakmile opustíte tuto obrazovku, nemůžete tento tajný kód znovu zobrazit.

Krok 3: Přiřazení oprávnění k instančnímu objektu

Instanční objekt, který jste vytvořili, funguje jako identita, která poskytuje přístup k řídicímu panelu prostřednictvím vaší aplikace. Jeho oprávnění platí jenom v případě, že řídicí panel není publikovaný s oprávněními ke sdíleným datům. Pokud se použijí oprávnění ke sdíleným datům, přihlašovací údaje vydavatele přistupují k datům. Další podrobnosti a doporučení najdete v tématu Přístupy k ověřování vkládání.

  1. Kliknutím na Řídicí panely na bočním panelu pracovního prostoru otevřete stránku výpisu řídicího panelu.
  2. Klikněte na název řídicího panelu, který chcete vložit. Otevře se publikovaný řídicí panel.
  3. Klikněte na Share (Sdílet).
  4. Pomocí textového pole v dialogovém okně Sdílení vyhledejte instanční objekt a klikněte na něj. Nastavte úroveň oprávnění na CAN RUN. Potom klikněte na Přidat.
  5. Poznamenejte si ID řídicího panelu. ID řídicího panelu najdete v adrese URL řídicího panelu (např https://<your-workspace-url>/dashboards/<dashboard-id>. ). Podrobnosti o pracovním prostoru Databricks

Poznámka:

Pokud publikujete řídicí panel s individuálními oprávněními k datům, musíte služebnímu principálovi udělit přístup k datům použitým v řídicím panelu. Výpočetní přístup vždy používá přihlašovací údaje vydavatele, takže nemusíte udělovat výpočetní oprávnění instančnímu objektu.

Aby mohl instanční objekt číst a zobrazovat data, musí mít alespoň SELECT oprávnění k tabulkám a zobrazením odkazovaným na řídicím panelu. Podívejte se, kdo může spravovat oprávnění?

Krok 4: Použití ukázkové aplikace k ověřování a generování tokenů

Ukázkovou aplikaci můžete použít k externímu vložení řídicího panelu. Aplikace obsahují pokyny a kód, které inicializuje potřebnou výměnu tokenů ke generování tokenů s vymezeným oborem. Následující bloky kódu nemají žádné závislosti. Zkopírujte a uložte jednu z následujících aplikací.

Python

Zkopírujte a uložte ho do souboru s názvem 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

Zkopírujte a uložte ho do souboru s názvem 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>`;
}

Krok 5: Spuštění ukázkové aplikace

Nahraďte následující hodnoty a spusťte blok kódu z terminálu. Hodnoty by neměly být ohraničené úhlovými závorkami (< >):

  • Pomocí adresy URL pracovního prostoru vyhledejte a nahraďte následující hodnoty:
    • <your-instance>
    • <workspace_id>
    • <dashboard_id>
  • Nahraďte následující hodnoty hodnotami, které jste vytvořili při vytváření instančního objektu (krok 2):
    • <service_principal_id>
    • <service_principal_secret> (tajný klíč klienta)
  • Nahraďte následující hodnoty identifikátory přidruženými uživatelům externí aplikace:
    • <some-external-viewer>
    • <some-external-value>
  • Nahraďte </path/to/example> cestou k .py souboru, .js který jste vytvořili v předchozím kroku. Zahrňte příponu souboru.

Poznámka:

Do hodnoty nezahrnujte žádné identifikovatelné osobní údaje (PII EXTERNAL_VIEWER_ID ).


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