Skip to content

Auditoría de Identidad, Organización y Acceso — revisión 9

Estado: H05 está cerrado y su garantía se define en DECISIONS.md, no aquí. Este documento conserva la evidencia de las baterías de persistencia: transferencia, bloqueo y recuperación privilegiados, cierre terminal de soporte, ciclo de PIN y revocación de sesiones por credenciales comprometidas.

Alcance y evidencia

Esta revisión reúne contratos de persistencia del baseline SQL y el contrato de ingreso de la aplicación. La evidencia SQL por sí sola no certifica un flujo de autenticación: insertar verified_at no demuestra que un correo recibió un OTP, ni insertar una sesión demuestra que se verificó una contraseña.

Base revisada: 1764cf6bfa8227cc3e2500490a34430f3695389c, PR #1. La reproducción se publicó primero en 29b2e9fd5987a80d2029704b2bc56f8516e0d74e: ejecución roja de PostgreSQL. Las regresiones anteriores pasaron; el nuevo script reportó 27 fallos de 35 comprobaciones. 25 corresponden a violaciones de estado reproducidas; dos fueron expectativas incorrectas del test: TN01 recibía una validación de negocio antes de RLS y SC01 chocaba con unicidad antes de probar el traslado de alcance. Ambas pruebas fueron corregidas. No son 27 vulnerabilidades de una API publicada.

PASS PostgreSQL 18.6 del código ad72ce751c8e14db335feec9286a34595da3e9a0; GitHub ejecutó su merge de prueba dbb7c307f41c00d3eaf462b742451c38972de541 con main. Se instalaron 41 SQL, 808 tablas, 89 filas iniciales y 722 tablas tenant con RLS. Pasaron las regresiones anteriores y las 52 comprobaciones nuevas, sin fallos. H05 se reprodujo por separado en ese momento y se cerró después; database/tests/contracts/administration-contracts.ts es su batería actual.

GrupoComprobaciones aprobadas
Alta y rollback5
Principal e identificador7
Desafíos, intentos y consumo concurrente9
Credenciales, TOTP y recuperación3
Sesiones y bloqueo concurrente7
Invitaciones7
Tenant y contexto de conexión5
Alcances y concurrencia5
Vigencia de soporte4
Total52

El script genera test-results/identity-adversarial.json y su log, disponibles en el artifact de la ejecución. Las pruebas concurrentes de sesiones y alcances comprueban contención con pg_blocking_pids; el consumo de desafíos comprueba un único ganador. No se agregaron tablas ni un framework de estados. Es un baseline de instalación limpia; bases existentes necesitan una migración y revisión de datos históricos antes de aplicar estas restricciones.

Una fuente de verdad por entidad

No se agrega un motor genérico ni un segundo estado que contradiga las columnas existentes.

