Compartir a través de


¿Qué es la inserción de usuarios externos?

Importante

Esta característica está en versión preliminar pública.

En esta página se describe cómo funciona la inserción de usuarios externos, cómo configurar el área de trabajo de Azure Databricks para el uso compartido seguro de paneles insertados y cómo usar aplicaciones de ejemplo para empezar. La inserción para usuarios externos usa una entidad de servicio y tokens de acceso con ámbito para autenticar y autorizar el acceso a paneles incrustados. Este enfoque le permite compartir paneles con visores fuera de su organización, como asociados y clientes, sin aprovisionar cuentas de Azure Databricks para esos usuarios.

Para obtener información sobre otras opciones de inserción, incluidos los paneles de inserción para los usuarios de su organización, consulte Inserción de un panel.

Funcionamiento de la inserción para usuarios externos

El diagrama y los pasos numerados siguientes explican cómo se autentican los usuarios y los paneles se rellenan con resultados con ámbito de usuario al insertar un panel para usuarios externos.

Gráfico de flujo que muestra los intercambios de tokens necesarios en la aplicación y el área de trabajo de Databricks.

  1. Autenticación y solicitud de usuario: El usuario inicia sesión en la aplicación. El front-end de la aplicación envía una solicitud autenticada al servidor para un token de acceso del panel.
  2. Autenticación de entidad de servicio: El servidor usa el secreto de entidad de servicio para solicitar y recibir un token de OAuth del servidor de Databricks. Se trata de un token ampliamente de ámbito que puede llamar a todas las API de panel a las que Azure Databricks tiene acceso en nombre de la entidad de servicio. El servidor llama al /tokeninfo punto de conexión mediante este token, pasando información básica del usuario, como external_viewer_id y external_value. Consulte Presentar paneles de forma segura a usuarios individuales.
  3. Generación de tokens con ámbito de usuario: Con la respuesta del punto de /tokeninfo conexión y el punto de conexión de OpenID Connect (OIDC) de Databricks, el servidor genera un nuevo token de ámbito estricto que codifica la información del usuario que ha pasado.
  4. Representación del panel y filtrado de datos: La página de la aplicación crea DatabricksDashboard instancias de @databricks/aibi-client y pasa el token con ámbito de usuario durante la construcción. El panel se representa con el contexto del usuario. Este token autoriza el acceso, admite la auditoría con external_viewer_idy lleva external_value para el filtrado de datos. Las consultas de los conjuntos de datos del panel pueden hacer referencia __aibi_external_value para aplicar filtros por usuario, lo que garantiza que cada visor solo ve los datos que pueden ver.

Presentar paneles de forma segura a usuarios individuales

Configure el servidor de aplicaciones para generar un token de ámbito de usuario único para cada usuario en función de su external_viewer_id. Esto le permite realizar un seguimiento de las vistas del panel y el uso a través de los registros de auditoría. external_viewer_id se empareja con , external_valueque actúa como una variable global que se puede insertar en consultas SQL usadas en conjuntos de datos del panel. Esto le permite filtrar los datos que se muestran en el panel para cada usuario.

external_viewer_id se pasa a los registros de auditoría del panel y no debe incluir información de identificación personal. Este valor también debe ser único por usuario.

external_value se usa en el procesamiento de consultas y puede incluir información de identificación personal.

En el ejemplo siguiente se muestra cómo usar el valor externo como filtro en las consultas del conjunto de datos:

SELECT *
FROM sales
WHERE region = __aibi_external_value

Información general sobre la configuración

En esta sección se incluye información general conceptual de alto nivel de los pasos que debe realizar para configurar la inserción de un panel en una ubicación externa.

Para insertar un panel en una aplicación externa, primero debe crear una entidad de servicio en Azure Databricks y generar un secreto. A la entidad de servicio se le debe conceder acceso de lectura al panel y a sus datos subyacentes. El servidor usa el secreto de la entidad de servicio para recuperar un token que pueda acceder a las API del panel en nombre de la entidad de servicio. Con este token, el servidor llama al /tokeninfo punto de conexión de API, un punto de conexión de OpenID Connect (OIDC) que devuelve información básica del perfil de usuario, incluidos los external_value valores y external_viewer_id . Estos valores permiten asociar solicitudes a usuarios individuales.

Con el token obtenido de la entidad de servicio, el servidor genera un nuevo token con ámbito al usuario específico que accede al panel. Este token con ámbito de usuario se pasa a la página de la aplicación, donde la aplicación crea una instancia del DatabricksDashboard objeto de la @databricks/aibi-client biblioteca. El token incluye información específica del usuario que admite la auditoría y aplica el filtrado para que cada usuario solo vea los datos a los que están autorizados para acceder. Desde la perspectiva del usuario, el inicio de sesión en la aplicación proporciona automáticamente acceso al panel incrustado con la visibilidad de datos correcta.

Consideraciones de rendimiento y límites de velocidad

La inserción externa tiene un límite de velocidad de 20 cargas de paneles por segundo. Puede abrir más de 20 paneles a la vez, pero no más de 20 puede empezar a cargarse simultáneamente.

