Partilhar via


O que é incorporação para usuários externos?

Importante

Este recurso está no Public Preview.

Esta página descreve como funciona a incorporação para usuários externos, como configurar seu espaço de trabalho do Azure Databricks para compartilhamento seguro de painéis incorporados e como usar aplicativos de exemplo para começar. A incorporação para usuários externos usa uma entidade de serviço e tokens de acesso com escopo para autenticar e autorizar o acesso a painéis incorporados. Essa abordagem permite que você compartilhe painéis com visualizadores fora da sua organização, como parceiros e clientes, sem provisionar contas do Azure Databricks para esses usuários.

Para saber mais sobre outras opções de incorporação, incluindo a incorporação de painéis para usuários em sua organização, consulte Incorporar um painel.

Como funciona a incorporação para usuários externos

O diagrama e as etapas numeradas a seguir explicam como os usuários são autenticados e os painéis são preenchidos com resultados de escopo do usuário quando você incorpora um painel para usuários externos.

Um fluxograma mostrando as trocas de token necessárias em seu aplicativo e no espaço de trabalho Databricks.

  1. Autenticação e solicitação do usuário: O utilizador inicia sessão na sua aplicação. O frontend do seu aplicativo envia uma solicitação autenticada ao seu servidor para um token de acesso ao painel.
  2. Autenticação da entidade de serviço: Seu servidor usa o segredo da entidade de serviço para solicitar e receber um token OAuth do servidor Databricks. Este é um token de escopo amplo que pode chamar todas as APIs de painel às quais o Azure Databricks tem acesso em nome da entidade de serviço. Seu servidor chama o /tokeninfo ponto de extremidade usando esse token, passando informações básicas do usuário, como external_viewer_id e external_value. Consulte Apresentar painéis com segurança para usuários individuais.
  3. Geração de token com escopo do usuário: Usando a resposta do ponto de extremidade e o ponto de /tokeninfo extremidade Databricks OpenID Connect (OIDC), seu servidor gera um novo token com escopo restrito que codifica as informações do usuário que você passou.
  4. Renderização de painéis e filtragem de dados: A página do aplicativo instancia DatabricksDashboard e passa o token do @databricks/aibi-client escopo do usuário durante a construção. O painel é renderizado com o contexto do usuário. Esse token autoriza o acesso, suporta auditoria com external_viewer_ide carrega external_value para filtragem de dados. As consultas nos conjuntos de dados do painel podem fazer referência __aibi_external_value para aplicar filtros por usuário, garantindo que cada visualizador veja apenas os dados que tem permissão para visualizar.

Apresente painéis com segurança para usuários individuais

Configure seu servidor de aplicativos para gerar um token exclusivo com escopo de usuário para cada usuário com base em seu external_viewer_id. Isso permite que você acompanhe as visualizações e o uso do painel por meio de logs de auditoria. O external_viewer_id é emparelhado com um external_value, que atua como uma variável global que pode ser inserida em consultas SQL usadas em conjuntos de dados de painel. Isso permite filtrar os dados exibidos no painel para cada usuário.

external_viewer_id é passado para os logs de auditoria do painel e não deve incluir informações pessoalmente identificáveis. Esse valor também deve ser exclusivo por usuário.

external_value é usado no processamento de consultas e pode incluir informações pessoalmente identificáveis.

O exemplo a seguir demonstra como usar o valor externo como um filtro em consultas de conjunto de dados:

SELECT *
FROM sales
WHERE region = __aibi_external_value

Visão geral da configuração

Esta seção inclui uma visão geral conceitual de alto nível das etapas que você precisa executar para configurar a incorporação de um painel em um local externo.

Para incorporar um painel em um aplicativo externo, primeiro crie uma entidade de serviço no Azure Databricks e gere um segredo. A entidade de serviço deve receber acesso de leitura ao painel e seus dados subjacentes. Seu servidor usa o segredo da entidade de serviço para recuperar um token que pode acessar APIs de painel em nome da entidade de serviço. Com esse token, o servidor chama o ponto de /tokeninfo extremidade da API, um ponto de extremidade OpenID Connect (OIDC) que retorna informações básicas de perfil de usuário, incluindo os external_value valores e external_viewer_id . Esses valores permitem associar solicitações a usuários individuais.

Usando o token obtido da entidade de serviço, o servidor gera um novo token com escopo para o usuário específico que está acessando o painel. Esse token com escopo de usuário é passado para a página do aplicativo, onde o aplicativo instancia o DatabricksDashboard objeto da @databricks/aibi-client biblioteca. O token carrega informações específicas do usuário que suportam auditoria e impõe filtragem para que cada usuário veja apenas os dados que está autorizado a acessar. Do ponto de vista do usuário, o login no aplicativo fornece automaticamente acesso ao painel incorporado com a visibilidade de dados correta.

Limites de taxa e considerações de desempenho

A incorporação externa tem um limite de taxa de 20 cargas de painel por segundo. Você pode abrir mais de 20 painéis de uma só vez, mas não mais de 20 podem começar a carregar simultaneamente.

