Keycloak · Ingest de usuarios y logins
Servicio Flask en Cloud Run que lee la API admin de Keycloak (OAuth2 por realm) y carga en BigQuery los usuarios con sus roles y atributos (
keycloack_users, endpoint/extract) y, desde SOL-477, los eventos de usuario (LOGIN,LOGIN_ERROR,LOGOUT,IMPERSONATE) de cada realm (keycloack_events, endpoint/extract-events). Volumen bajo.
¿Llegaste hasta acá desde…?
| Si ves… | Eso sale de… | La lógica está en… |
|---|---|---|
| Tabla de usuarios de Keycloak en BigQuery | solvento-data-prod.keycloack.keycloack_users |
keycloack-ingest → cloud_run/keycloak_ingest/users.py |
| Roles / atributos de un usuario (tax_id, carrier_id, company_name, business_role) | Columnas de keycloack_users |
users.py (ATTRIBUTES y transform_user) |
| A qué realm pertenece un usuario | Columna realm de keycloack_users |
Itera los realms REALM_SUPPLIERS y REALM_PUBLIC_API (keycloak_ingest/config.py) |
| Logins / sesiones de un usuario | solvento-data-prod.keycloack.keycloack_events |
keycloack-ingest → cloud_run/keycloak_ingest/events.py |
Resumen
| Campo | Valor |
|---|---|
| Origen | API admin de Keycloak (https://auth.solvento.mx/), OAuth2 client_credentials por realm (CLIENT_ID=data-client) |
| Patrón | Pull por HTTP (Flask en Cloud Run) → carga a tabla temporal → MERGE en BigQuery |
| Destino | solvento-data-prod.keycloack.keycloack_users · solvento-data-prod.keycloack.keycloack_events |
| Realms (prod) | public-api (clientes; app carriers-login; llave de empresa carrier_id) y suppliers-portal-prod (proveedores; app suppliers-login; llave tax_id). Un secreto por realm: el client data-client existe por separado en cada uno |
| Frecuencia | Usuarios: diaria 0 2 * * * (job keycloack-ingest-daily, POST /extract, creado a mano) · Logins: diaria 0 3 * * * (job keycloack-events-daily 🔧, POST /extract-events, en terraform) |
| Criticidad | 🟢 Baja |
| Dueño | Martin (data eng) |
| Deploy | Imagen con ./publish_container.sh prod → tag de fecha en terraform-gcp-infrastructure (servicio keycloack-ingest-prod, timeout 1800s). Imagen con keycloack_events: 20261006-194846 |
| Repos | keycloack-ingest |
Diagrama de linaje
graph LR
SCH[["⏰ Cloud Scheduler<br/>keycloack-ingest-daily · 0 2 * * *<br/>POST /extract"]]
KC[["🔐 Keycloak<br/>API admin · realms"]]
SVC["Cloud Run<br/>keycloack-ingest-prod (Flask)"]
BQ[("solvento-data-prod<br/>keycloack.keycloack_users<br/><i>MERGE ON id</i>")]
SCH2[["⏰ Cloud Scheduler<br/>keycloack-events-daily · 0 3 * * *<br/>POST /extract-events"]]
BQ2[("solvento-data-prod<br/>keycloack.keycloack_events<br/><i>MERGE ON event_id</i>")]
SCH -->|dispara| SVC
SCH2 -->|dispara| SVC
KC -->|"OAuth2 + usuarios + roles"| SVC
KC -->|"eventos LOGIN, LOGIN_ERROR, LOGOUT, IMPERSONATE (view-events)"| SVC
SVC -->|"tabla temporal + MERGE por realm"| BQ
SVC -->|"tabla temporal + MERGE"| BQ2
classDef src fill:#fde68a,stroke:#b45309,color:#000;
classDef store fill:#bfdbfe,stroke:#1e40af,color:#000;
class SCH,SCH2,KC src;
class BQ,BQ2 store;
Paso a paso (POST /extract)
- Disparo: Cloud Scheduler
keycloack-ingest-daily(0 2 * * *) hacePOST /extract. También se puede llamar a mano; hayGET /health. - Autenticación por realm: en prod el servicio usa
REALM_SUPPLIERS=suppliers-portal-prod(+ realmpublic-api) sobreKEYCLOACK_URL=https://auth.solvento.mx/. Cada realm obtiene su token OAuth2 (grant_type=client_credentials,CLIENT_ID=data-client) con su propio secreto de Secret Manager (CLIENT_SECRET_SUPPLIERS,CLIENT_SECRET_PUBLIC_API). Los tokens duran ~60 s y se renuevan solos. - Extracción: pagina usuarios (50 por página); por cada usuario consulta sus roles de realm y toma el primer valor de los atributos (
taxId,carrier_id,company_name,business_role,type,phone, etc.). - Carga: por realm, carga las filas a una tabla temporal
keycloack_users__staging_*y hace unMERGEcon claveON T.id = S.id:WHEN MATCHEDactualiza todos los campos eingestion_updated_at;WHEN NOT MATCHEDinserta coningested_ateingestion_updated_at. Los textos vacíos quedan como''. - Respuesta: JSON con conteos por realm (
users,inserted,updated) yerrors. Responde 500 si falló algún realm (el otro se carga igual), así Cloud Scheduler marca la corrida como fallida. Ya no devuelve la lista de usuarios.
Incidente corregido (2026-10-06): hasta la imagen
20260519-105801elMERGEse armaba concatenando los valores en el SQL, con''como escape (BigQuery no lo acepta). Un apóstrofe en un nombre o una empresa rompía la página entera: la páginafirst=1400desuppliers-portal-prod(50 usuarios) fallaba todos los días y el endpoint igual respondía 200. Desde20261006-185724los valores no pasan por el texto del SQL.Nota: la lógica de roles y atributos vive en el propio servicio, no en dbt.
Config prod (terraform):
GCP_PROJECT_ID=solvento-data-prod,BIGQUERY_DATASET=keycloack,BIGQUERY_USERS_TABLE=keycloack_users. Los.envdel repo (dev.env/prod.env) son solo para el build de la imagen; el runtime y los secretos vienen de Terraform y Secret Manager. Tabla completa de variables en el README del repo.
Paso a paso (POST /extract-events) — eventos de usuario (SOL-477)
- Disparo: Cloud Scheduler
keycloack-events-daily(0 3 * * *MX, body{"days_back":3}). Carga inicial:{"days_back":10}(todo lo que retiene Keycloak). - Token por realm: mismo
data-clienty secretos que/extract. El service account necesita el rolview-events(clientrealm-management) además deview-users/query-users/query-groups. Asignado en prod y staging el 2026-10-02. - Extracción:
GET /admin/realms/{realm}/events?type=LOGIN&type=LOGIN_ERROR&type=LOGOUT&type=IMPERSONATE&dateFrom=<hoy-N>paginado (500 por página). Tipos configurables conEVENT_TYPES. Keycloak devuelve portimeDESC: si entran eventos mientras se pagina aparecen duplicados, nunca huecos. - Transformación: una fila por evento. Se guarda el nombre del realm (el
realmIddel evento es el id interno, p. ej.suppliers-prod).detailsva como JSON con claves permitidas (auth_method,auth_type,grant_type,redirect_uri,response_type,response_mode,remember_me,identity_provider,restart_after_timeout,redirected_to_client,impersonator_realm,impersonator_id). Nunca se guardanipAddress,details.username(email, o lo tipeado en unLOGIN_ERROR),details.impersonator(usuario de soporte) ni ids de tokens. - Carga: load a una tabla temporal
keycloack_events__staging_*→MERGEsolo-INSERT porevent_id→ borra la temporal. Idempotente: repetir días no duplica. La tabla destino se crea sola (CREATE TABLE IF NOT EXISTS) en la primera corrida.
Cómo medir uso (leer antes de armar una métrica): - Solo
type = 'LOGIN'yclient_id IN ('carriers-login', 'suppliers-login')para uso del portal.client_idigual al nombre del realm (public-api,suppliers-portal-prod) son logins por fuera del portal (probablemente API; confirmar conJSON_VALUE(details, '$.grant_type')). - Sesiones =COUNT(DISTINCT session_id), nuncaCOUNT(*): el frontend a veces entra en un loop de redirecciones en/keycloak-authy genera decenas deLOGINde la misma sesión en segundos (visto: 18 en 18 s). La métrica más robusta es días activos (COUNT(DISTINCT DATE(event_ts, 'America/Mexico_City'))). - Excluir las sesiones que tengan unIMPERSONATE: es soporte entrando como el usuario, no uso del cliente. -LOGINsinauth_typees válido (login completado después de que venció la pantalla de login). - Keycloak no guardaREFRESH_TOKEN: solo se ven inicios y cierres de sesión, no actividad dentro de la sesión. - De usuario a empresa:JOIN keycloack_users ON id = user_id→carrier_id(public-api) otax_id(suppliers).redirect_uridice a qué pantalla iba (?redirect=/loads/list).-- Actividad por usuario (portal, sin sesiones impersonadas) SELECT e.realm, e.user_id, COUNT(DISTINCT DATE(e.event_ts, 'America/Mexico_City')) AS dias_activos, COUNT(DISTINCT e.session_id) AS sesiones, MAX(e.event_ts) AS ultimo_login FROM `solvento-data-prod.keycloack.keycloack_events` e WHERE e.type = 'LOGIN' AND e.client_id IN ('carriers-login', 'suppliers-login') AND e.session_id NOT IN ( SELECT session_id FROM `solvento-data-prod.keycloack.keycloack_events` WHERE type = 'IMPERSONATE' AND session_id IS NOT NULL) GROUP BY 1, 2Historia: Keycloak solo guarda 10 días. La tabla arranca con la carga inicial (
days_back: 10) y desde ahí acumula; no hay forma de traer eventos más viejos.keycloack_eventsreemplazó akeycloack_login_events(solo LOGIN, sindetails) el 2026-10-06: se recargaron los 10 días y la vieja se borra.
Tablas que alimenta
| Tabla | Contenido | Job que la carga |
|---|---|---|
solvento-data-prod.keycloack.keycloack_users |
Usuarios de Keycloak (id, username, email, phone, user_id, tax_id, company_name, business_role, remote_auth, user_type, supplier_id, carrier_id, roles, realm, created_at, enabled, totp, timestamps de carga) | Cloud Run keycloack-ingest (POST /extract, MERGE por id) |
solvento-data-prod.keycloack.keycloack_events |
Eventos LOGIN, LOGIN_ERROR, LOGOUT, IMPERSONATE (event_id, event_ts, realm, type, client_id, user_id, session_id, auth_type, error, details JSON, ingested_at). Partición DATE(event_ts), cluster realm, type, user_id |
Cloud Run keycloack-ingest (POST /extract-events, MERGE por event_id) |
DDL de referencia: sql/ddl.sql en el repo.
Consumo aguas abajo
- Análisis de usuarios / accesos sobre
keycloack_users. No hay modelado dbt conocido sobre esta tabla. - Sesiones y días activos por usuario sobre
keycloack_events(pedido de Customer Success para priorizar contacto a usuarios sinbusiness_role). Sin modelo dbt todavía. - Calidad de frontend: sesiones con muchos
LOGINen pocos segundos = loop de redirecciones en/keycloak-auth.
Si falla 🛠️
Impacto: bajo. Las tablas quedan desactualizadas hasta la próxima corrida. Los logins sí se pueden perder si la caída dura más de 10 días.
| Síntoma | Dónde mirar | Acción |
|---|---|---|
/extract o /extract-events devuelve 400 "Faltan variables de entorno" |
Variables y secretos del servicio en terraform | Corregir en terraform.tfvars (keycloack-ingest-prod); los secretos vienen de Secret Manager |
Responde 500 con errors: [{realm, error}] |
Logs Cloud Run keycloack-ingest-prod (JSON, jsonPayload.event="keycloak_run") |
El realm que falló no se cargó; el otro sí. Corregir y volver a disparar el endpoint (es idempotente) |
| Error de auth contra Keycloak | KEYCLOACK_URL, CLIENT_ID, secretos por realm |
Verificar credenciales y URL del realm |
/extract-events falla con 403 en /admin/realms/{realm}/events |
Roles del service account de data-client en ese realm |
Asignar view-events (client realm-management). Lo administra backend (Fede) |
| Faltan días de eventos | ¿El job keycloack-events-daily estuvo caído más de 10 días? |
Keycloak solo retiene 10 días: lo anterior no se recupera. Si fueron menos, correr /extract-events con {"days_back":10} |
Escalación: SQL / Metabase → Diego · Airbyte → Gero · resto → Martin.
Enlaces
- 📊 Estado en vivo: dashboard "Data Processes" (Cloud Monitoring)
- 📦 Servicio:
keycloack-ingest— entradacloud_run/app.py, lógica encloud_run/keycloak_ingest/
Última actualización: 2026-10-06 (tabla keycloack_events con details y LOGIN_ERROR/LOGOUT/IMPERSONATE; antes: refactor a cloud_run/, fix del MERGE de usuarios) · Si cambia el flujo, los realms o el destino, actualizar esta ficha en el mismo MR.