Skip to content

ADR-065: Nada se borra, solo se inactiva

Estado: Aceptado Fecha: 2026-11-04 Autores: Giampiero (mantenedor principal) Deroga: ADR-064, salvo su D11 y el asiento de envío de D10


Contexto

Orden del mantenedor: «En el sistema nada se borra solo se inactiva y se le quita de la vista de los usuarios». Una falla de seguridad no debe poder borrar expedientes ni radicados, y tener lista la destrucción es esa falla. Orfeo inactivaba anexos y anulaba radicados; nunca destruía.

Hasta aquí, POST /api/v1/eliminacion/lotes/{id}/ejecutar destruía en línea, y POST /internal/documentos/purgar lo hacía con solo el token interno, sin lote ni aval. Ninguna marca existente ocultaba nada: anulado sigue visible a propósito, files.deleted_at no oculta el anexo y excluido afecta solo al vínculo.

Decisiones

D1. Nada se borra. Ningún documento de archivo, su contenido o sus copias se borra físicamente, por ninguna vía de la aplicación ni con la credencial de aplicación. Se retira toda capacidad de hacerlo: rutas, servicios, jobs, consumidores y remove_object.

D2. La opción es inactivar, con el filtro en el motor. radicados, anexos, borrador y expedientes pasan a X_todos, con una vista X que lleva WHERE inactivado_at IS NULL. Una consulta nueva no puede olvidar el filtro: una lista de condiciones a mano ya falló varias veces en este proyecto. Solo los repositorios de inactivación y los predicados declarados nombran _todos. Costo: todo ALTER va contra X_todos y recrea la vista.

D3. Alcance. Se inactiva el expediente por un lote avalado, con sus radicados, anexos, borradores y correo; y el anexo individual con PERM_BORRAR_ANEXO. El radicado suelto no: ya tiene la anulación, y un botón de ocultar sin Comité sería la vía de una brecha.

D4. Reversible, con segunda persona. Una inactivación irreversible por una brecha equivaldría a destruir.

D5. Lo inactivo no se ve. Para el usuario final equivale a borrar: no sale en estadísticas ni en reportes, ni a ningún usuario en ningún proceso. Responde el mismo 404 de «no existe». Gestión documental ve solo un registro de metadatos en GET /api/v1/eliminacion/inactivados. Leer el contenido exige reactivar. Dos excepciones, sin contenido: (a) los actos de PRESERVACIÓN siguen corriendo sobre lo inactivo —renovación LTA, migración de formato, reempaque del AIP y retención de valoración—, porque inactivar es reversible (D4) y lo que se degrada mientras está oculto no se recupera al reactivar; (b) la métrica de infraestructura del almacén (orpycamcp_storage_*{state}) y la de vigilancia de sellos suman lo inactivo sin etiqueta de tenant ni identificadores.

D6. La disposición final E es inactivar. CT se conserva visible; E oculta, siempre por lote, aval y acta. Se conservan propuesta, publicidad, aval, acta y addendum. Ninguno afirma destrucción: el método lo fija el servidor y dice que no se borró nada. expediente_lapida pasa a expediente_inactivacion.

D7. Sin evento para búsqueda ni asistente. Basta el WHERE del gate vivo, sin retraso y reversible. Se retira document.radicado.purged y el vaciado de texto: lo replicado es copia de algo que sigue existiendo.

D8. Binarios sin referencia: no se borran ni se marcan. Ninguna vista los alcanza, y su purga era una primitiva general de destrucción.

D9. Tres capas en el motor. Trigger que rechaza DELETE y TRUNCATE, FK sin cascada, y REVOKE también sobre la vista.