EntidadFuente de estadoTransiciones y sentido
Principal globalidentities.statusactive, blocked, disabled, archived. Archivado es terminal. Activo significa disponible; no significa correo verificado, sesión autenticada ni acceso a un tenant. El tipo humano/servicio no cambia.
Identificador de accesologin_identifiers.verified_at/revoked_atPendiente → verificado; pendiente o verificado → revocado. Revocado prevalece. Cambiar correo/teléfono requiere otro registro y verificación; no se transfiere la verificación anterior.
Método de autenticaciónauthentication_methods.status/revoked_atActivo ↔ deshabilitado mientras no esté revocado. Comprometido o revocado requiere reemplazo. El cambio a comprometido registra revocación.
Desafío de autenticaciónconsumed_at/revoked_at/expires_at/attempt_count/max_attemptsNuevo pendiente; consumido, revocado, vencido o agotado no es utilizable. Consumo/revocación son terminales; los intentos no retroceden ni se modifica sujeto, propósito, secreto o plazo.
TOTP / código de recuperaciónVerificación y revocación / uso y revocaciónUn secreto TOTP nuevo requiere otro enrolamiento; un código usado no vuelve a estar disponible. La verificación criptográfica pertenece al servicio Auth.
Sesiónsessions.revoked_at/expires_atEmitir exige principal activo. Revocada o vencida no admite rotación. Dueño y plazo no cambian; renovar duración requiere otra sesión. Bloquear/deshabilitar/archivar revoca sesiones existentes; reactivar el principal no las resucita.
Invitaciónaccepted_at/revoked_at/expires_atNueva pendiente → aceptada o revocada; el vencimiento se deriva del reloj. Aceptar exige humano activo con el identificador invitado verificado y vigente. Destinatario, prueba y plazo no cambian. Se congela su configuración al aceptar, revocar o vencer.
Acceso a organizaciónmemberships.statusActivo ↔ suspendido; activo/suspendido → terminado. Terminado es terminal; un reingreso crea otra membresía. Suspender aquí afecta a esta organización; bloquear el principal afecta a todas.
Soporte con suplantaciónstarted_at/expires_at/ended_atAntes del inicio no autoriza; después del vencimiento tampoco. La comprobación usa el reloj de cada sentencia, incluso en una transacción larga. Se puede registrar el cierre después del vencimiento.

El acceso efectivo debe conjugar principal activo, organización activa, membresía activa, permiso y alcance vigentes, capacidad contratada y admisión del comando. RLS aísla tenants; no sustituye verificar que el actor tiene permiso para administrar roles. El contexto SQL lo establece un backend confiable después de validar la sesión; nunca debe provenir libremente del cliente.

La cuenta de acceso (Identity) es distinta del cliente persona/empresa (Party) y de su cuenta corriente/deuda. Un cliente puede acumular ventas sin tener login. La organización es el tenant; puede contener varias entidades legales/RUC, sucursales y cajas. Los permisos delimitan dónde opera cada persona; no fusionan deudas, saldos ni titularidad legal entre RUC. El catálogo y el surtido siguen siendo contratos separados de identidad.

Flujos que debe implementar el primer módulo de aplicación

Primer módulo: Identidad y acceso, incluyendo alta y pertenencia a organizaciones. Organización constituye su contexto. No iniciar Ventas mientras la admisión de comandos todavía permita confundir al actor, al tenant o su alcance.

  1. Alta: normalizar identificador, aplicar controles de abuso, generar hash de contraseña en el servicio Auth; persistir principal, identificador pendiente, método y desafío en una transacción. Enviar la verificación después de confirmar, con una entrega reintentable. Un duplicado debe revertir el alta completa. No crear una membresía por el hecho de registrarse.
  2. Verificación: comprobar secreto, propósito, destinatario, vencimiento e intentos; consumir condicionalmente la prueba y verificar el identificador en la misma transacción. Dos consumidores no deben ganar. Una entrega repetida no debe repetir efectos. Una prueba fallida no debe perder el incremento de intentos por un rollback mal diseñado.
  3. Ingreso y renovación: el ingreso exige contraseña válida, identificador verificado y elegibilidad revalidada hasta confirmar la sesión. Cada rotación conserva el hash gastado en identity.session_refresh_token_history dentro de la misma transacción. Reutilizar cualquier token gastado revoca su sesión, incluso después de varias renovaciones; la revocación se confirma antes de devolver el rechazo. Un token desconocido no revoca sesiones ajenas. POST /sign-outs revoca la sesión que llama y POST /sign-outs/everywhere revoca todas las sesiones vivas del principal, incluida la que llama; ambas dejan sin renovación los refresh tokens alcanzados. El access token ya emitido no es stateless en el guard: SessionGuard verifica en cada petición autenticada, tras validar la firma, que la sesión nombrada en el token sigue viva (no revocada, no vencida, identidad activa) mediante una consulta por clave primaria contra identity.sessions e identity.identities. Revocar la sesión —por POST /sign-outs, POST /sign-outs/everywhere, cambio de contraseña o recuperación de contraseña— deja sin efecto el access token en la siguiente petición, sin esperar su expiración natural. Un fallo de base de datos durante esa comprobación nunca admite al llamador. MFA sigue pendiente.
  4. Crear organización o aceptar invitación: resolver identidad verificada, cupos, rol autorizado y alcances; crear organización/membresía/asignaciones con idempotencia y transacción. La aceptación y la membresía deben confirmarse juntas. Los triggers validan la aceptación persistida, pero no crean automáticamente la membresía ni prueban el token recibido.
  5. Administración: invitar/revocar/reenviar, suspender/reactivar/terminar membresía, cambiar roles y alcances, transferir administración y auditar actor/objetivo/motivo. Configurar alcances usa READ COMMITTED y bloqueos de padres; una reasignación exige otorgar/revocar explícitamente.
  6. Recuperación y baja: recuperar contraseña sin enumerar cuentas, reemplazar credenciales comprometidas, restablecer MFA bajo un procedimiento verificable, bloquear y cerrar sesiones. Política de retención y recuperación de administración pendiente; no implementar baja como borrado indiscriminado del historial.

