Volver a la wiki

Cierre de BAJA/MEDIA en sa5 (wireless+UPS) — Auditoría Suprema Sesión 112

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:

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:

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:

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:

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:

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:

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:

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:

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:

  1. test_available_metrics_unknown_profile_404: Verifica que GET /wireless/available-metrics/999999 devuelve 404
  2. test_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ÁreaDescripción
sa5 B2Correctnessavailable-metrics → 404 para profile desconocido
sa5 B5PerfBatch m2m en assign_profiles_to_group
sa5 B8RLSDashboard layout/notas por-usuario, no por-org
sa5 B9ObservabilidadLog (no silent pass) en vendor OID lookup
sa5 M1/M5RobustezIdempotencia en deep-discover (anti-DoS)
sa5 M6RobustezTimeout 15s en queries a VictoriaMetrics

Items deferidos (con justificación):


Véase también

Subir