Module 4 · Authentication and security

Lesson 21 — Authorization

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

Published
In this lesson
  1. Objectives
  2. 1. TicketFlow's roles
  3. 2. The three check levels
  4. 3. IDOR: API hole number 1
  5. 4. RBAC, ABAC and scopes (quick map)
  6. 5. TicketFlow's authorization matrix (a design exercise)
  7. Self-assessment

Stack: DRF · Project: TicketFlow Status: Published Prerequisite: Lesson 20 — Hashing and MFA


Objectives

  1. Distinguish authentication (who you are) from authorization (what you may do) and map TicketFlow's roles.
  2. Implement authorization at three levels: endpoint (permission_classes), object (has_object_permission) and domain (service rules).
  3. Avoid the classic holes: IDOR, escalation through missing checks, and rules that only live in the frontend.

1. TicketFlow's roles

RoleMay
BUYERview events, reserve for themselves, view/cancel THEIR reservations, pay THEIR reservations
ORGANIZERall of the above + create/edit THEIR events, view sales of THEIR events
STAFF/ADMINeverything + 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?):

python
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?):

python
class IsOrganizerOfEvent(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.organizer_id == request.user.id or request.user.is_staff

Level 3 — Domain (the living rule in the service, not in the view):

python
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:

  1. 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).
  2. has_object_permission as a second layer.
  3. 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 role from 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:write without 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

  1. Why must the ownership check ALSO live in the service if it is already in the viewset?
  2. Why does the filtered queryset answer 404 and not 403 for someone else's resource, and what information leak does the 404 avoid?
  3. Give me a TicketFlow ABAC case that pure RBAC cannot express.
  4. How do you prevent an ORGANIZER from self-promoting to STAFF? Where is the exact check?
  5. What is the difference between the tickets:write scope and the ORGANIZER role, and when does each apply?

Continue with the exercises. The solutions only after trying it yourself.