Pruebas de alta verifican atomicidad y ausencia de derechos automáticos; no prueban correo, contraseña, CAPTCHA, OIDC ni endpoints inexistentes. Alta de servicio y alta humana requieren políticas distintas.

Ingreso con contraseña: elegibilidad hasta el commit

SignInUseCase verifica la contraseña antes de adquirir bloqueos de filas. Después, dentro de la misma transacción, revalida el método exacto y el hash verificado, el principal activo y el identificador verificado y no revocado. lockPasswordHolder adquiere FOR SHARE sobre identificador, principal, método y credencial, y conserva esos bloqueos hasta confirmar la sesión. Si la elegibilidad cambió durante la verificación, rechaza el ingreso sin insertar una sesión.

Orden concurrenteResultado exigido
Revocación, deshabilitación, reemplazo del método/hash o bloqueo del principal antes de la revalidaciónRechazo del ingreso; ninguna sesión nueva.
Emisión obtiene los bloqueos antes de revocar el método o bloquear el principalLa revocación espera al commit; después, los triggers existentes revocan también la sesión recién emitida.
Falla la emisión después de insertarRollback de la sesión y liberación de los bloqueos.

La revocación automática por método se ejecuta al marcarlo comprometido o pasar revoked_at de nulo a no nulo; no equivale a cerrar sesiones ante cualquier cambio de hash, identificador o estado del método. Bloquear, deshabilitar o archivar el principal también revoca sus sesiones. Estas garantías no certifican el cierre inmediato de access tokens ya emitidos ni sustituyen la autorización por comando.

La prueba sign-in-credential-race.integration-spec.ts ejecuta el caso de uso contra PostgreSQL, coordina ambos órdenes con barreras y comprueba contención mediante pg_blocking_pids. Incluye contraseña correcta e incorrecta, sustitución del método con el mismo hash, revocación del identificador y rollback; no introduce una actualización manual de sesiones para simular los triggers.

Correcciones y pruebas adversariales

  • Congelar identidad/procedencia, verificación y evidencia terminal; impedir reset de intentos, reutilización de códigos y cambios silenciosos de secreto TOTP.
  • Serializar emisión de sesiones y cambios de estado del principal; revocar las sesiones al desactivar y mantenerlas revocadas al reactivar. Probar ambos órdenes concurrentes: emisión antes del bloqueo y bloqueo antes de la emisión.
  • Verificar destinatario de invitación y vencimiento real; no permitir insertar una invitación ya aceptada para saltar la transición; preservar configuración aceptada.
  • Serializar cambios de dimensiones de alcance por asignación e invitación. Se prueba bloqueo concurrente y rechazo de una segunda dimensión incompatible.
  • Impedir trasladar un alcance a otra asignación dejando la original huérfana.
  • Probar aislamiento entre tenants, contexto falsificado y limpieza de contexto LOCAL al reutilizar conexión.
  • Revalidar el tiempo de soporte en cada sentencia y permitir registrar su cierre tras vencer.
  • Mantener la regresión previa de organización, cuotas, catálogo, tres cajas, ventas, inventario, caja y contabilidad.

