Volver a los textos

El log no es una sola cosa

Operación, concesión y acceso tienen volumen, retención y criticidad distintos. Y no todo fallo del rastro debe tener la misma consecuencia.

Casi todo sistema tiene una tabla audit_log con una columna JSONB donde cabe todo: login, error, cambio de configuración, concesión de role, apertura de documento. Funciona hasta que alguien hace una pregunta de gobernanza de verdad, y entonces consultar se vuelve arqueología.

Yo separo el rastro en tres planos, porque responden a preguntas distintas:

PlanoPreguntaEjemplos
Operativo¿Qué pasó en el sistema?login, error, configuración, procesamiento
Concesión¿Quién dio qué poder a quién, y cuándo?roles, grants directos, shares, revocaciones
Acceso¿Quién vio o consultó qué contenido?apertura, descarga, impresión, consulta de microdatos

Cada plano tiene volumen, retención, índices y criticidad propios. La concesión es poco volumen y mucho valor probatorio. El acceso es volumen alto y pide particionado por tiempo. El operativo es ruido útil. Mezclarlo todo hace que un rastro importante dependa de un writer pensado para otra finalidad.

Estado no es evento

Una tabla user_role dice quién tiene el role hoy. No dice quién lo tenía hace tres meses. Para eso existe una tabla append-only de eventos. Las dos son necesarias, y una no sustituye a la otra.

Una tabla de estado dice quién tiene acceso. Solo el rastro de eventos dice cómo llegó ahí.

Asimetría de fallo

No todo fallo de auditoría debe tener la misma consecuencia:

NaturalezaSi el rastro fallaPor qué
operativobest-effortel hecho puede haber ocurrido ya
concesiónfallo cerradoun poder invisible es inaceptable
acceso sensiblefallo cerradoun acceso sin evidencia rompe la accountability

Es una decisión regulatoria y operativa, y tiene un coste: fail-closed pone el storage del rastro en el camino crítico. Antes de adoptarlo, mide el p50 y el p99 del insert, la tasa de error causada por el rastro, el crecimiento diario, el bloat y el comportamiento del pool. La política no puede exigir una disponibilidad que la infraestructura no se dimensionó para sostener.

Concesión y evento en la misma transacción

BEGIN;
  INSERT INTO grant_event (...);
  INSERT INTO user_role (...);
  UPDATE app_user
     SET permission_version = permission_version + 1
   WHERE id = ...;
COMMIT;

Si cualquier paso falla, rollback de todo. Un evento que afirma una concesión que no existe es tan malo como una concesión sin evento. Sin un Unit of Work global, un helper estrecho lo resuelve:

security_transaction(operation):
    begin
    operation(shared_session)
    commit_or_rollback

Y la regla que le da dientes al helper: cualquier call site de concesión fuera de él es un bug estructural, y el CI lo busca.

Registrar antes de servir

Cuando el acceso sensible es fail-closed, el orden es autorizar, registrar el evento y solo entonces serializar y devolver el contenido. El error residual es registrar un acceso que no llegó al usuario porque algo falló después. Ese error “por exceso” es auditable. Lo inverso, contenido entregado sin evento, es exactamente lo que la política quiere impedir.

Registra la vía

Cuando alguien abre un recurso, vale la pena guardar por qué camino se satisfizo la autorización:

access_via = own_partition | share | all_partitions | platform_bypass | legacy

Si el share se revoca mañana, el rastro sigue explicando el acceso de hoy sin necesidad de reconstruir el estado pasado. Cuando más de una vía es verdadera a la vez, define una precedencia y mantenla, o guárdalas todas. Lo importante es no dejar ambigüedad histórica.

Qué no registrar

El polling de la interfaz no es acceso. Listar metadatos y abrir contenido son eventos distintos, y guardar cada refresh automático genera un volumen enorme con poco valor probatorio. Define qué cuenta como acceso antes de encender el writer.

Y recuerda que el propio rastro es dato personal: identidad, IP, hora, objeto accedido, patrón de uso. Necesita control de acceso, retención explícita y, en los entornos más exigentes, un usuario de aplicación sin UPDATE ni DELETE sobre él.