Pré-requisitos

Para implementar a incorporação externa, certifique-se de atender aos seguintes pré-requisitos:

Etapa 1: Criar uma entidade de serviço

Crie uma entidade de serviço para atuar como a identidade para seu aplicativo externo no Azure Databricks. Esta entidade de serviço autentica pedidos em nome da sua aplicação.

Para criar uma entidade de serviço:

  1. Como administrador do espaço de trabalho, faça logon no espaço de trabalho do Azure Databricks.
  2. Clique no seu nome de utilizador na barra superior da área de trabalho do Azure Databricks e selecione Definições.
  3. Clique em Identidade e acesso no painel esquerdo.
  4. Ao lado de Entidades de serviço, clique em Gerenciar.
  5. Clique em Adicionar principal do serviço.
  6. Clique em Adicionar novo.
  7. Insira um nome descritivo para a entidade de serviço.
  8. Clique em Adicionar.
  9. Abra a entidade de serviço que você acabou de criar na página de listagem Entidades de serviço . Use o campo Filtrar entrada de texto para procurá-lo por nome, se necessário.
  10. Na página Detalhes da entidade de serviço , registre a ID do aplicativo. Verifique se as caixas de seleção Acesso ao Databricks SQL e Acesso ao espaço de trabalho estão marcadas.

Etapa 2: Criar um segredo OAuth

Gere um segredo para a entidade de serviço e colete os seguintes valores de configuração, que você precisará para seu aplicativo externo:

  • ID da entidade de serviço (cliente)
  • Segredo do cliente

A entidade de serviço usa um segredo OAuth para verificar sua identidade ao solicitar um token de acesso do seu aplicativo externo.

Para gerar um segredo:

  1. Clique em Segredos na página Detalhes da entidade de serviço .
  2. Clique em Gerar segredo.
  3. Insira um valor vitalício para o novo segredo em dias (por exemplo, entre 1 e 730 dias).
  4. Copie o segredo imediatamente. Não é possível visualizar este segredo novamente depois de sair deste ecrã.

Etapa 3: atribuir permissões à entidade de serviço

A entidade de serviço que você criou atua como a identidade que fornece acesso ao painel por meio do seu aplicativo. As suas permissões aplicam-se apenas se o dashboard não for publicado com permissões de dados compartilhados. Se forem usadas permissões de dados compartilhados, as credenciais do editor acessam os dados. Para obter mais detalhes e recomendações, consulte Incorporando abordagens de autenticação.

  1. Clique em Painéis na barra lateral do espaço de trabalho para abrir a página de listagem do painel.
  2. Clique no nome do painel que você deseja incorporar. O painel publicado é aberto.
  3. Clique em Share (Partilhar).
  4. Use o campo de entrada de texto na caixa de diálogo Compartilhamento para localizar sua entidade de serviço e clique nele. Defina o nível de permissão como CAN RUN. Em seguida, clique em Adicionar.
  5. Registre o ID do painel. Você pode encontrar o ID do painel no URL do painel (por exemplo, https://<your-workspace-url>/dashboards/<dashboard-id>). Consulte Detalhes do espaço de trabalho Databricks.

Observação

Se publicar um dashboard com permissões de dados individuais, deverá conceder ao serviço principal acesso aos dados usados no dashboard. O acesso de computação sempre usa as credenciais do editor, portanto, você não precisa conceder permissões de computação à entidade de serviço.

Para ler e exibir dados, a entidade de serviço deve ter pelo menos SELECT privilégios nas tabelas e exibições referenciadas no painel. Consulte Quem pode gerenciar privilégios?.

Etapa 4: Use o aplicativo de exemplo para autenticar e gerar tokens

Use um aplicativo de exemplo para praticar a incorporação externa do seu painel. Os aplicativos incluem instruções e código que inicia a troca de tokens necessária para gerar tokens com escopo. Os blocos de código a seguir não têm dependências. Copie e salve um dos seguintes aplicativos.

Python

Copie e salve isso em um arquivo chamado 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

Copie e salve isso em um arquivo chamado 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>`;
}

Etapa 5: Executar o aplicativo de exemplo

Substitua os seguintes valores e, em seguida, execute o bloco de código a partir do seu terminal. Seus valores não devem ser cercados por colchetes angulares (< >):

  • Use a URL do espaço de trabalho para localizar e substituir os seguintes valores:
    • <your-instance>
    • <workspace_id>
    • <dashboard_id>
  • Substitua os seguintes valores pelos valores criados ao criar a entidade de serviço (etapa 2):
    • <service_principal_id>
    • <service_principal_secret> (segredo do cliente)
  • Substitua os seguintes valores por identificadores associados aos usuários do aplicativo externo:
    • <some-external-viewer>
    • <some-external-value>
  • Substitua </path/to/example> pelo caminho para o .py arquivo ou .js que você criou na etapa anterior. Inclua a extensão do arquivo.

Observação

Não inclua nenhuma informação de identificação pessoal (PII) no EXTERNAL_VIEWER_ID valor.


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