Los roles adversariales son NOLOGIN, NOSUPERUSER y NOBYPASSRLS. El rol Auth tiene escritura global deliberada para probar invariantes de persistencia del servicio confiable; no representa un usuario final con acceso SQL. La función trigger de invitaciones usa SECURITY DEFINER y search_path fijo únicamente para consultar/bloquear el principal y su identificador; la escritura de invitación sigue sujeta a RLS. No se conceden a tenants permisos de escritura sobre credenciales globales.

Offline: contrato necesario, todavía no certificado

El cliente puede conservar una autorización local de duración limitada para operación previamente habilitada. No puede descubrir un bloqueo remoto mientras está desconectado. Debe existir un plazo máximo y una política explícita para ese intervalo.

Alta, verificación, recuperación, aceptación de invitaciones y otorgamiento de permisos requieren validación del servidor; un cliente offline no debe convertirse en autoridad de identidad. Para operaciones comerciales encoladas, conservar actor, dispositivo, organización, entidad legal, alcance, versión de autorización e idempotency key. Al reconectar, revalidar y separar rechazo/conflicto de aceptación; no borrar movimientos de caja ni producir ventas duplicadas. Hora declarada por dispositivo no prueba que una operación ocurrió antes de una revocación.

Este baseline y esta auditoría no certifican autorización offline completa ni revocación de dispositivo durante desconexión.

Brechas abiertas y límites

  • H05 — Último administrador: cerrado. Lo que esta sección pedía —contrato de transferencia y recuperación, y protección concurrente de todos los caminos que quitan acceso, no solo el DELETE— es exactamente lo que se implementó: nueve triggers diferidos sobre rol, permiso, membresía, alcance, principal, organización y admisión, con un lock por organización. La garantía y sus caminos están en DECISIONS.md.
  • H06 — Cambios retroactivos de entidad legal/unidad de negocio: requieren revisar efecto sobre autorización e historial.
  • H07 — Fecha de autorización: effective_permission aún usa current_date; falta explicitar fecha local de operación frente a timezone de sesión. El arreglo del reloj de soporte no resuelve este asunto.
  • H08 — Dispositivo revocado/offline: falta contrato de admisión y resolución al sincronizar.
  • Los límites de intentos por cuenta ya existen y se comparten entre sign-up, recuperación de contraseña, step-up, sign-in y confirmación de métodos de ingreso: un contador de fallos por cuenta bloquea con backoff exponencial y se limpia en el primer éxito. El ingreso con contraseña sigue sin completar la autorización por comando, los límites de intentos por dispositivo, la protección contra enumeración, MFA/recovery completo, la auditoría de transiciones ni la idempotencia de todos los endpoints.
  • La revocación automática de sesiones cubre los cambios del principal y del método descritos en el contrato de ingreso; otros cambios de credenciales requieren una política explícita de cierre.
  • La revisión temporal de soporte no endurece todavía todas las ediciones de su autorización: actor, objetivo, plazo y reapertura de un cierre requieren una siguiente batería específica. El ciclo del PIN operativo, las credenciales externas/passkeys y sus reemplazos tampoco está certificado por estas pruebas.
  • Las escrituras iniciales verificadas de identificadores/TOTP se reservan a un servicio confiable (p. ej. evidencia de proveedor externo); SQL no verifica esa evidencia. No dar DML global de Auth a clientes.
  • Las pruebas son un conjunto definido de invariantes, no una promesa de todas las casuísticas ni una certificación de producción.

Application Foundation in progress. Tracked in issue #13.