CreaRack-SL

Auditoría Suprema s105: Blueprints / Auto-Plan (22 fixes)

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) → lanza HttpError(403) si el usuario no tiene el permiso requerido.
  • Aplicado a los ~25 endpoints de blueprints por verbo:
    • view → lectura de blueprints, racks, anotaciones
    • edit → 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_bg
  • blueprints/api/racks.py: list_placements, update_placement, delete_placement, update_rack_positions, get_blueprint_racks, create_rack_in_blueprint, delete_rack_total, duplicate_element
  • blueprints/api/annotations.py: get_annotations, create_annotation, update_annotation, delete_annotation, delete_connection
  • blueprints/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_prompt
  • blueprints/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 (AIPrompt con nombre reservado __brain_active__).
  • El fichero prompts/blueprint_analyst.md queda como semilla read-only (default si la org no ha personalizado).
  • Endpoints /prompt/save y /prompt/reset operan 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 si file.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 ValueError para errores esperables (imagen inválida), except Exception con mensaje genérico al cliente (no str(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 / operator en endpoints clave
  • Auto-Plan gate (solo edit)
  • /placements reparado
  • 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):

  1. Handler global de errores (config/urls.py): filtrar str(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.

  2. 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 de blueprints y otros módulos auditados.
  • ✅ Siempre filtrar deleted_at__isnull=True en listados de edición (soft-delete consistency).
  • ✅ AIPrompt.__brain_active__ es ahora el prompt del AI Brain per-org; /prompt/save y /prompt/reset usan BD.
  • ✅ Auto-Plan: validar tamaño de imagen, usar transaction.atomic(), never expose str(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]]