Buscar en la documentación

Módulo Audit

Auditoría global de solo lectura sobre el historial de incidentes y el registro de administración, con permisos separados, alcance por jerarquía y paginación por cursor.

5 min de lecturaVer en GitHub

Muestra lo que ya guardan otros módulos: el historial de todos los incidentes (incident_activities) y el registro de la administración (admin_actions). No escribe nada: solo lee, filtra y decide quién ve qué. Cada registro trae la IP y el navegador desde donde se hizo, que el historial de un incidente no muestra.

Endpoints

Todos exigen sesión.

MétodoRutaPermisoRespuesta
GET/api/v1/audit/catalogaudit.incidents.view o audit.admin.viewTipos de cada pestaña que se puede ver (la otra va en null), roles y permisos (solo con audit.admin.view) y personas
GET/api/v1/audit/incidentsaudit.incidents.viewHistorial de todos los incidentes, también de los eliminados
GET/api/v1/audit/adminaudit.admin.viewRegistro de administración dentro del alcance de quien consulta

Parámetros de las dos listas:

ParámetroQué hace
filter[from], filter[to]Rango (ISO 8601). Sin fechas, los últimos 30 días. Con solo to, los 30 días anteriores. Con solo from, hasta ahora
filter[actor]ULID de quien hizo el cambio
filter[type]Códigos de tipo, separados por comas
filter[incident]Solo incidentes: el código como lo escriba una persona (TZ-26-0142, tz26-142 o 26-142; el año es obligatorio)
filter[user], filter[role]Solo administración: persona afectada (ULID) o rol afectado (código; también encuentra un rol eliminado)
cursorEl meta.next_cursor de la respuesta anterior
per_pageDe 1 a 100; 25 por defecto

Un filtro que apunta a algo que no existe (una persona, un tipo o un incidente) devuelve la lista vacía.

GET /api/v1/audit/incidents?filter[incident]=26-629

{
  "data": [{
    "id": 3159,
    "created_at": "2026-10-24T21:54:57+00:00",
    "type": { "code": "status_changed", "name": "Cambio de estado" },
    "user": { "id": "01m5nzqh9rk9w9sr18d87hwkat", "name": "Carlos Mejía" },
    "incident": { "id": "01m5qyhqrkxexrgj35w0b3fh9y", "code": "TZ-26-0629", "title": "…", "deleted": false },
    "changes": { "status": { "from": { "code": "open", "name": "Abierto" }, "to": { "code": "in_progress", "name": "En progreso" } } },
    "ip_address": "172.20.0.1",
    "user_agent": "Mozilla/5.0 …",
    "links": { "incident": true }
  }],
  "meta": { "next_cursor": "MjAyNi0xMC0yNCAyMTo1NDo1NHwzMTU4", "from": "2026-09-24T…", "to": "2026-10-24T…" }
}

En la administración, cada registro trae target_user y target_role en lugar de incident, y details en lugar de changes. Un rol eliminado llega con deleted: true y con el código y el nombre que quedaron en el detalle.

Quién ve qué

RolIncidentesAdministración
SupervisorTodo—
Administrador—Lo propio y lo que toca a personas o roles por debajo de su nivel
Super adminTodoTodo
  • Dos permisos. Con un solo audit.view, el Administrador veía los incidentes de todos y el Supervisor veía la administración. Los dos se pueden conceder a una persona puntual.
  • Alcance de la administración (AdminAuditScope). Para quien no es la cima, un registro se ve si lo hizo esa persona, o si la persona o el rol afectados están por debajo de su nivel.
    • El nivel de una persona es el de su rol más alto hoy.
    • Un rol eliminado se mide por el nivel guardado en el detalle.
    • Un registro sin persona ni rol afectado solo lo ve quien lo hizo y la cima. Es el caso de las exportaciones de incidentes (incidents_exported), que guardan el número de filas y los filtros.
    • La misma regla está en SQL (DatabaseAdminAudit) y una prueba compara las dos registro por registro.
  • Enlaces (links): el back dice si quien consulta puede abrir lo que se nombra.
    • El incidente, con la misma regla del detalle (IncidentAccess).
    • La ficha de la persona, con users.view.
    • La del rol, con roles.manage y si el rol todavía existe.

Cursor

  • Orden. De lo más reciente a lo más antiguo, por (created_at, id). El id desempata los registros del mismo segundo.
  • Cursor. Es la fecha y el id del último registro entregado, en base64 sin relleno: opaco para el front. Uno alterado responde 422 con INVALID_CURSOR.
  • Fin de la lista. Se pide una fila de más: si llega, hay otra tanda y next_cursor trae su cursor; si no, va en null.
  • Sin total. No se cuenta la tabla. Contar recorrería todo el rango, cada vez más grande.

Rendimiento medido sobre 523.088 filas: menos de 2 ms en cada caso, sin ordenar en memoria (ADR-0056).

Errores

CódigoHTTPCuándo
FORBIDDEN_ACTION403Sin el permiso de la pestaña
INVALID_CURSOR422El cursor no es uno que entregó la API
INCIDENT_CODE_FORMAT422filter[incident] no tiene forma de código
DATE_TOO_EARLY422filter[to] es anterior a filter[from]

Capas

CapaPiezaQué hace
DomainAuditAccessPermisos de cada pestaña y alcance de la administración
DomainAdminAuditScope, AuditPeriodRegla “solo hacia abajo”; rango por defecto. El cursor (TimeCursor) vive en Shared: también lo usan los eventos del sistema
ApplicationListIncidentAudit, ListAdminAudit, ReadAuditCatalogCasos de uso: revisan el permiso y piden la consulta
ApplicationPorts/*, Data/*Contratos de las consultas y los datos que devuelven
InfrastructureDatabaseIncidentAudit, DatabaseAdminAuditConsultas en SQL. Traducen ULID, tipos e incidente a id antes de consultar, para usar el índice que corresponde
InfrastructureAuditWindow, DatabaseAuditCatalogRango, cursor y fila de más, comunes a las dos; catálogos
HttpAuditController, AuditResourceRespuesta con meta y links
HttpRequests/AuditRequest (+ IncidentAuditRequest, AdminAuditRequest)Revisan el permiso antes de validar, igual que la administración (ChecksPermission)
HttpRules/IncidentCode y Rules/Cursor (de Shared)Código de incidente normalizado a TZ-26-0142 (con IncidentCode del dominio de Incidentes) y cursor válido

Quien consulta se arma con RequestAdmin del módulo Users: nivel y permisos efectivos.

Dónde está en el código