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_playlistsya 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
-
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”).
-
Consistencia: El portal ya valida scope al crear cambios (en
portal_update_playlist, etc.); es natural validar también al aprobar. -
UX clara: Si el admin revoca un scope, los cambios pendientes relacionados quedan bloqueados (estado
pending_revokedo similar, en lugar dependingmudo).
Trade-offs
- Coste bajo: Una
filter().exists()por playlist/schedule (índice porallowed_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]]