Ejercicio 1 — La matriz (la del profesor)
| Acción | BUYER | ORGANIZER | STAFF | SYSTEM |
|---|---|---|---|---|
| Listar/detalle evento | sí | sí | sí | sí |
| Crear evento | no | sí | sí | no |
| Editar evento | no | solo el suyo (+ABAC 24h) | sí | no |
| Disponibilidad | sí | sí | sí | sí |
| Crear reserva | sí (para sí) | sí | sí | no |
| Ver reserva ajena | no | no | sí | n/a |
| Cancelar reserva | solo la suya | no | sí | job de expiración (PENDING vencidas) |
| Iniciar pago | solo su reserva | no | no | webhook firmado |
| Ver ventas | no | de sus eventos | sí | no |
| Reembolsar | no | no | sí | no |
| Cambiar rol | no | no | sí | no |
Las celdas con condición son las que el framework no trae: viven en objeto + servicio.
Ejercicio 2 — IDOR
- Si respondió 200 con la reserva de B: agujero en vivo (y la 9172 de B con sus datos). Es EL bug de seguridad más frecuente en APIs.
- Con
get_querysetfiltrado: 404. El 404 evita confirmar la existencia del recurso (no filtra que existe pero no es tuyo; el 403 sobre IDs enumerables confirma). Con UUIDs (13) + 404: nada que enumerar ni confirmar. - Orden: permisos de vista → filtrado de queryset (el objeto ni se busca) → has_object_permission (por si el objeto llegó por otra vía). Ambas capas: si mañana alguien usa
get_object_or_404sin filtro, el permission de objeto salva.
Ejercicio 3 — Organizer
- B sobre evento de A: 404 (queryset del organizador filtrado por
organizer=request.user) o 403 con permission de objeto — el 404 es preferible por la misma fuga. B sobre el suyo: 200. organizer = serializers.HiddenField(default=serializers.CurrentUserDefault())oread_only+ asignación en perform_create. Si acepta organizer del input: cualquier organizador podría crear eventos "a nombre de" otro (y con suerte de STAFF): escalada por mass assignment.
Ejercicio 4 — ABAC
- Vive en el servicio (
update_event) — no en el serializer ni en el permission (necesita estado del mundo y será llamada desde múltiples entradas). LanzaDomainError("event-locked", 422,...)(13). - draft → 200; publicado a 48h → 200; a 2h → 422
event-lockedcon detail "El evento se bloquea a la edición 24h antes de comenzar". - Política sugerida: STAFF cancela siempre; el organizador puede cancelar si faltan >48h; el efecto: estado CANCELLED + todas las reservas activas → CANCELLED con reembolso automático en cola (transacciones por reserva, la 10; la notificación por la 17/29). El ADR registra la decisión de reembolso automático vs manual.
Ejercicio 5 — Escalada
- Cerrado:
rolefuera de los fields del serializer de usuario; endpoint de staff aparte (POST /api/staff/users/{id}/role/) con audit log (quién, a quién, cuándo, antes/después — la 45/56). - Test de regresión:
def test_buyer_no_puede_elescarse(self):
self.client.force_authenticate(self.buyer)
resp = self.client.patch("/api/users/me/", {"role": "STAFF"}, format="json")
self.buyer.refresh_from_db()
self.assertEqual(resp.status_code, 200) # edita lo que puede (nombre...)
self.assertEqual(self.buyer.role, "BUYER") # el rol NO cambióResumen del profesor
- Matriz primero: los agujeros se ven en la tabla, no en el código.
- Tres capas: endpoint (roles), objeto (propiedad), servicio (regla viva). Las tres, siempre.
- IDOR se cierra con queryset: el recurso ajeno no existe (404).
- La escalada clásica: campo sensible aceptado del input. Los roles se cambian por puertas de staff con auditoría.
Después: Lección 22 — OWASP Top 10.