Authorization in Django: From Permissions to Policies : Part 1 — Why Authorization Feels Confusing in Django

Authorization in Django often feels unclear at first because permissions, groups, roles, and access checks appear disconnected, and the boundary between framework responsibility and application responsibility is rarely made explicit.
This post explains why that confusion exists, what Django’s authorization system actually provides, and how to approach it with a mental model that keeps authorization predictable and maintainable as systems grow.
What this post covers (and what it does not)
Covered here
Why authorization is inherently complex
What Django permissions are meant to represent
The responsibility split that makes Django authorization understandable
Covered later in the series
How permissions are stored in database tables (Parts 2 and 5)
ContentTypes and how Django identifies models (Part 3)
How default add / change / delete / view permissions are created (Part 4)
Policy objects and invariant-driven authorization (Parts 8–10)
If it feels like something “practical” is missing at this stage, that is intentional. This part establishes the conceptual foundation.
Authorization is inherently complex
Authorization answers intertwined questions:
Who may perform an action, on which data, and under what conditions?
These questions depend on business rules, workflows, and security constraints. This complexity exists in every framework, not just Django.
Django does not attempt to encode all of these rules. Instead, it provides a consistent and predictable foundation, leaving context-specific decisions to application logic.
Understanding this design choice early prevents many misunderstandings later.
Core terminology (used throughout this series)
Before going further, it helps to align on a few terms that will recur frequently:
→ Permission: A named capability that represents “this user may attempt this type of action.”
→ Group: A named collection of permissions, commonly used to model roles by convention.
→ Role: Not a first-class Django concept; typically implemented using groups or external policy logic.
→ Access check: The enforcement point where authorization is evaluated (admin, views, DRF permission classes, services, background tasks, etc.).
Django provides the permission data model and basic checking utilities. Enforcement happens wherever application code performs actions.
This split is a major source of confusion if it is not made explicit.
Why Django authorization feels confusing?
1. Permissions appear before they are explained
Permissions often appear early in Django codebases:
user.has_perm("blog.add_entry")
At that point, it may not yet be clear:
where this permission comes from
how it is stored
what it represents internally
what it does not enforce
Without this context, permissions can feel abstract rather than concrete.
2. Permissions resemble rules, but they are not
It is common to assume that a permission such as change_entry enforces rules like:
ownership (only the author can edit)
workflow state (only drafts can be edited)
But, Django permissions do not encode these rules. Rather, they intentionally express the capability, not the context:
“This user is allowed to attempt this type of action.”
They do not determine whether the action is correct, appropriate, or valid in a specific situation.
3. Permissions do not enforce anything by themselves
This is the single most important expectation to set early:
A Django permission is not protection unless something checks it.
Permissions are data. They become meaningful only when enforcement points explicitly evaluate them:
admin integration
decorators/mixins
DRF permission classes
view logic
service or domain logic
If a code path does not perform an authorization check, the existence of permissions has no effect.
4. Authorization logic becomes fragmented
Because Django permissions are intentionally simple, additional checks are often introduced across:
views
serializers
templates
model methods
When this happens without a clear structure, authorization logic becomes fragmented and difficult to reason about.
The confusion comes not from missing features, but from unclear responsibility boundaries.
What Django permissions are designed to handle?
Django’s permission system is optimized for:
coarse-grained access control
role-based authorization
admin interface integration
fast and predictable permission checks
It works well when answering: “Should this user be allowed to attempt this operation at all?”
It is not intended to answer:
whether an object is in the correct state?
whether the user owns the object?
whether the action satisfies business rules?
Those concerns belong elsewhere in the system.
The takeaway
Django permissions are not a rule engine, and they are not meant to be.
They are a capability system: simple, explicit, and reliable. They work best as coarse-grained gates that define who may attempt an action.
Once the question becomes “who can do what to which object under what conditions,” the problem has moved into policy and domain logic.
A practical mental model is:
Permissions determine who may attempt an action. Domain and application logic determine whether the action is valid.
Keeping these responsibilities separate makes authorization easier to design, test, and maintain—especially as systems grow in complexity.



