CreaRack-SL

Raíz R6: Fallback silencioso a primer tenant permitía lectura/escritura cross-tenant (CERRADO)

Resumen ejecutivo

Severidad: CRÍTICA (aislamiento multi-tenant quebrado)
Status: 🟢 CERRADO (2026-06-11, commit e786894)
Período afectado: Desde refactor a Django (mono-tenant → multi-tenant incompleto)
Usuarios en riesgo: Cualquier principal autenticado sin organización asignada, o endpoints legacy anónimos

Qué pasaba

El código heredado contenía Organization.objects.first() como fallback en ~26 call-sites. Consecuencia: un principal sin org caía automáticamente al primer tenant de la BD.

Escenarios explorados

Lectura cross-tenant (confirmado)

  • GET /api/users/ → listaba todos los usuarios del tenant #1
  • GET /api/search → buscaba entre los datos del tenant #1 (equipos, racks, etc.)
  • GET /api/logs/data → exponía logs de operaciones ajenas
  • GET /api/status/ → mostraba counts de racks/devices del tenant #1
  • GET /api/settings/company → leía nombre, dirección, logo de la empresa ajena
  • Vista dashboard de racks (/) → mostraba todos los racks del tenant #1
  • Vista reporte de proyecto (/report) → expone datos del tenant #1

Escritura cross-tenant (confirmado)

  • PUT /api/settings/company → podía editar nombre/dirección/logo del tenant #1
  • PUT /api/settings/session-timeout → podía cambiar timeout de sesión del tenant #1
  • PUT /api/settings/onboarding/complete → podía marcar onboarding completado en tenant #1
  • POST /api/users/ → podía crear usuarios en el tenant #1 (como admin sin org)
  • PUT /api/users/{id} → podía editar usuarios del tenant #1
  • DELETE /api/users/{id} → podía eliminar usuarios del tenant #1
  • POST /api/admin/impersonate → podía hacerse pasar por usuarios del tenant #1
  • PUT /api/admin/permissions → podía cambiar permisos de usuarios del tenant #1
  • Vista rack editor (/rack/{id}) → cargaba el rack sin scope de org (404cross-tenant)

Raíz causa

  1. Arquitectura heredada: código mono-empresa que usaba Organization.objects.first() como “la” org por defecto
  2. Refactor incompleto: al pasar a multi-tenant, no se eliminaron los fallbacks; solo se agregó request.user.organization
  3. Falta de validación: endpoints read-only retornaban datos, endpoints write permitían cambios, sin validar org context

Cómo se encontró

Auditoría Suprema (2026-06): reporte de seguridad R6 flagged “Aislamiento multi-tenant incompleto”. Análisis manual de core/api/*, core/views, racks/views → encontrados 26 call-sites.

Solución

Tanda T5 (2026-06-11): eliminación del fallback + fail-closed design:

  1. ✅ get_current_org(request) retorna None si no hay org (no intenta adivinar)
  2. ✅ require_org(request) nuevo helper: lanza 403 si no hay org
  3. ✅ Endpoints read-only: retornan listas vacías, defaults, o counts = 0
  4. ✅ Endpoints write: lanzan 404 si get_current_org() es None
  5. ✅ Vistas (views.py): dashboard y reports muestran vacío
  6. ✅ Bonus: rack_editor ahora scoped a org (antes get_object_or_404(Rack, id=...) sin org)

Cambios de código

Eliminación de fallback (~26 sites):

# ✗ ANTES (inseguro)
org = request.user.organization or Organization.objects.first()

# ✓ DESPUÉS (fail-closed)
org = get_current_org(request)  # retorna None si no hay org
if not org:
    return []  # read-only: vacío
    # o raise HttpError(404, ...) para write

Promoción de utility:

  • require_org movido de monitoring/api/common a core.utils
  • Re-exportado en monitoring/api/common (backward compat)

Scope de rack_editor:

# ✗ ANTES
rack = get_object_or_404(Rack, id=rack_id)  # sin org

# ✓ DESPUÉS
org = request.user.organization if hasattr(...) else None
rack = get_object_or_404(Rack, id=rack_id, organization=org)  # 404 cross-tenant

Testing

14 tests nuevos (tests/api/test_t5_org_fallback.py):

  • ✓ Admin sin org no ve usuarios del tenant #1
  • ✓ CREATE/UPDATE/DELETE de usuarios 400-404 sin org
  • ✓ Settings (company, timeout, onboarding) 404 sin org
  • ✓ Search, logs, status retornan vacío sin org
  • ✓ Dashboard vacío sin org
  • ✓ rack_editor 404 cross-tenant
  • ✓ Helper require_org raises/returns ok

Impacto

Breaking change: Cualquier cliente/bot que dependiera de Organization.objects.first() fallará (intencional).

No-impact: Todo principal normalmente tiene org asignada. Solo afecta a:

  • Testing con users sin org
  • Legacy endpoints que permitían anónimo
  • Bots internos que asumían fallback

Status actual (2026-06-11)

  • ✅ Merged a main (e786894)
  • ✅ Suite en Docker sin regresiones (diff de fallos = 0)
  • ✅ RELEASE_NOTES actualizado (s128)
  • ✅ CHANGELOG actualizado (Unreleased 2026-06-11)

Véase también

  • [[decision—20260611—t5-organizacion-fallback-multitenant]]
  • [[entity—core—utility—require-org]]
  • [[concept—saas—multi-tenancy]]
  • [[entity—core—model—organization]]
  • [[entity—core—model—user]]