CreaRack-SL

Portal Approval: revalidar scope del ClientShareLink en cada aprobación

Contexto

En el flujo de aprobación del portal de cliente (signage/api/approvals.py), un cambio pendiente (creado por un cliente externo vía un link público ClientShareLink) puede modificar una playlist o schedule. El change request se almacena con referencias a playlist_id / schedule_id.

Entre el momento en que el cliente crea la petición de cambio (en el portal) y el momento en que un admin del CMS aprueba ese cambio, el scope del ClientShareLink puede cambiar:

  • El admin pudo haber revocado el acceso a esa playlist del link (allowed_playlists ya no incluye el id)
  • El admin pudo haber expirado o desactivado el link

Sin revalidación, un cambio “antiguo” cuyo scope ya fue revocado se aplicaría igual, violando la política de aislamiento del portal.

Decisión

En approvals._apply_payload, revalidar que playlist_id / schedule_id del cambio sigue en el scope permitido del ClientShareLink antes de aplicar.

# signage/api/approvals.py
def _apply_payload(link: ClientShareLink, change: PortalChangeLog):
    payload = change.payload
    
    if 'playlist_id' in payload:
        playlist_id = payload['playlist_id']
        # Revalidar que el playlist sigue en el scope del link
        if not link.allowed_playlists.filter(id=playlist_id).exists():
            raise ValidationError(
                f"Playlist {playlist_id} ya no está en el scope del link"
            )
    
    if 'schedule_id' in payload:
        schedule_id = payload['schedule_id']
        if not link.allowed_schedules.filter(id=schedule_id).exists():
            raise ValidationError(
                f"Schedule {schedule_id} ya no está en el scope del link"
            )
    
    # ... aplicar el cambio

Rationale

  1. Seguridad: El scope del link define qué contenido el cliente externo puede tocar. Un scope revocado debe impedir que cambios previos se apliquen (“revocar = revocar retroactivamente”).

  2. Consistencia: El portal ya valida scope al crear cambios (en portal_update_playlist, etc.); es natural validar también al aprobar.

  3. UX clara: Si el admin revoca un scope, los cambios pendientes relacionados quedan bloqueados (estado pending_revoked o similar, en lugar de pending mudo).

Trade-offs

  • Coste bajo: Una filter().exists() por playlist/schedule (índice por allowed_playlists__id).
  • Cambios no recuperables: Un admin que revoca un scope no puede volver a aplicar cambios pendientes de ese scope. Esto es intencional (si quería, no revoca). Si quiere recuperar, crea un nuevo link y pide al cliente que vuelva a presentar los cambios.

Estado

✅ Implementado en s121 (2026-06-09).

Véase también

  • [[entity—signage—model—clientsharelink]]
  • [[entity—signage—model—portalchangelog]]
  • [[feature—signage—s121-permisos-rol-y-seguridad]]
  • [[concept—saas—multi-tenancy]]