Consecuencias, sin suavizar

  • Excepción a D4 (ADR-066 D16): un anexo de expediente sin radicado inactivado en un expediente que luego se cierra no se reactiva; sus bytes y su fila se conservan.

  • Incumplimiento por diseño del Ac. AGN 001/2024 art. 4.3.2.5, que ordena eliminar lo electrónico vencido y no permanente con procedimientos de borrado seguro. Una entidad sujeta al Acuerdo no lo cumple con este sistema, y un auditor puede no aceptar el acta como acta de eliminación. El sistema no debe anunciarse como conforme en este punto.

  • Datos personales sin plazo. Ley 1581/2012 y DUR 1074/2015 art. 2.2.2.25.2.8: tras vencer la retención, los datos siguen en reposo y el sistema no puede atender una solicitud de supresión. La responsable ante la SIC es la entidad. La plantilla de política de tratamiento deja de prometer supresión.
  • Inactivar no reduce el riesgo de una brecha sobre los datos. Lo inactivo sigue en disco y en respaldos.
  • Crece el almacenamiento, también con binarios abandonados.
  • El índice del art. 20 (Ley 1712) pierde lo inactivo, aunque su calificación como clasificado o reservado siga vigente: el índice se arma con lo visible.
  • Consultar exige reactivar. No hay lectura controlada del contenido de lo inactivo, ni registro por radicado: solo el registro de metadatos por expediente.
  • El ciudadano recibe «No encontrado» en la verificación pública, no una explicación de que el documento existe y está inactivo.
  • El consecutivo queda con huecos visibles: el número de un radicado inactivo sigue ocupado.
  • Los radicados anulados vinculados a un expediente inactivado también se ocultan.
  • Sin alerta por radicados compartidos. El Modelo de Requisitos §6.2.2.7 pide alertar al Administrador Funcional y copiar lo compartido; aquí un radicado vigente en otro expediente activo simplemente no se inactiva.

  • El inventario físico deja de cuadrar contra la caja (ronda de auditoría, B4). El FUID vivo (/fuid, /fuid.xml, GET /transferencias/{id}/fuid) excluye todo lo inactivo, por la regla del dueño de que lo inactivo sale de la vista de todos: una caja puede contener papel de un expediente que el inventario ya no lista. El papel se ubica por GET /eliminacion/inactivados (signatura, unidad). El acta de entrega ya congelada no se altera. Se conserva nivel_maximo_de_unidad_arbol (da un entero y evita prestar papel reservado).

  • La renovación LTA SÍ corre sobre lo inactivo (D4/D5). FirmaRepository.firmas_a_renovar no filtra por inactivación: renueva también la firma de un radicado, expediente o borrador inactivo. Se decidió así tras haberlo excluido en una primera ronda, porque inactivar es reversible (D4) y un sello de archivo vencido no se recupera: si el job lo saltara, al reactivar el último eslabón ya habría caducado (el job no renueva sobre una TSA caducada, EN 319 102-1 §5.6.2) y la evidencia de la firma se habría perdido. La renovación es un acto de sistema sobre la cadena de sellos: no lee, muestra ni modifica el contenido del objeto, solo añade un sello sobre bytes que storage ya conserva bajo WORM. Lo que sí oculta lo inactivo es lo que ve una PERSONA: el listado de sellos y sus cuentas siguen usando no_inactivo. Se retiraron omitidas_inactivas y inactividad_no_verificable.

Lo que queda a cargo de la entidad

Su TRD convalidada y su PGD dicen eliminar y el sistema oculta; publicar el acta por su cuenta; tramitar las observaciones y los conceptos del art. 4.5.4; eliminar el papel y dejar constancia fuera del sistema; no certificar la inexistencia de lo inactivo; mantener completo por su cuenta el índice del art. 20 de la Ley 1712, que el sistema arma solo con lo visible; anotar el inventario físico (FUID) apoyándose en GET /eliminacion/inactivados, porque el FUID vivo excluye lo inactivo; y fijar la política de retención y custodia de los respaldos, que conservan lo inactivo —datos personales incluidos— sin plazo (Ley 1581 art. 17 lit. d). Una suspensión por observación o por concepto se registra como retención legal de motivo administrativo, con el concepto como fundamento.

