Descripción
Tanda de correcciones menores (BAJA/MEDIA) de la Auditoría Suprema sub-área 5 (wireless + UPS), cerrada en la sesión 112 (2026-06-07). Aborda 5 áreas de robustez, rendimiento y seguridad multi-tenant.
Cambios principales
1. Distinción clara: 404 para profile desconocido (sa5 B2)
Endpoints afectados:
GET /api/monitoring/wireless/available-metrics/{profile_id}→ ahora devuelve404 {"error": "AP not found"}siprofile_idno existe o pertenece a otra orgGET /api/monitoring/ups/available-metrics/{profile_id}→ análogo para UPS
Motivo: Antes, un profile_id inexistente era indistinguible (para el cliente) de un AP/UPS sin métricas disponibles (ambos devolvían 200 {"metrics": []}). Ahora:
- 404 = profile no existe o no es de tu org (error del cliente)
- 200
{"metrics": [...]}= profile válido, con 0+ métricas (operación exitosa)
Código:
# monitoring/api/wireless.py:241-243
try:
profile = DeviceProfile.objects.get(id=profile_id, organization=org)
except DeviceProfile.DoesNotExist:
# 404 (sa5 B2): an unknown/other-org profile is distinct from an AP with no metrics.
return 404, {"error": "AP not found"}
2. Logging de errores silenciados (sa5 B9)
Endpoints afectados:
GET /api/monitoring/wireless/available-metrics/{profile_id}GET /api/monitoring/ups/available-metrics/{profile_id}
Cambio: El bloque except: pass que se tragaba fallos de BD/OID lookup ahora registra un warning:
# monitoring/api/wireless.py:268-270
except Exception as e:
# sa5 B9: don't swallow DB/data errors silently — log and fall back to base metrics.
logger.warning(f"available-metrics: vendor OID lookup failed for {vendor_slug}: {e}")
Impacto: Los admin/ops pueden ahora detectar si hay fallos de índices o acceso a VendorProfile que antes pasaban desapercibidos.
3. Dashboard layout/notas: aislamiento por usuario (sa5 B8)
Endpoints afectados:
GET /api/monitoring/wireless/dashboard-layoutPOST /api/monitoring/wireless/dashboard-layoutDELETE /api/monitoring/wireless/dashboard-layoutGET /api/monitoring/wireless/dashboard-notesPUT /api/monitoring/wireless/dashboard-notes- Análogos para UPS
Cambio: La clave de cache ahora incluye request.user.id además de organization.id:
# monitoring/api/wireless.py:513
# Antes: cache.get(f"wireless_layout_{org.id}")
# Ahora:
return {"layout": cache.get(f"wireless_layout_{org.id}_{request.user.id}") or []}
Motivo (RLS): Un usuario de org A no debe ver (ni pisar) el layout/notas personalizadas de otro usuario de la misma org. Este cambio refuerza el aislamiento.
Efecto colateral: Tras el deploy, todos los layouts/notas previos se invalidan (la clave cambió) → cada usuario debe re-personalizar desde el default.
4. Timeout en consultas a VictoriaMetrics (sa5 M6)
Endpoints afectados:
GET /api/monitoring/wireless/dashboard-charts(threaded query a VM)GET /api/monitoring/ups/dashboard-charts(ídem)GET /api/monitoring/wireless/report(ídem)GET /api/monitoring/ups/report(ídem)
Cambio: El .result() en ThreadPoolExecutor ahora está acotado a 15 segundos:
# monitoring/api/wireless.py:318
# Antes: charge, load = pool.submit(asyncio.run, _run()).result()
# Ahora:
clients, bw_in, bw_out = pool.submit(asyncio.run, _run()).result(timeout=15)
Motivo: Si VictoriaMetrics está colgado o muy lento, un cliente que espere indefinidamente bloqueaba el worker de Ninja. Con timeout, el error se captura en el siguiente except block y devuelve datos parciales/vacíos con warning.
Limitación: La conversión completa a async def (eliminando ThreadPoolExecutor) queda deferida — la auditoría marcó el timeout como mitigation “riesgo medio” que exige prueba de carga completa.
5. Idempotencia en deep-discover (sa5 M1/M5)
Endpoint/servicio afectado:
build_and_dispatch_deep_discover()enmonitoring/services/deep_discover_service.py
Cambio: Antes de lanzar un barrido SNMP de flota, se verifica si ya hay uno en curso para la org:
# monitoring/services/deep_discover_service.py:78-82
# Idempotency / anti-DoS (sa5 M1/M5): don't relaunch a fleet-wide SNMP sweep while one
# is already running for this org — coalesce repeated clicks into the in-flight job.
existing = cache.get(job_key)
if isinstance(existing, dict) and existing.get("status") == "running":
return 200, {"status": "already_running", "total": existing.get("total", 0)}
Motivo: Si un usuario hace clic varias veces en “Scan fleet”, no debe lanzarse un sweep por cada clic (anti-DoS, ahorrar banda). El retorno already_running permite al frontend mostrar estado sin relanzar.
6. Batch assign_profiles_to_group (sa5 B5)
Servicio afectado:
assign_profiles_to_group()enmonitoring/services/group_service.py
Cambio: En lugar de un .add(profile) y .remove(profile) por perfil, ahora se batch:
# monitoring/services/group_service.py:178-186
to_add = [p for p in all_profiles if p.id in selected_ids]
to_remove = [p for p in all_profiles if p.id not in selected_ids]
if to_add:
group.device_profiles.add(*to_add)
if to_remove:
group.device_profiles.remove(*to_remove)
return len(to_add)
Impacto de rendimiento:
- Antes: N+1 queries (una
.add()o.remove()por perfil) - Ahora: 2 queries (un batch add + un batch remove)
Cross-tenant safety: Los profile_ids que no pertenecen a la org simplemente no aparecen en all_profiles → se ignoran de forma segura. El count devuelto es el número realmente asignado (no todo lo solicitado).
Tests
Se añadieron 2 nuevos tests en tests/api/test_monitoring_sa4_sa5.py:
test_available_metrics_unknown_profile_404: Verifica queGET /wireless/available-metrics/999999devuelve 404test_dashboard_layout_is_per_user: Verifica que el layout de un usuario no es visible para otro de la misma org
class TestSa5BajaCleanup:
def test_available_metrics_unknown_profile_404(self, operator_user, organization):
"""B2 — un profile_id inexistente devuelve 404, no {metrics: []}."""
client = _client_for(operator_user, organization)
resp = client.get("/api/monitoring/wireless/available-metrics/999999")
assert resp.status_code == 404
Referencia de items auditoria
| Item | Área | Descripción |
|---|---|---|
| sa5 B2 | Correctness | available-metrics → 404 para profile desconocido |
| sa5 B5 | Perf | Batch m2m en assign_profiles_to_group |
| sa5 B8 | RLS | Dashboard layout/notas por-usuario, no por-org |
| sa5 B9 | Observabilidad | Log (no silent pass) en vendor OID lookup |
| sa5 M1/M5 | Robustez | Idempotencia en deep-discover (anti-DoS) |
| sa5 M6 | Robustez | Timeout 15s en queries a VictoriaMetrics |
Items deferidos (con justificación):
- sa5 B4: Paginación de fleet-report — truncar reporte agregado induce error; aceptable a escala actual
- sa5 M2: Merge total de gemelos (wireless/UPS) → PR propia con tests de paridad
- sa5 M6 async-def completo: Requiere prueba de carga; PR propia
Véase también
- [[docsupercontextauditoria-supremamonitoring-sa5]]
- [[entity—monitoring—api—wireless]]
- [[entity—monitoring—api—ups]]
- [[entity—monitoring—service—deep-discover]]
- [[concept—saas—multi-tenancy]]