Module 4 · Authentication and security

Lesson 21 — Authorization

Roles, permissions and per-resource access control: who can touch what.

Published
In this lesson
  1. Exercise 1 — The matrix (the professor's)
  2. Exercise 2 — IDOR
  3. Exercise 3 — Organizer
  4. Exercise 4 — ABAC
  5. Exercise 5 — Escalation
  6. Professor's summary

Exercise 1 — The matrix (the professor's)

ActionBUYERORGANIZERSTAFFSYSTEM
Event list/detailyesyesyesyes
Create eventnoyesyesno
Edit eventnoonly their own (+ABAC 24h)yesno
Availabilityyesyesyesyes
Create reservationyes (for themselves)yesyesno
View someone else's reservationnonoyesn/a
Cancel reservationonly their ownnoyesexpiration job (expired PENDING)
Start paymentonly their reservationnonosigned webhook
View salesnoof their eventsyesno
Refundnonoyesno
Change rolenonoyesno

The cells with a condition are the ones the framework doesn't ship: they live in object + service.

Exercise 2 — IDOR

  1. 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.
  2. 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.
  3. 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_404 without a filter, the object permission saves you.

Exercise 3 — Organizer

  1. 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.
  2. organizer = serializers.HiddenField(default=serializers.CurrentUserDefault()) or read_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

  1. 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). Raise DomainError("event-locked", 422,...) (13).
  2. draft → 200; published 48h ahead → 200; 2h ahead → 422 event-locked with detail "The event locks for editing 24h before it starts".
  3. 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

  1. Closed: role out 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).
  2. Regression test:
python
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 change

Professor'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.