Compuertas que siguen

  • El ref es un lote de eliminación de verdad. POST /api/v1/internal/documentos/inactivar ya no acepta un UUID cualquiera: document-service lee la vista contrato v_lote_eliminacion_radicado de archive-service (migración 108) y exige que ref sea un lote avalado, ejecutado_parcial o ejecutado (409 lote_no_avalado) y que cada radicado esté vinculado a un expediente de su inventario no omitido (422 radicado_fuera_del_lote), antes de tocar nada y otra vez en el WHERE del UPDATE (carrera con una anulación). Sin la vista, 503 contrato_de_lotes_ausente: falla cerrado. /reactivar y /compensar exigen que ref sea un lote, sin importar su estado (puede haberse anulado después): 409 lote_desconocido; lo mismo vale para python -m app.ops.reactivar_por_ref. Radicado compartido (migración 109 de archive): la vista añade compartido_con_activo —el radicado sigue vinculado, con vínculo vigente, a un expediente activo fuera del inventario no omitido del lote, la misma regla con la que archive-service lo excluye al ejecutar— y document-service lo rechaza con 409 radicado_compartido_con_expediente_activo antes de tocar nada y en el WHERE del UPDATE (AND NOT v.compartido_con_activo); sin esa columna, 503 contrato_de_lotes_ausente.
  • Vencimiento (B1). Nada se confirma, aplaza, propone, publica ni inactiva antes de que venza la retención del archivo central (fin_archivo_central <= hoy, zona de la entidad; Ac. AGN 001/2024 art. 4.4.3 par. 1). Un solo predicado (fecha viva y congelada, NULL rechazado) en decidir, proponer, publicar, ejecutar, el WHERE del UPDATE y los triggers de la migración 098. Códigos: retencion_no_vencida, lote_con_retencion_no_vencida, legal_hold_activo.
  • Addendum (B2). Toda ejecución lo deja y lo sella la entidad por la ruta interna; las causales de reactivación y retiro son fijas en el XML (el motivo libre vive en la tabla).
  • Índice pendiente (B3). indice_pendiente y fechas_extremas_vacias se rechazan por ítem al proponer.

  • La migración de formato y la métrica del almacén miran el uso del archivo (D5). El barrido de migración de formato de storage no convierte un archivo cuyo único uso es un anexo, o el anexo de un radicado, inactivo; basta un uso vigente para que un binario compartido siga procesándose, y un archivo que ningún anexo cita (AIP, índice) no se toca. Se evalúa en vivo en el WHERE (función archivo_estado_de_uso, migraciones 031 y 032 de storage): al reactivar vuelve solo. GET /metrics de storage publica archivos y bytes del almacén por estado (vigente, inactivo, sin_referencia), calculados desde la tabla files y sumados sobre los tenants (el exportador no admite etiqueta de tenant); no lista el bucket. Todos los citadores (migración 032): además de los anexos de radicado cuentan como uso los anexos de expediente (cualquier versión; vigente si el anexo y su expediente están activos), el documento del borrador, el correo de origen, el acuse de recibo y la versión pública (vigente si su objeto no está inactivo). Regla: inactivo solo si hay citas y ninguna vigente; vigente si alguna lo es. La métrica llama a la misma función: una sola definición de «en uso». Los artefactos del sistema (AIP, índice, acta, constancias de firma) siguen siendo sin_anexo.

  • La retención diferida de la valoración NO excluye lo inactivo, a propósito. Es un acto único del Comité sobre el expediente, no un barrido: si se saltara el archivo de un anexo inactivo, ese archivo quedaría sin retención para siempre al reactivarlo, sin que nada volviera a pasar por él. Protegerlo de más es inocuo (inactivar no destruye).
  • indice_sin_worm. Ya no protege una destrucción. Su motivo vigente es evitar que un índice quede sin proteger: los jobs de preservación no vuelven a ver lo inactivo, y un índice sin sello no se sellaría después.
  • Reactivar genera y sella un addendum del acta con quién y por qué, además del asiento de auditoría y del registro reactivado_*. Una constancia dada de baja no vuelve a estar vigente al reactivar: la guarda de eliminacion_lote_soporte_citado solo admite marcarla.