Prerrequisitos

Para implementar la inserción externa, asegúrese de cumplir los siguientes requisitos previos:

Paso 1: Creación de una entidad de servicio

Cree una entidad de servicio para que actúe como identidad de la aplicación externa en Azure Databricks. Esta entidad de servicio autentica las solicitudes en nombre de la aplicación.

Para crear una entidad de servicio:

  1. Como administrador del área de trabajo, inicia sesión en el área de trabajo de Azure Databricks.
  2. Haga clic en el nombre de usuario en la barra superior del área de trabajo de Azure Databricks y seleccione Configuración.
  3. Haga clic en Identidad y acceso en el panel izquierdo.
  4. Siguiente a Entidades de servicio, haga clic en Administrar.
  5. Haga clic en Agregar entidad de servicio.
  6. Haga clic en Agregar nuevo.
  7. Escriba un nombre descriptivo para la entidad de servicio.
  8. Haga clic en Agregar.
  9. Abra la entidad de servicio que acaba de crear a partir de la página de descripción de entidades de servicio. Use el campo Filtrar entrada de texto para buscarlo por nombre, si es necesario.
  10. En la página Detalles de la entidad de servicio , registre el identificador de aplicación. Compruebe que las casillas Acceso a SQL de Databricks y Acceso al área de trabajo están activadas.

Paso 2: Creación de un secreto de OAuth

Genere un secreto para la entidad de servicio y recopile los siguientes valores de configuración, que necesitará para la aplicación externa:

  • Identificador de entidad de servicio (cliente)
  • Secreto del cliente

La entidad de servicio usa un secreto de OAuth para comprobar su identidad al solicitar un token de acceso desde la aplicación externa.

Para generar un secreto:

  1. Haga clic en Secretos en la página Detalles de la entidad de servicio .
  2. Haga clic en Generar secreto.
  3. Escriba un valor de duración para el nuevo secreto en días (por ejemplo, entre 1 y 730 días).
  4. Copie el secreto inmediatamente. No puede volver a ver este secreto después de salir de esta pantalla.

Paso 3: Asignación de permisos a la entidad de servicio

La entidad de servicio que creó actúa como la identidad que proporciona acceso al panel a través de la aplicación. Sus permisos solo se aplican si el panel no está publicado con permisos de datos compartidos. Si se usan permisos de datos compartidos, las credenciales del publicador acceden a los datos. Para obtener más información y recomendaciones, consulte Inserción de enfoques de autenticación.

  1. Haga clic en Paneles en la barra lateral del área de trabajo para abrir la página de descripción del panel.
  2. Haga clic en el nombre del panel que desea insertar. Se abre el panel publicado.
  3. Haga clic en Compartir.
  4. Use el campo de entrada de texto en el cuadro de diálogo Uso compartido para buscar la entidad de servicio y, a continuación, haga clic en él. Establezca el nivel de permiso en CAN RUN. A continuación, haga clic en Agregar.
  5. Registre el identificador del panel. Puede encontrar el identificador del panel en la dirección URL del panel (por ejemplo, https://<your-workspace-url>/dashboards/<dashboard-id>). Consulte Detalles del área de trabajo de Databricks.

Nota:

Si publica un panel con permisos de datos individuales, debe conceder a la entidad de servicio acceso a los datos usados en el panel. El acceso de proceso siempre usa las credenciales del publicador, por lo que no es necesario conceder permisos de proceso a la entidad de servicio.

Para leer y mostrar datos, la entidad de servicio debe tener al menos SELECT privilegios en las tablas y vistas a las que se hace referencia en el panel. Consulte ¿Quién puede administrar privilegios?.

Paso 4: Uso de la aplicación de ejemplo para autenticar y generar tokens

Use una aplicación de ejemplo para practicar la inserción externa del panel. Las aplicaciones incluyen instrucciones y código que inicia el intercambio de tokens necesario para generar tokens con ámbito. Los siguientes bloques de código no tienen dependencias. Copie y guarde una de las siguientes aplicaciones.

Pitón

Copie y guárdelo en un archivo denominado 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 y guárdelo en un archivo denominado 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>`;
}

Paso 5: Ejecución de la aplicación de ejemplo

Reemplace los valores siguientes y, a continuación, ejecute el bloque de código desde el terminal. Los valores no deben estar rodeados por corchetes angulares (< >):

  • Use la dirección URL del área de trabajo para buscar y reemplazar los valores siguientes:
    • <your-instance>
    • <workspace_id>
    • <dashboard_id>
  • Reemplace los valores siguientes por los valores que creó al crear la entidad de servicio (paso 2):
    • <service_principal_id>
    • <service_principal_secret> (secreto de cliente)
  • Reemplace los siguientes valores por identificadores asociados a los usuarios de la aplicación externa:
    • <some-external-viewer>
    • <some-external-value>
  • Reemplace </path/to/example> por la ruta de acceso al .py archivo o .js que creó en el paso anterior. Incluya la extensión de archivo.

Nota:

No incluya ninguna información de identificación personal (PII) en el 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