Skip to main content

Command Palette

Search for a command to run...

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

Published
4 min read
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.

Thinking in Django & DRF

Part 13 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.

Start from the beginning

Understanding Django Proxy Models: Usage, Benefits, and Limitations

When we first learn Django, models are usually explained in a very concrete way. One model maps to one database table, the relationship is one-to-one, and each row represents an instance of that model. This mental model works well for most use cases ...

More from this blog

A

Abhilash PS — Engineering Thought & Software Architecture

23 posts