Authorization in Django: From Permissions to Policies : Part 7 — Failure as a Boundary
Boundaries of Capability in Django Authorization

Up to this point, Django’s authorization system has been deliberately conservative.
— Permissions are static.
— They are model-scoped.
— They express capability, not context.
If permissions were meant to answer every authorization question, they would have failed. They have not—because that was never their role.
This part examines where permissions stop working, and why those limits are signals, not defects.
The First Failure: Ownership
Consider the most common authorization question in application development:
“Can this user modify this object?”
Permissions can only answer a weaker question:
“Can this user modify objects of this type?”
That gap is intentional. Ownership is not a property of a model class. It is a relationship between a user and a specific row.
No amount of permission granularity can encode:
— “Only the creator of this order may cancel it”
— “A user may edit their own profile, but not others”
— “A tenant admin may manage users within their tenant, but not outside it”
These are not missing permissions.
They are state-dependent facts.
Trying to model ownership with permissions usually leads to one of two anti-patterns:
Exploding permission sets (
edit_own,edit_any,edit_team,edit_org)Conditional permission checks that quietly smuggle logic into the authorization layer
# Example for logic smuggling
def has_permission(self, request, view):
return (
request.user.has_perm("orders.change_order")
and view.get_object().status == OrderStatus.OPEN
and view.get_object().owner == request.user
and not view.get_object().is_locked
)
Permission check
├── capability (belongs here)
├── ownership (does not)
├── state (does not)
└── invariant (does not)
In both cases, permissions are being asked to carry information they were never designed to hold.
The Second Failure: State
Permissions are timeless. They do not change based on what is happening to an object.
They do not know whether something is:
Draft or published
Open or closed
Active or archived
Pending, approved, rejected, or expired
Yet many real authorization rules depend entirely on state.
— An order can only be canceled while it is pending.
— An invoice can only be edited before it is issued.
— A recipe cannot be modified once it is archived.
These rules are not about who can act. They are about when an action is valid.
This creates a direct tension. Permissions describe potential capability. State rules enforce temporal validity.
When systems try to force state into permissions, they tend to drift into fragile designs:
Revoking and re-granting permissions on every state change
Encoding state checks into permission names
Treating permission updates as workflow transitions
At that point, the permission table starts behaving like a state machine—without transitions, guarantees, or invariants.
The Third Failure: Context
Permissions are global. They apply everywhere, without awareness of circumstance.
They do not know:
Which tenant the request belongs to
Which environment it is running in
Which workflow step is active
Which business rule triggered the action
But many authorization decisions depend entirely on that context.
— Support staff may act only during escalation.
— An operation may be allowed in staging but forbidden in production.
— Bulk updates may run only through automated jobs, not user requests.
These rules are not about capability alone. They are about where, when, and why an action occurs.
When context is forced into permissions, meaning collapses. Permission names grow longer, denser, and still fail to explain their intent.
At that point, authorization stops being declarative and becomes accidental.
The Critical Insight: These Are Not Missing Features
It is easy to treat these failures as gaps. To say that Django permissions are too simple, that object-level checks should exist everywhere, or that the framework ought to handle more for us.
That view is backwards.
Permissions fail in these cases because they are doing exactly what they are meant to do. They draw a clear boundary around their responsibility.
Their role is limited and deliberate. They exist
to identify who may attempt an action
to expose a stable and inspectable surface of capability
to remain static across deployments and environments.
What they explicitly refuse to decide is just as important.
They
do not determine whether an action is valid at a given moment.
do not evaluate object state.
do not enforce business rules or protect invariants.
Those questions are not missing from the system. They belong elsewhere.
Failure as a Signal, Not a Bug
Every place where permissions fall short points to a different architectural tool:
| Question Type | Proper Tool |
| Who may attempt this action? | Permissions |
| Is this object in the right state? | State machine |
| Does this violate system guarantees? | Invariants |
| Is this allowed under business rules? | Policy layer |
When permissions are forced to answer all four, systems rot quietly.
When boundaries are respected, systems stay legible.
Django’s choice is intentional: it stops permissions early so they do not metastasize into an implicit policy engine.
What This Means Practically
When authorization feels incomplete, the solution is not to keep extending permissions.
The right response is to pause and ask what kind of question is being answered. Is this about capability, or does it depend on state, time, identity, or relationships? Is the system checking whether something may be attempted, or whether it should be allowed?
If the question is not about capability, permissions are already the wrong tool.
Where This Leads Next
We have now reached the boundary of Django’s built-in authorization model.
Beyond that boundary are policies, domain invariants, workflow-aware enforcement, and authorization treated as a first-class architectural concern.
Part 8 begins assembling these pieces. It separates permissions, policies, and invariants, and shows how they work together without collapsing into one another.
Not by extending Django’s permission system—but by placing it exactly where it belongs.
Bibliography / References
Django Documentation — Permissions and Authorization — Django Software Foundation — https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions-and-authorization
Django REST Framework — Permissions — Tom Christie — https://www.django-rest-framework.org/api-guide/permissions/
Authorization vs Authentication — OWASP Foundation — https://owasp.org/www-community/Authorization
Patterns of Enterprise Application Architecture — Martin Fowler — Addison-Wesley, 2002
Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans — Addison-Wesley, 2003
Policy-Based Access Control (PBAC) — NIST SP 800-162 — https://csrc.nist.gov/publications/detail/sp/800-162/final
Designing Data-Intensive Applications — Martin Kleppmann — O’Reilly Media, 2017
Clean Architecture — Robert C. Martin — Prentice Hall, 2017



