Skip to main content

Command Palette

Search for a command to run...

Authorization in Django: From Permissions to Policies — Part 9 — Policies: Making Context Explicit

From Capability to Contextual Validity

Updated
6 min read
Authorization in Django: From Permissions to Policies — Part 9 — Policies: Making Context Explicit

By now, the limits of permissions are no longer abstract.

Permissions work precisely because they are small, static, and boring. They express capability in principle—nothing more. They do not know when an action happens, why it happens, or whether it should happen now. Likewise, they cannot see time, state, ownership, quotas, or transitions—and that blindness is a feature, not a flaw.

And yet, real systems must answer a different question entirely:

Is this action valid right now, for this request, under these conditions?

This is the question permissions cannot answer—by design, so this Part introduces the layer that does.

Not as an extension of permissions or as a framework feature or as a rule engine. But as a deliberate architectural construct: policies.

Capability vs Validity

Permissions answer a narrow, stable question:

May this actor attempt this kind of action at all?

Policies answer a different one:

Is this action allowed now, given the current state of the system?

The distinction matters.

Permissions are static. Context is dynamic.
Permissions survive deployments. Context changes per request.

When systems attempt to encode context into permissions—through state-based codenames, conditional permission checks, or permission churn—the permission layer collapses under responsibilities it was never designed to carry.

Policies exist to prevent that collapse.

What a Policy Is

A policy is a pure decision. It evaluates known facts at a specific moment and returns a clear outcome: allow or deny.

Nothing more.

A policy:

  • Evaluates current, explicit inputs

  • Makes no assumptions about future state

  • Produces a deterministic decision

  • Does not mutate data

  • Does not perform the action it guards

It is not a helper or a shortcut or a place to hide logic.

A policy exists to answer one question clearly, and then step aside.

What a Policy Is Not

Precision here matters, because many systems fail by blurring boundaries.

A policy is not:

  • A permission check

  • A business operation

  • A workflow transition

  • A rule engine

  • A god-object full of conditions

— Policies do not replace permissions.
— They do not orchestrate flows.
— They do not enforce invariants.

They decide whether a proposed action is contextually valid—nothing more.

The Shape of a Policy

Policies do not require a framework to be understood.

Conceptually, they all share the same structure:

  • Subject — who is attempting the action

  • Resource — what the action targets

  • Context — the relevant facts now

  • Decision — allow or deny (optionally with a reason)

Given the same inputs, a policy must always produce the same output. If it cannot, it is not a policy—it is hidden control flow.

This conceptual shape is what keeps policies readable, testable, and stable over time.

A Concrete Policy Example

class PublishArticlePolicy:
    def __init__(self, *, actor, article, now):
        self.actor = actor
        self.article = article
        self.now = now

    def allows(self) -> bool:
        if not self.actor.is_editor:
            return False

        if self.article.status != "draft":
            return False

        if self.article.scheduled_at and self.article.scheduled_at > self.now:
            return False

        return True

if not PublishArticlePolicy(actor=user, article=article, now=now).allows():
    raise PermissionDenied()

This example shows a policy in its simplest correct form: a small, explicit decision that evaluates current facts and returns a clear allow or deny.

All inputs—the actor, the resource, and the relevant context—are passed in directly, making the outcome deterministic and easy to reason about. The policy does not mutate state, perform the action it guards, or attempt to replace permission checks; it assumes capability has already been established and focuses only on contextual validity.

What matters here is not the logic itself, but where it lives.

Conditions involving timing, resource state, or role are evaluated here rather than encoded into permission names or scattered across ad-hoc conditionals. This keeps permissions stable over time while allowing contextual rules to evolve without churn or ambiguity.

Just as importantly, the policy refuses to orchestrate. It does not advance workflows, enforce invariants, or decide what happens next. It answers a single question—is this action valid now?—and then steps aside.

That restraint is what allows policies to remain small, readable, and durable as systems grow.

Where Policies Live

Policies sit at a precise boundary in the request lifecycle.

  • After capability is established - Permission Check

  • Before state is mutated - Invariant correctness check

They are invoked deliberately—not implicitly.
They are not buried in decorators, serializers, or signals.

A system that cannot point to where policy decisions are made does not have policies—it has scattered conditionals.

This placement is not an implementation detail. It is an architectural contract.

Preventing Permission Collapse

The failures explored earlier in the series all share a root cause: permissions being asked to answer questions they cannot answer cleanly.

Policies resolve those failures directly.

They:

  • Eliminate permission churn by removing state from permission identity

  • Replace state-based permission naming with explicit evaluation

  • Keep ownership and timing out of permission checks

  • Allow permission strings to remain stable for years

Permissions regain their original role: a stable capability boundary.
Policies absorb context—explicitly, visibly, and intentionally.

This is the payoff.

Why This Is Not a Rule Engine

The distinction must be drawn sharply.

— Policies are local.
— Rule engines are global.

— Policies evaluate facts.
— Rule engines coordinate systems.

— Policies answer “is this allowed now?”
— Rule engines answer “what should happen next?”

Confusing the two leads to overgeneralized abstractions and brittle systems. Policies remain small precisely because they refuse to orchestrate.

Policy as Contract

A well-defined policy does more than gate actions.

  • It documents intent.

  • It records assumptions.

  • It provides an audit surface.

  • It survives refactors better than conditionals ever will.

Policies age gracefully because they encode why a decision exists—not just how it is enforced.

They are not conveniences.
They are contracts.

The Remaining Gap

And yet—policies still cannot do everything.

They cannot:

  • Enforce system truth

  • Prevent impossible states

  • Guarantee invariants across transitions

A policy can deny an action, but it cannot prove that the system itself remains valid.

That responsibility belongs to the final layer.

In Part 10, we will introduce invariants: the rules the system must never violate—regardless of permissions, policies, or intent.

— Permissions define who may attempt.
— Policies define what is valid now.
— Invariants define what must always be true.

That is not extension. It is architecture.

Bibliography / References

  1. Saltzer, J. H., & Schroeder, M. D. (1975). The Protection of Information in Computer Systems. MIT.
    https://web.mit.edu/Saltzer/www/publications/protection/

  2. Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley.

  3. Fowler, M. (2003). Specification Pattern.
    https://martinfowler.com/apsupp/spec.pdf

  4. Fowler, M. (2005). Rules Engines.
    https://martinfowler.com/bliki/RulesEngine.html

  5. Kleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly Media.

  6. Django Software Foundation. Django Authentication and Authorization.
    https://docs.djangoproject.com/en/stable/topics/auth/

Thinking in Django & DRF

Part 7 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 7 — Failure as a Boundary

Boundaries of Capability in Django Authorization

More from this blog

A

Abhilash PS — Engineering Thought & Software Architecture

23 posts