Stack: DRF · Project: TicketFlow Status: Published Prerequisite: Lesson 20 — Hashing and MFA
Objectives
- Distinguish authentication (who you are) from authorization (what you may do) and map TicketFlow's roles.
- Implement authorization at three levels: endpoint (permission_classes), object (has_object_permission) and domain (service rules).
- Avoid the classic holes: IDOR, escalation through missing checks, and rules that only live in the frontend.
1. TicketFlow's roles
| Role | May |
|---|---|
| BUYER | view events, reserve for themselves, view/cancel THEIR reservations, pay THEIR reservations |
| ORGANIZER | all of the above + create/edit THEIR events, view sales of THEIR events |
| STAFF/ADMIN | everything + refunds, deactivate events, manage users |
| SYSTEM (webhook/worker) | signed gateway actions, expiration jobs |
Authentication (18) answers "you are Ana, user_id=7". Authorization answers: "can Ana SEE reservation 9172?" — and the correct answer is "only if it is hers": the part frameworks don't ship by default.
2. The three check levels
Level 1 — Endpoint (may this class of user call here?):
class EventViewSet(ModelViewSet):
def get_permissions(self):
if self.action in ("create", "update", "partial_update", "destroy"):
return [IsOrganizer()] # custom permission
return [AllowAny()]Level 2 — Object (may they touch THIS resource?):
class IsOrganizerOfEvent(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.organizer_id == request.user.id or request.user.is_staffLevel 3 — Domain (the living rule in the service, not in the view):
def cancel_reservation(user, reservation):
if reservation.user_id != user.id and not user.is_staff:
raise Forbidden() # the ownership rule LIVES in the service
...Why level 3 matters: viewset checks get skipped if tomorrow you call the service from a management command, a Celery worker or an internal webhook. The service is the last line: every ownership rule is evaluated there too (defense in depth).
3. IDOR: API hole number 1
Insecure Direct Object Reference: GET /api/reservations/9173/ when 9173 belongs to someone else — if the viewset doesn't filter by owner, the 404 never arrives and the data leaks. Combined defenses:
- Filtered queryset (the most robust):
get_queryset(): return Reservation.objects.filter(user=request.user)— someone else's object DOES NOT EXIST in your universe → 404, not 403 (you don't confirm existence). has_object_permissionas a second layer.- Public UUIDs (13) so enumerating is expensive — hygiene, not control.
With staff: the queryset adds the exception (| Q(user=request.user) | Q(...) or a branch) — but explicit, not "admin passes by accident".
4. RBAC, ABAC and scopes (quick map)
- RBAC (role-based): BUYER/ORGANIZER/STAFF — roles with fixed permissions. Simple and enough for 80%: implement with roles on the user (you already have
rolefrom 00b) + permissions per role. - ABAC (attribute-based): rules by attributes ("the organizer can edit their event ONLY while state=DRAFT or the event hasn't started") — it isn't a framework: it is the domain rule you write in the service.
- Scopes (OAuth, 19): for API clients (integrators):
tickets:writewithout being a "human role". Scopes limit THE CLIENT; roles limit THE USER; they coexist.
Privilege escalation to watch: an ORGANIZER assigning themselves STAFF via PATCH /users (the users serializer must never accept role from non-staff input); a BUYER promoting themselves organizer of someone else's event (the organizer check is by object, not by role).
5. TicketFlow's authorization matrix (a design exercise)
Before writing checks, write the table: rows=actions, columns=roles, cells=yes/no/condition. The matrix is the review artifact: holes show up in the table before they show up in the code. In the exercises you build it completely.
Self-assessment
- Why must the ownership check ALSO live in the service if it is already in the viewset?
- Why does the filtered queryset answer 404 and not 403 for someone else's resource, and what information leak does the 404 avoid?
- Give me a TicketFlow ABAC case that pure RBAC cannot express.
- How do you prevent an ORGANIZER from self-promoting to STAFF? Where is the exact check?
- What is the difference between the
tickets:writescope and the ORGANIZER role, and when does each apply?
Continue with the exercises. The solutions only after trying it yourself.