Skip to main content

Command Palette

Search for a command to run...

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

Boundaries of Capability in Django Authorization

Updated
5 min read
Authorization in Django: From Permissions to Policies : Part 7 — Failure as a Boundary

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 TypeProper 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

  1. Django Documentation — Permissions and Authorization — Django Software Foundation — https://docs.djangoproject.com/en/stable/topics/auth/default/#permissions-and-authorization

  2. Django REST Framework — Permissions — Tom Christie — https://www.django-rest-framework.org/api-guide/permissions/

  3. Authorization vs Authentication — OWASP Foundation — https://owasp.org/www-community/Authorization

  4. Patterns of Enterprise Application Architecture — Martin Fowler — Addison-Wesley, 2002

  5. Domain-Driven Design: Tackling Complexity in the Heart of Software — Eric Evans — Addison-Wesley, 2003

  6. Policy-Based Access Control (PBAC) — NIST SP 800-162 — https://csrc.nist.gov/publications/detail/sp/800-162/final

  7. Designing Data-Intensive Applications — Martin Kleppmann — O’Reilly Media, 2017

  8. Clean Architecture — Robert C. Martin — Prentice Hall, 2017

Thinking in Django & DRF

Part 8 of 13

Thinking in Django & DRF is a series about Django & DRF by understanding why things are designed the way they are. It explores insights from mastering Django & DRF, like syntax, shortcuts, abstraction, invariants, and architectural boundaries.

Up next

Authorization in Django: From Permissions to Policies: Part 5 — Permissions and Groups at the Database Level

Stable Identities for Deterministic Permission Resolution

More from this blog

A

Abhilash PS — Engineering Thought & Software Architecture

23 posts