Contexto
Auditoría sa6 (monitoring, sesión s108): 9 hallazgos confirmados en vistas HTMX + realtime del Observatory. El hallazgo ALTA más grave:
Las 4 vistas (wireless_view, observatory_view, ups_view, signage_view) auto-provisionan MonitoringTarget/DeviceProfile — escriben en BD. Esas escrituras estaban bajo protección de solo @login_required, sin validación de rol.
Un usuario con permiso observatory:view (solo lectura) podía abrir la página y disparar esas altas automáticas, violando el principio de separación de permisos.
Paralelamente, en los commits #64/#65 se había cerrado el mismo hueco en los endpoints Ninja de monitoring (require_perm aplicado). Las vistas server-rendered quedaron fuera del perímetro.
Problema
- Broken Access Control:
@login_requiredsin role check → cualquier usuario autenticado puede causar escrituras. - Writes en GET: la auto-provisión ocurría durante el render de la página (GET), no idempotente — cada reload refrescaba los datos.
- Duplicación: la lógica de provisión era idéntica (~110 LOC) en 4 vistas hermanas → difícil de mantener y asegurar en todos lados.
- Guarda
org=Nonedébil:get_current_orgpuede devolverNone; las vistas pasaban ese valor a queries sin validar.
Decisión
1. Extraer lógica a un servicio (monitoring/services/provisioning.py)
Beneficio: la lógica de escritura vive en un solo lugar. El caller puede decidir si ejecutarla o no, y puede gatearla detrás de un permiso.
Implementación:
# En vistas:
if has_permission(user, "observatory", "edit"):
provision_profile_targets(org, profiles, scope)
Ahora el caller es responsable de validar el permiso antes de llamar. El servicio no valida (separación de responsabilidades).
2. Gate en las vistas: observatory:edit
Las 4 vistas ahora:
- Obtienen
org = get_current_org(request)— rechaza siorg is None. - Validan
has_permission(user, "observatory", "edit")antes de any write. - Solo si ambas condiciones son verdad, llaman
provision_profile_targets(...). - Un usuario
readonly(observatory:view) sigue viendo la página íntegra pero no escribe.
Beneficio: seguridad defensiva en capas — el permiso es el principal gate; la validación en el servicio es secundaria.
3. Refactorizar monitoring/consumers.py
WebSocket realtime ya tenía aislamiento correcto (no había fuga cross-tenant). Se reforzó:
- Validación de payload JSON + coerción de
target_ids→ evita crash por junk. - Logging de rechazos (anónimo, sin org) → auditoría.
- Rama
elsepara tipo de mensaje desconocido → no caiga en silencio.
4. Cumplir Regla 5: módulo >500 LOC
views.py tenía 677 LOC. Extrayendo la provisión y deduplicando, bajó a 446 LOC.
Implementación
Commit f263a03
Cambios:
-
monitoring/services/provisioning.py(new, 89 LOC):provision_profile_targets(org, profiles, scope)— auto-provisiona batch de targets.build_snmp_config(profile)— compila dict SNMP desde credenciales.
-
monitoring/views.py(modified, 677 → 446 LOC):_can_provision(user, org)helper — validahas_permission(user, "observatory", "edit")+org ≠ None.- Reemplaza 4 copias del loop inline con una llamada a
provision_profile_targets(...)dentro del guard. - Elimina ~110 LOC de duplicación.
-
monitoring/consumers.py(modified, 285 LOC):_coerce_target_ids(raw)— convierte client input a ints limpios.- Validación de payload:
json.loadsen try/except, tipo checkisinstance(data, dict). - Logging en rechazos de conexión.
- Rama
elsepara tipo desconocido.
-
tests/api/test_monitoring.py(modified):+4 tests(TestObservatoryViewsProvisioningGate):- Readonly user no provisiona.
- Admin user sí provisiona.
- User sin org no rompe render.
- WebSocket coerce de
target_ids.
Alternativas consideradas
❌ No extraer — dejar la lógica duplicada en views
Más simple en el corto plazo. Pero:
- Asegurar el gate en 4 lugares simultáneamente es error-prone.
- Cambios futuros (p.ej., cifrar credenciales SNMP) requieren editar 4 archivos.
- Vulnerabilidad a un nuevo hueco en una vista que olvidemos gatear.
❌ Mover a un job Huey asíncrono
Más robusto a largo plazo (no bloquea el request). Pero:
- Aumenta latencia percibida (usuario abre página, ve vacío, espera a que se popule).
- Requiere mensaje de confirmación/UI que diga “descubriendo dispositivos…”.
- Se deja para Etapa 3 como carve-out.
✅ Extraer + gatear en vistas (elegido)
- Lógica centralizada → fácil de mantener y auditar.
- Gate explícito en cada vista que escribe.
- Sin cambios de UX (provisión ocurre igual, pero validada).
- Puente a futuro: si se mueve a Huey, el servicio ya está extraído.
Riesgos mitigados
| Riesgo | Mitigación |
|---|---|
| User readonly dispara writes | Gate observatory:edit + validación de rol en cada vista |
| Concurrent race: dos provisiones simultáneas | bulk_create(..., ignore_conflicts=True) → idempotente |
| Org nula → integrity error | Helper _can_provision rechaza org is None |
| Payload malformado en WebSocket → crash | Try/except + validación de tipo |
| N+1 en provision (mt.save() por iteración) | Extrayendo a servicio y usando bulk_update |
Métricas
- Hallazgos arreglados: 2 ALTA + 4 MEDIA = 6 / 9 confirmados.
- Líneas de código: -110 LOC (dedup) → views.py: 677 → 446.
- Tests agregados: +4 (
TestObservatoryViewsProvisioningGate). - Tokens consumidos (auditoría): 1.33M.
- Complejidad futura: reducida (lógica centralizada).
Próximos pasos (Etapa 3)
- Mover provisión a job Huey (async, fuera del request).
- Hardening en profundidad: nombre de grupo WebSocket
target_<org_id>_<id>(redundancia). - Auditar sa3, sa4, sa5 de monitoring (IA, ITSM, CNS).
Véase también
- [[feature—monitoring—auditoria-suprema-sa6-observatory-realtime]]
- [[entity—monitoring—service—provisioning]]
- [[concept—security—role-based-access-control]]
- [[decision—20260415—require-perm-monitoring-endpoints]]
Referenciado desde
- Auditoría #261 ciclo 4 — el Observatory deja de quedarse dormida
- Auditoría Suprema monitoring sa6 — páginas del Observatory + tiempo real (s108)
- Cerrar Broken Access Control en el CRUD de racks y en /api/settings
- Las gráficas del Observatory salían vacías a ratos: el suavizado se leía fuera del hilo del ORM (task #297)
- Provisioning de MonitoringTargets sale del GET — reconciliación en Huey (sa6-G1, v1.89.0)