# ETAPA AUDIT LOG — Registro de auditoría append-only (Sprint 5, SEC-002) ## 1. Objetivo Implementar un registro de auditoría append-only en Postgres que loggee eventos de configuración crítica: cambios en `user_roles` (altas/bajas/modificaciones de acceso) y cambios de `status` en `surveys` (activación/desactivación de encuestas). Opcionalmente, si la implementación es simple y sin fricción, agregar también un trigger DDL para loggear migraciones aplicadas. ## 2. Contexto - Migración de referencia para patrones: `20260627000001_auth_user_roles.sql` (patrón SECURITY DEFINER + GRANT puntual a reutilizar). - Helpers RLS existentes a reusar: `public.current_app_role()` y `public.current_org_id()` — no reinventar. - Gap conocido y aceptado (SEC-003): esto NO protege contra TRUNCATE/DROP ejecutado por superusuario directo en el VPS. Documentar en comentario de la migración, no resolver. - Acceso a datos sensibles: NO loggear en esta etapa — hoy no existe código que lea columnas `is_sensitive=true`. Deferido a cuando ese código exista. - Naming de migración: después de `20260627000001` — usar timestamp del día de ejecución, formato `YYYYMMDDHHMMSS`. ## 3. DECISIONES YA TOMADAS — no consultar - Tabla nueva: `public.audit_log`, append-only real. - Ningún rol (`anon`, `authenticated`, `service_role` vía API) tiene INSERT/UPDATE/DELETE directo en `audit_log`. Solo escriben funciones SECURITY DEFINER de los triggers. - Segunda barrera: trigger `BEFORE UPDATE OR DELETE ON audit_log` que aborta con excepción — defensa en profundidad. - Lectura de audit_log: RLS, solo `platform_admin` via `current_app_role()`. - Patrón de función trigger: SECURITY DEFINER, propiedad de `postgres`, GRANT EXECUTE solo al rol que lo necesita (no a PUBLIC). - Columnas de audit_log: ```sql id bigserial PRIMARY KEY, occurred_at timestamptz NOT NULL DEFAULT now(), actor_role text, actor_user_id uuid, action text NOT NULL, target_table text NOT NULL, target_id text, detail jsonb ``` - Triggers obligatorios (Prioridad 1): - AFTER INSERT/UPDATE/DELETE en `user_roles` - AFTER UPDATE en `surveys` cuando cambia `status` - Trigger DDL (Prioridad 2 — opcional): solo implementar si es simple y sin fricción. Si requiere extensión nueva o configuración de infraestructura, NO incluir y reportarlo. ## 4. Tareas ### 4.1 Diagnóstico previo — DDL trigger Antes de implementar, verificar si el DDL trigger (`ddl_command_end`) está disponible sin extensiones adicionales en esta versión de Postgres self-hosted: ```bash docker exec -i supabase-db psql -U postgres -d postgres -c " SELECT event_object_schema, trigger_name FROM information_schema.triggers WHERE trigger_name LIKE '%ddl%' LIMIT 5;" ``` Reportar si es viable sin fricción. Si requiere configuración extra, documentarlo y omitirlo de esta etapa. ### 4.2 Migración principal Archivo: `supabase/migrations/YYYYMMDDHHMMSS_audit_log.sql` Contenido en orden: 1. Crear tabla `audit_log` con columnas definidas arriba. 2. RLS en `audit_log`: solo `platform_admin` puede SELECT. 3. Trigger `BEFORE UPDATE OR DELETE ON audit_log` → función que lanza excepción (append-only enforcement). 4. Función `log_user_roles_change()` SECURITY DEFINER → INSERT en audit_log. 5. Trigger AFTER INSERT/UPDATE/DELETE en `user_roles` → llama a la función. 6. Función `log_survey_status_change()` SECURITY DEFINER → INSERT en audit_log solo cuando `NEW.status IS DISTINCT FROM OLD.status`. 7. Trigger AFTER UPDATE en `surveys` → llama a la función. 8. Si DDL trigger es viable (4.1): función + trigger DDL. 9. Comentario al final documentando el gap SEC-003. ### 4.3 Verificación en VPS NO aplicar la migración — solo generar el archivo y reportar el SQL completo para revisión del director. El director aplica manualmente después de aprobar. ## 5. DoD antes de reportar - [ ] `git log --name-only -1` — solo el archivo de migración - [ ] SQL completo de la migración en el reporte (no resumido — el director necesita leerlo antes de aplicar) - [ ] Diagnóstico DDL trigger reportado (4.1) - [ ] Build sin errores: `npm run build` - [ ] Commit + push a Gitea - [ ] NO aplicado en VPS — esperar aprobación del director ## 6. Qué reportar ``` Diagnóstico DDL trigger (4.1): - ¿Viable sin extensiones? ✓/✗ - Si no: razón y alternativa SQL completo de la migración: [pegar el SQL íntegro — no resumido] Verificado: - Build: ✓/✗ - git log --name-only -1: [lista] - Commit: [hash] NO aplicado en VPS — pendiente aprobación del director. ``` ## 7. Qué NO hacer - ❌ No aplicar la migración en el VPS sin aprobación explícita del director - ❌ No implementar logging de accesos a datos sensibles (deferido) - ❌ No agregar extensiones nuevas a Postgres - ❌ No tocar el formulario público, las RPCs de dashboard, ni el middleware de auth - ❌ No usar GRANT EXECUTE a PUBLIC — solo al rol específico que necesita cada función - ❌ No omitir el comentario del gap SEC-003 en la migración ## 8. Versionado ``` feat(compliance): audit_log append-only — user_roles y surveys.status [SEC-002] Tabla audit_log con RLS (solo platform_admin), trigger BEFORE UPDATE/DELETE que aborta (append-only enforcement), triggers AFTER en user_roles y surveys.status. Patrón SECURITY DEFINER reutilizado de auth_user_roles. Gap SEC-003 (TRUNCATE por superusuario) documentado y aceptado. ```