Resumen
Tercera tanda de la Auditoría Suprema (iniciativa de seguridad exhaustiva). Ámbito: Map Editor (blueprints) y Auto-Plan de IA. Se arreglan los 22 hallazgos confirmados de una vez (decisión de Edu: “definitivo, todo de una vez”).
Alcance: 7 ALTA + 12 MEDIA + 3 BAJA (de 61 hallazgos crudos → 40 únicos → 22 confirmados).
Datos: auditoria-suprema/blueprints.md / .json.
Hallazgos y Fixes (SEGURIDAD)
1. Broken Access Control sistemático (~25 endpoints)
Problema: NINGÚN endpoint del módulo blueprints aplicaba gate de permisos. Un usuario con rol readonly podía:
- Crear / borrar blueprints (mapas)
- Hard-deletear racks (irreversible, saltaba papelera)
- Disparar Auto-Plan (operación IA facturable)
Fix:
- Nuevo helper:
core.utils.require_perm(request, scope, level)→ lanzaHttpError(403)si el usuario no tiene el permiso requerido. - Aplicado a los ~25 endpoints de
blueprintspor verbo:view→ lectura de blueprints, racks, anotacionesedit→ creación, actualización, borrado, Auto-Plan
- Archivo:
core/utils/__init__.py(exportado, reutilizable en otros módulos)
Endpoints afectados:
blueprints/api/blueprints.py:list_blueprints,create_blueprint,get_blueprint,update_blueprint_settings*,delete_blueprint,clone_blueprint,create_empty_blueprint,update_blueprint_bg,delete_blueprint_bgblueprints/api/racks.py:list_placements,update_placement,delete_placement,update_rack_positions,get_blueprint_racks,create_rack_in_blueprint,delete_rack_total,duplicate_elementblueprints/api/annotations.py:get_annotations,create_annotation,update_annotation,delete_annotation,delete_connectionblueprints/api/autoplan.py:magic_import_blueprint(clave: costo IA)blueprints/api/prompt.py:get_prompt_content,save_prompt_content,reset_prompt_content,list_saved_prompts,save_prompt_as,load_saved_prompt,delete_saved_promptblueprints/api/trash.py:list_trash_blueprints,restore_blueprint,permanent_delete_blueprint,empty_blueprint_trash
2. Fuga cross-tenant en views.py
Problema: El Map Editor usaba Organization.objects.first() como fallback para usuarios sin organización asignada, mostrándoles blueprints y racks del primer tenant de la BD. Violación de RLS (multi-tenancy).
Fix: Eliminado el fallback. Ahora render vacío si el usuario no tiene org (o se redirige correctamente en el frontend).
3. AI Brain prompt global compartido entre tenants
Problema: Los endpoints /prompt/save y /prompt/reset escribían un único fichero global (prompts/blueprint_analyst.md) que pisaban todos los tenants. Si una org lo editaba, afectaba a todas.
Fix: Resemantización del modelo AIPrompt:
- Ahora el prompt activo es por organización (
AIPromptcon nombre reservado__brain_active__). - El fichero
prompts/blueprint_analyst.mdqueda como semilla read-only (default si la org no ha personalizado). - Endpoints
/prompt/savey/prompt/resetoperan sobre BD, no archivo. - Auto-Plan lee el prompt activo de la org:
get_active_prompt_text(org).
Subcomponentes:
- Función
_seed_prompt_text(): lee la semilla del fichero (fallback). - Función
get_active_prompt_text(org): retorna prompt personalizado de la org o la semilla. - Tabla
AIPrompt: ahora usada como registro activo per-org (no solo “saved prompts”).
4. Auto-Plan: validación de subida + atomicidad + audit
Problema: Falta de límite de tamaño, transacciones incompletas, sin registro de auditoría, errores exponen str(exc).
Fix:
- Límite de tamaño: constante
MAX_AUTOPLAN_UPLOAD_BYTES = 25 * 1024 * 1024(25 MB). Rechaza sifile.size > límite. - Atomicidad:
transaction.atomic()envolviendo Blueprint + BlueprintPlacement/Rack/Annotation. Si la IA falla, no queda blueprint huérfano. - Audit log:
logger.info(...)con org, user, provider, blueprint_id, racks_count. - Errores genéricos:
except ValueErrorpara errores esperables (imagen inválida),except Exceptioncon mensaje genérico al cliente (nostr(exc)).
5. Decompression bomb en Auto-Plan
Problema: AutoPlanService.get_image_scaling() abría la imagen sin validar tamaño (Pillow tiene MAX_IMAGE_PIXELS desactivado a nivel de proceso).
Fix:
- Guard en
get_image_scaling(): rechaza imágenes > 64 MP (leyendo solo cabecera, sin procesar píxeles). - Output del modelo blindado:
_as_list()valida tipo + guard adicional para no reventar ante JSON inesperado.
Hallazgos y Fixes (CORRECTNESS)
6. GET /{bp_id}/placements roto (500)
Problema: Endpoint llamaba a BlueprintService.list_placements(), método inexistente → AttributeError en cada llamada.
Fix: Implementado en el service (excluye racks en papelera).
7. delete_rack_total hard-deleteaba
Problema: El endpoint /delete de racks hacía rack.delete() (hard-delete irreversible), saltándose el sistema de papelera del resto del producto.
Fix: Ahora rack.soft_delete(). El rack desaparece de los mapas pero es recuperable desde la papelera (papelera/restore).
8. Racks soft-deleted seguían en el mapa
Problema: Los listados de placements no filtraban rack__deleted_at, así que racks en papelera seguían apareciendo.
Fix: Filtro rack__deleted_at__isnull=True en:
get_blueprint_racks(): placements con racks no borrados.list_placements(): implementación nueva (también filtra).
9. duplicate_element sin validación JSON
Problema: Duplicar una anotación hacía json.loads(annot.data) sin try/except → 500 ante data corrupta.
Fix: Try/except con mensaje 400 claro:
try:
data = json.loads(annot.data) if annot.data else {}
except (json.JSONDecodeError, TypeError):
return 400, {"message": "Annotation data is corrupt and cannot be duplicated"}
10. create_annotation permitía data JSON inválida
Problema: Una data string no-JSON se persistía sin validar → corrompe la BD.
Fix: Validación previa:
try:
json.loads(annot_data)
except (json.JSONDecodeError, TypeError):
return 400, {"message": "Field 'data' must be valid JSON"}
11. N+1 en /list (blueprints)
Problema: Un COUNT por blueprint en el listado de filtros → N+1.
Fix: annotate(Count("placements")) (una sola query agregada).
12. Endpoints de edición operaban sobre blueprints en papelera
Problema: Filtro deleted_at__isnull=True faltaba en varios endpoints de edición.
Fix: Agregado a:
get_blueprint(),_update_blueprint_settings_impl(),delete_blueprint(),clone_blueprint(),update_blueprint_bg(),delete_blueprint_bg(),update_rack_positions(),create_rack_in_blueprint(),duplicate_element().
Tests
Archivo nuevo: tests/api/test_blueprints_security.py (10 tests)
- Gate
readonly/operatoren endpoints clave - Auto-Plan gate (solo
edit) /placementsreparado- Soft-delete + papelera
- Racks en papelera ocultos del mapa
- Validación JSON de anotación
- Aislamiento del prompt por tenant
Verificación local: 54 tests verdes (blueprints + racks security + tenant isolation), ruff + django check limpios.
Fuera de tanda (documentado)
Dos decisiones de follow-up (misma sesión, PR distinto):
-
Handler global de errores (
config/urls.py): filtrarstr(exc)en todos los exception handlers (transversal a ~509 endpoints). Decisión aparte porque es global y podría tener side effects. Por ahora algunos endpoints siguen exponiendo mensajes internos. -
Auto-Plan asíncrono: convertir
magic_import_blueprint()a async (Celery / tarea en background). Toca frontend del Map Editor (UX: spinner, polling). PR propio.
Datos detallados: auditoria-suprema/blueprints.md (hallazgos + análisis) y .json (estructura de datos).
Impacto para devs
- ✅ Usar
require_perm(request, scope, level)en todos los endpoints nuevos deblueprintsy otros módulos auditados. - ✅ Siempre filtrar
deleted_at__isnull=Trueen listados de edición (soft-delete consistency). - ✅
AIPrompt.__brain_active__es ahora el prompt del AI Brain per-org;/prompt/savey/prompt/resetusan BD. - ✅ Auto-Plan: validar tamaño de imagen, usar
transaction.atomic(), never exposestr(exc). - ⚠️ No confundir “saved prompts” (AIPrompt con nombre custom) con el prompt activo (AIPrompt reservado
__brain_active__).
Véase también
- [[entity—core—service—require-perm]]
- [[entity—blueprints—model—aiprompt]]
- [[concept—saas—multi-tenancy]]
- [[feature—blueprints—auto-plan]]
- [[decision—20260603—auditoria-s105-decisiones]]