Cliente: Centrica Soluciones
Proyecto: Nebula ERP — Componente Nebula Vault (DataVault)
Version: 1.0
Fecha: 16 de Abril, 2026
Autor: Carlos Alberto Torres Camargo — Arquitecto de Software
Clasificacion: Uso Interno — Equipo de Desarrollo
Estado: EN VALIDACION (rama feature en entorno dev)
Este informe documenta la arquitectura actual del backend DataVault tras la migracion de conectividad SSH/SFTP a acceso local mediante volumenes Docker compartidos y gestion de daemons via Docker API. La solucion se encuentra desplegada y validada en el entorno de desarrollo (api-datavault-legacy-dev.centricasoluciones.com), pendiente de merge a develop y posterior promocion a produccion.
+-------------------------------------------------------------------------+
| nodo-01 (10.120.0.2) — Docker Engine |
| Red: dev-network (bridge) |
| |
| +---------------------+ +---------------------+ |
| | datavault-backend | | oais-watcher | |
| | FastAPI + Uvicorn | | Python 3.11 | |
| | Puerto 8547:8000 | | Monitorea ingest_sip| |
| | Memoria: 512MB | | Memoria: 256MB | |
| +----------+----------+ +----------+----------+ |
| | | |
| +----------- VOLUMEN -----------+ |
| | oais-repositorio | |
| | /home/egs/datavault/ | |
| | Repositorio/ | |
| +-------------------------------+ |
| |
| +---------------------+ +---------------------+ |
| | oais-cloud-sync-aip | | oais-preservation | |
| | (profile: cloud) | | -verifier | |
| | Sync AIP → Azure | | (profile: preserve) | |
| +---------------------+ +---------------------+ |
| |
| +---------------------+ |
| | oais-cloud-sync-sip | Docker Socket: /var/run/docker.sock |
| | (profile: cloud) | (montado en datavault-backend) |
| +---------------------+ |
+-------------------------------------------------------------------------+
|
| HTTPS (Nginx reverse proxy)
v
api-datavault-legacy-dev.centricasoluciones.com
| Volumen |
Punto de Montaje |
Servicios |
Modo |
oais-repositorio |
/home/egs/datavault/Repositorio |
backend, watcher, cloud-sync-aip, cloud-sync-sip, preservation-verifier |
read-write |
datavault-uploads |
/app/uploads |
backend |
read-write |
./oais (bind mount) |
/home/egs/datavault/datavault |
backend |
read-only |
| Docker socket |
/var/run/docker.sock |
backend |
read-write |
| Variable |
Valor |
Proposito |
STORAGE_MODE |
local |
Selecciona LocalStorageClient via factory |
OAIS_REPO_PATH |
/home/egs/datavault/Repositorio |
Raiz del repositorio OAIS |
OAIS_INGEST_PATH |
/home/egs/datavault/Repositorio/ingest_sip |
Carpeta de ingesta |
OAIS_STATUS_PATH |
/home/egs/datavault/datavault/preservation_status.json |
Estado de preservacion |
SIMAPPE_ADMIN_URL |
http://simappe-gateway-server:8090/simappe-admin |
Resolucion de BD por tenant |
SIMAPPE_JWT_SECRET |
(secreto) |
Validacion JWT HS512 |
DATABASE_URL |
postgresql+asyncpg://... |
BD legacy/global |
+---------------------+
| <<abstract>> |
| RepositoryClient |
|---------------------|
| + test_access() |
| + upload() |
| + upload_folder() |
| + list_directory() |
| + list_projects() |
| + create_project() |
| + create_subfolder()|
| + scan_subfolders() |
| + stat() |
| + read_file() |
| + write_file() |
| + remove_file() |
| + chmod() |
| + list_dir() |
| + exists() |
+----------+----------+
|
+----------------+----------------+
| |
+----------+----------+ +----------+----------+
| LocalStorageClient | | SFTPClient |
|---------------------| |---------------------|
| - base_path: str | | - host: str |
| Usa: pathlib, os, | | - port: int |
| shutil | | - user/password |
+---------------------+ | Usa: paramiko |
+---------------------+
+---------------------+
| StorageFactory |
|---------------------|
| + get_repository_ |
| client() | STORAGE_MODE=local → LocalStorageClient
| + get_ingest_ | STORAGE_MODE=sftp → SFTPClient
| client() |
+---------------------+
Endpoint (ingest.py, cloud.py, etc.)
|
+-- get_repository_client() / get_ingest_client()
|
+-- Lee STORAGE_MODE env var
|
+-- STORAGE_MODE == "local"
| |
| +-- LocalStorageClient(base_path=OAIS_REPO_PATH)
| |
| +-- Acceso directo a filesystem (volumen Docker)
|
+-- STORAGE_MODE == "sftp"
|
+-- SFTPClient(host, port, user, password)
|
+-- Conexion SSH/SFTP via Paramiko (fallback legacy)
1. Usuario sube archivo via frontend
2. Backend recibe en /app/uploads/ (temporal)
3. get_ingest_client() → LocalStorageClient
4. client.upload(local_path="/app/uploads/doc.zip", subdir="PROYECTO/SIP")
5. Resultado: /home/egs/datavault/Repositorio/ingest_sip/PROYECTO/SIP/doc.zip
6. Watcher detecta nuevo SIP y procesa (pipeline OAIS)
1. GET /api/oais/inspect/{document_id}
2. OAISIntegrationService() → get_repository_client()
3. client.exists() → Busca AIP en estructura de proyectos
4. client.stat() → Tamano, permisos, fecha
5. client.read_file() → Lee ZIP, extrae metadata.json
6. Checksum SHA-256, verifica lock files y retention files
7. Retorna AIPInspectionResponse
1. POST /api/oais/apply-legal-hold/{document_id}?legal_hold_id=LH-001
2. OAISIntegrationService().aplicar_legal_hold_fisico()
3. Busca AIP en repositorio
4. client.write_file("{doc_id}.legal_hold_LH-001.lock", json_metadata)
5. client.chmod(aip_path, 0o444) → Solo lectura
6. Retorna {exito: true}
Remocion:
1. POST /api/oais/remove-legal-hold/{document_id}?legal_hold_id=LH-001
2. client.remove_file("{doc_id}.legal_hold_LH-001.lock")
3. client.chmod(aip_path, 0o644) → Restaura escritura
1. POST /api/oais/apply-retention/{document_id}
2. client.write_file("{doc_id}.retention.json", {
policy_id, retention_start_date, retention_end_date,
disposition_action, aplicado_en, ruta_aip
})
Frontend / Admin Panel
|
v
datavault-backend (FastAPI)
|
+-- OAISDockerClient
|
+-- docker-py SDK
|
+-- /var/run/docker.sock (montado en contenedor)
|
+-- Docker Engine (nodo-01)
|
+-- oais-watcher (container)
+-- oais-cloud-sync-aip (container)
+-- oais-cloud-sync-sip (container)
+-- oais-preservation-verifier (container)
| Metodo |
Endpoint |
Descripcion |
Requiere |
GET |
/api/cloud/daemon/status?daemon_name=X |
Estado de un daemon |
superadmin |
GET |
/api/cloud/daemon/status/all |
Estado de todos los daemons |
superadmin |
POST |
/api/cloud/daemon/start?daemon_name=X |
Iniciar daemon |
superadmin |
POST |
/api/cloud/daemon/stop?daemon_name=X |
Detener daemon |
superadmin |
POST |
/api/cloud/daemon/restart?daemon_name=X |
Reiniciar daemon |
superadmin |
GET |
/api/cloud/daemon/stats?daemon_name=X |
CPU, memoria del daemon |
superadmin |
GET |
/api/cloud/daemon/logs?daemon_name=X&lines=50 |
Logs recientes |
superadmin |
| Nombre Logico |
Contenedor Docker |
oais_watcher |
oais-watcher |
cloud_sync_daemon_aip |
oais-cloud-sync-aip |
cloud_sync_daemon_sip |
oais-cloud-sync-sip |
preservation_verifier |
oais-preservation-verifier |
{
"oais_watcher": {
"is_running": true,
"status": "running",
"daemon_name": "oais_watcher",
"container_name": "oais-watcher",
"container_id": "17ac4468501a",
"image": "python:3.11-slim",
"created": "2026-03-31T20:58:25Z",
"started_at": "2026-04-16T17:01:01Z"
},
"cloud_sync_daemon_aip": {
"is_running": false,
"status": "not_found",
"daemon_name": "cloud_sync_daemon_aip",
"container_name": "oais-cloud-sync-aip",
"message": "Contenedor no encontrado"
}
}
| Aspecto |
Implementacion |
| Nombres de contenedores |
Hardcoded en DAEMON_CONTAINERS. No se aceptan nombres arbitrarios del usuario |
| Validacion |
_get_container_name() lanza ValueError si el daemon_name no esta en el mapeo |
| Autorizacion |
Todos los endpoints verifican current_user.is_superadmin |
| Docker socket |
Montado read-write (necesario para start/stop). Acceso limitado al contenedor backend |
/home/egs/datavault/Repositorio/ ← OAIS_REPO_PATH
├── ingest_sip/ ← OAIS_INGEST_PATH (staging)
│ └── {carpeta_sip}/ ← Watcher procesa aqui
├── _PROCESADOS/ ← SIPs procesados exitosamente
├── _ERRORES/ ← SIPs con error de procesamiento
├── PROYECTO_1/
│ ├── SIP/
│ │ └── {unique_id}/
│ │ ├── data/
│ │ └── metadata/
│ ├── AIP/
│ │ ├── {document_id}.zip ← Paquete archivistico
│ │ ├── {document_id}.legal_hold_LH001.lock ← Lock file (Legal Hold)
│ │ └── {document_id}.retention.json ← Metadata de retencion
│ └── DIP/
│ └── {document_id}.zip
├── PROYECTO_2/
│ └── SUBCARPETA_1/
│ ├── SIP/
│ ├── AIP/
│ └── DIP/
└── RH/ ← Documentos de RRHH
└── {document_id}.zip
Frontend Simappe OAuth2 Backend DataVault
| | |
|-- POST /login ------------>| |
|<-- accessToken (JWT) ------| |
| | |
|-- POST /jwt-select-company?companyId=7 -->| |
|<-- JWT final (con company) | |
| | |
|-- GET /api/preservation/status --------->| |
| (Authorization: Bearer JWT) | |
| |-- Valida JWT HS512
| |-- Extrae companyId
| |-- GET /simappe-admin/api/v2/
| | database-config/connection
| |-- Obtiene host/port/db/user/pass
| |-- Conecta a BD del tenant
| |-- Ejecuta query
|<-- Respuesta JSON ----------------------|
La resolucion de BD por tenant permanece intacta — no fue modificada durante esta migracion:
| Componente |
Archivo |
Funcion |
| JWT decode |
app/utils/auth.py |
verify_simappe_token() — extrae SimappeUserSession del JWT |
| Company check |
app/dependencies.py |
require_company_selected() — verifica companyId en claims |
| DB config fetch |
app/services/simappe_client.py |
get_tenant_db_config() — llama a SimappeAdmin con el JWT |
| DB pool |
app/database.py |
TenantDatabaseManager — LRU pool con cache TTL 30 min |
| FastAPI dependency |
app/database.py |
get_tenant_db() — inyecta AsyncSession por request |
| # |
Endpoint |
Metodo |
HTTP |
Resultado |
| 1 |
/health |
GET |
200 |
{"status":"healthy"} |
| 2 |
/api/preservation/status |
GET |
200 |
Estado vacio (correcto) |
| 3 |
/api/preservation/file?file_path=/test |
GET |
404 |
"no disponible" (correcto) |
| 4 |
/api/ingest/projects |
GET |
200 |
[] (correcto) |
| 5 |
/api/oais/inspect/test-doc-123 |
GET |
200 |
"AIP no encontrado" (correcto) |
| 6 |
/api/oais/apply-legal-hold/test-doc |
POST |
400 |
"AIP no encontrado" (correcto) |
| 7 |
/api/oais/remove-legal-hold/test-doc |
POST |
400 |
"AIP no encontrado" (correcto) |
| 8 |
/api/oais/apply-retention/test-doc |
POST |
400 |
"AIP no encontrado" (correcto) |
| 9 |
/api/cloud/daemon/status/all |
GET |
200 |
Watcher running, resto not_found |
| 10 |
/api/document-retention/policies |
GET |
200 |
[] (correcto) |
Se realizo auditoria forense completa post-migracion:
| Capa |
Archivos |
Resultado |
| Models (ORM) |
16 archivos, 23 modelos |
LIMPIO |
| Clients (Facade) |
6 archivos |
LIMPIO |
| API Routers |
6 archivos, ~60 endpoints |
LIMPIO (post-correccion) |
| Services |
1 archivo |
LIMPIO |
| Config/Infra |
5 archivos |
LIMPIO |
| Database |
1 archivo |
INTACTO |
Bugs encontrados y corregidos durante auditoria: 2 (referencias rotas a funciones/variables eliminadas).
| Item |
Estado |
| Codigo funcional en entorno dev |
Validado |
| Auditoria forense |
Completada — 0 bugs pendientes |
| Rama feature |
En validacion |
| Merge a develop |
Pendiente aprobacion |
| Deploy a staging |
Pendiente |
| Deploy a produccion |
Pendiente |
| # |
Item |
Severidad |
Archivo |
Descripcion |
| 1 |
repository.py sin migrar |
MEDIA |
app/api/repository.py |
~3000 lineas con 7 bloques import paramiko directos. Endpoints de gestion de repositorio que crean conexiones SSH ad-hoc. Funciona porque usa tenant.server_type de BD (ya actualizado a local) pero el codigo Paramiko sigue presente |
| 2 |
Path traversal en LocalStorageClient |
BAJA |
app/clients/local_storage_client.py |
No valida que paths resueltos esten dentro de base_path. Mitigado porque los paths provienen de BD, no de input directo del usuario |
| 3 |
5 select(Tenant) redundantes |
BAJA |
app/api/preservation.py |
Bloques que fetchean tenant para pasarlo a read_preservation_status_from_server(), que ahora ignora el parametro. Funcionan pero son innecesarios |
| 4 |
Rutas hardcoded |
BAJA |
local_storage_client.py, sftp_client.py |
create_project_directory() y create_subfolder() usan /home/egs/datavault/Repositorio hardcoded en lugar de self.base_path |
Departamento de Arquitectura — Centrica Soluciones