Skip to content

feat(api-externa): expone indicadores del Widget Engine vía API M2M

Infraestructura requested to merge qa into main

El Portal AUP necesita alimentar sus KPIs con indicadores del portal, pero por política de seguridad no puede conectarse a la base del data lake. El JWT actual no sirve para eso: está atado a una fila de users y expira a las 8h, así que un consumidor tendría que guardar la contraseña de un empleado y re-loguearse cada 8 horas.

Se agrega una fachada estable sobre el Widget Engine, con credencial de larga vida, revocable y de alcance acotado.

Endpoints M2M (X-API-Key): GET /api/external/v1/metrics catálogo de lo expuesto GET /api/external/v1/metrics/{key} un escalar (1 fila, 1 número) GET /api/external/v1/widgets/{key} filas completas (series, tablas)

Administración (JWT admin): emitir/revocar keys, ver consumo, definir modo de exposición por widget y verificar un widget antes de exponerlo.

Tres compuertas independientes, todas deben pasar:

  1. key válida, activa, no revocada, no expirada, con scope metrics:read
  2. categoría del widget dentro del alcance del key
  3. widget_definitions.external_mode habilita el endpoint pedido

external_mode es de tres valores ('none'|'scalar'|'full') y no un booleano porque exponer el agregado y exponer el desglose son decisiones distintas: fin.ebitda_total devuelve un consolidado, fin.top_empresas_ebitda devuelve el EBITDA empresa por empresa. Con un booleano, voltearlo pasaría en silencio de lo primero a lo segundo. Un widget en 'scalar' da 404 en /widgets.

Notas de seguridad:

  • El key crudo se devuelve una sola vez; la BD guarda solo su SHA-256.
  • 401 idéntico para key desconocida, revocada, inactiva y expirada, para que no se pueda distinguir una de otra. El motivo real va al log de la app.
  • 404 idéntico para inexistente, 'none' y "expuesto en otro modo", para no permitir enumerar el catálogo interno.
  • Los fallos de auth no escriben en la bitácora de BD: sería un amplificador de escrituras ante un barrido de keys.
  • Params en modo estricto: un param no declarado es 400, no se ignora.
  • Tope de filas configurable con bandera truncated explícita.

Default cerrado: la migración no expone ningún widget.

Verificación: 44 pruebas nuevas (144 en total en la suite unitaria), grafo de modelos OK, migración aplicada dos veces contra Postgres 15 para comprobar idempotencia y que los widgets preexistentes quedan en modo 'none'.

Pendientes conocidos: rate limiting y pantallas de admin en React.

Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com

Merge request reports

Loading