Deuda abierta

  • Las tablas de catálogo conservan DELETE: D1 habla de documento de archivo y no las alcanza.
  • La purga operativa cubre batch_jobs de tipo documents completados sin errores (purgar_lotes_antiguos, batch_retencion_dias), eventos_salientes ya enviados (7 días, outbox.py) y las filas terminales de la DLQ (resolved/discarded, 90 días, jobs/deadletter_retention.py en knowledge, notification, signature y workflow). Siguen sin purga, a propósito, los lotes con errores o fallidos y la DLQ pending (alguien debe revisarlos); el resto de batch_jobs (otros tipos) no tiene política.

Resueltas en esta ronda y retiradas de la lista: el ref de la inactivación interna se comprueba contra el lote de eliminación (ver «Compuertas que siguen»), la renovación LTA corre también sobre lo inactivo (ver «Consecuencias»), y la inactivación individual de un anexo nunca usó el token interno: va por PERM_BORRAR_ANEXO (nivel 3, reactivar nivel 4) con la persona firmada y su asiento (documents.py /{document_id}/anexos/{anexo_id}/inactivar; expediente_anexos.py /{anexo_id}/inactivar).

Credencial S3 (resuelto)

Antes, storage-service corría con la credencial raíz del almacén: podía borrar y sobrescribir cualquier objeto. Ahora hay dos credenciales:

  • Aplicación: la usa storage-service. Puede escribir y leer, y sobrescribir (la versión anterior se conserva porque el bucket está versionado). El almacén le niega borrar objetos o versiones, suspender el versionado, fijar reglas de ciclo de vida o políticas de bucket, y crear o borrar buckets. Se comprobó en RustFS 1.0.0.
  • Raíz: solo la ve el aprovisionamiento (python -m app.ops.aprovisionar_almacen, servicio objectstore-init). Crea la política y el usuario de aplicación, y por cada entidad el bucket de documentos con versionado y los buckets WORM (preservación y réplica). Es idempotente y no borra nada; si un bucket de documentos ya existía sin versionado, se lo activa.

En ejecución storage-service ya no crea buckets: si falta el bucket responde 503 almacen_no_aprovisionado, y si el de documentos no tiene versionado, 503 almacen_sin_versionado. Un alta de entidad (scripts/init-tenant.sh) corre el aprovisionamiento.

El espejo de infra/backup.sh copia solo la versión vigente de cada objeto; las anteriores quedan en el almacén primario.

Lo que esto no cubre

  • La credencial de propietario de la base puede desactivar triggers. En producción los servicios no la usan; la pila local sí.
  • La credencial de aplicación puede inactivar en masa con UPDATE. Con el token interno se recupera con app/ops/reactivar_por_ref.py; con un UPDATE directo sin registro hay que corregirlo por SQL. No puede cambiar el objeto al que apunta un archivo ni un anexo: el motor lo rechaza (storage 030, document 064).
  • Lo que ya salió del sistema: correos, webhooks, descargas y proveedor externo de lenguaje.

Derogaciones parciales

ADR Se deroga Sigue
ADR-063 Inventario en la intención, recuento desde ella, eliminacion_lote_progreso, la lápida Pertenencia viva, retiro y re-entrada, descarga del addendum
ADR-061 Destrucción en fases, «punto de no retorno», causa WORM Resolución de la revisión, compuerta, omisión
ADR-047 D-3 (purga de la ficha provisional) Correo autenticado, ALTCHA
ADR-043 La baja de soporte como destrucción Retención legal, préstamo, soporte citado
ADR-025 La lectura «autoriza destruir» Instrumento, gobierna_eliminacion como dato
ADR-023 Gate WORM en la purga Object-Lock