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

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
Saltzer, J. H., & Schroeder, M. D. (1975). The Protection of Information in Computer Systems. MIT.
https://web.mit.edu/Saltzer/www/publications/protection/Evans, E. (2003). Domain-Driven Design: Tackling Complexity in the Heart of Software. Addison-Wesley.
Fowler, M. (2003). Specification Pattern.
https://martinfowler.com/apsupp/spec.pdfFowler, M. (2005). Rules Engines.
https://martinfowler.com/bliki/RulesEngine.htmlKleppmann, M. (2017). Designing Data-Intensive Applications. O’Reilly Media.
Django Software Foundation. Django Authentication and Authorization.
https://docs.djangoproject.com/en/stable/topics/auth/



