Exercise 1 — The matrix (the professor's)
| Action | BUYER | ORGANIZER | STAFF | SYSTEM |
|---|---|---|---|---|
| Event list/detail | yes | yes | yes | yes |
| Create event | no | yes | yes | no |
| Edit event | no | only their own (+ABAC 24h) | yes | no |
| Availability | yes | yes | yes | yes |
| Create reservation | yes (for themselves) | yes | yes | no |
| View someone else's reservation | no | no | yes | n/a |
| Cancel reservation | only their own | no | yes | expiration job (expired PENDING) |
| Start payment | only their reservation | no | no | signed webhook |
| View sales | no | of their events | yes | no |
| Refund | no | no | yes | no |
| Change role | no | no | yes | no |
The cells with a condition are the ones the framework doesn't ship: they live in object + service.
Exercise 2 — IDOR
- If it answered 200 with B's reservation: live hole (and B's 9172 with its data). It is THE most frequent security bug in APIs.
- With a filtered
get_queryset: 404. The 404 avoids confirming the resource's existence (it doesn't leak that it exists but isn't yours; a 403 over enumerable IDs confirms it). With UUIDs (13) + 404: nothing to enumerate nor confirm. - Order: view permissions → queryset filtering (the object isn't even fetched) → has_object_permission (in case the object arrived by another path). Both layers: if tomorrow someone uses
get_object_or_404without a filter, the object permission saves you.
Exercise 3 — Organizer
- B on A's event: 404 (the organizer's queryset filtered by
organizer=request.user) or 403 with an object permission — 404 is preferable for the same leak reason. B on their own: 200. organizer = serializers.HiddenField(default=serializers.CurrentUserDefault())orread_only+ assignment in perform_create. If it accepts organizer from the input: any organizer could create events "on behalf of" another (and hopefully of STAFF): escalation by mass assignment.
Exercise 4 — ABAC
- It lives in the service (
update_event) — not in the serializer nor in the permission (it needs the world's state and will be called from multiple entry points). RaiseDomainError("event-locked", 422,...)(13). - draft → 200; published 48h ahead → 200; 2h ahead → 422
event-lockedwith detail "The event locks for editing 24h before it starts". - Suggested policy: STAFF always cancels; the organizer may cancel if more than 48h remain; the effect: state CANCELLED + all active reservations → CANCELLED with automatic refunds queued (transactions per reservation, 10; notification via 17/29). The ADR records the automatic vs manual refund decision.
Exercise 5 — Escalation
- Closed:
roleout of the user serializer's fields; a separate staff endpoint (POST /api/staff/users/{id}/role/) with an audit log (who, whom, when, before/after — 45/56). - Regression test:
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) # edits what they can (name...)
self.assertEqual(self.buyer.role, "BUYER") # the role did NOT changeProfessor's summary
- Matrix first: holes are visible in the table, not in the code.
- Three layers: endpoint (roles), object (ownership), service (the living rule). All three, always.
- IDOR is closed with the queryset: someone else's resource doesn't exist (404).
- The classic escalation: a sensitive field accepted from input. Roles change through staff doors with auditing.
Next: Lesson 22 — OWASP Top 10.