In our earlier discussions on Django authentication, useful source we drew a clear line between Authentication (AuthN)—verifying who a user is—and Authorization (AuthZ)—determining what an authenticated user is permitted to do.
While Django provides a robust, built-in permission system out of the box (assigning standard add, change, delete, and view permissions to models and groups), real-world business requirements are rarely that simple. What happens when a user should only be allowed to edit an article if they are the original author? What if access to an endpoint depends on whether a user has an active subscription tier, or whether a project status is set to “Draft”?
When default flags fall short, you need Custom Permissions.
In this comprehensive guide, we will explore how Django’s permission architecture works, how to declare custom model permissions, how to build advanced object-level permissions in Django REST Framework (DRF), and how to maintain a secure, scalable authorization layer.
The Limitations of Built-In Permissions
Out of the box, Django assigns four standard permissions to every registered model:
add_<model>change_<model>delete_<model>view_<model>
These permissions are global. If a user has the change_article permission, they can edit every single article in the database.
However, modern applications require object-level permissions (restricting access to specific database records) or dynamic conditional permissions based on runtime business logic. This is where custom permission classes become essential.
Part 1: Defining Custom Model Permissions in Django
If you want to add bespoke model-level permissions (such as can_publish_article or can_export_reports) without writing complex code, you can define custom permissions directly inside a model’s Meta options.
1. Declaring Permissions in models.py
Python
# models.py
from django.db import models
from django.contrib.auth.models import User
class Article(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
is_published = models.BooleanField(default=False)
class Meta:
permissions = [
("can_publish_article", "Can publish articles"),
("can_archive_article", "Can archive articles"),
]
When you run python manage.py makemigrations and migrate, Django creates these custom permissions in the database Permission table, making them assignable to users and groups via the Django Admin panel or custom management scripts.
2. Checking Model Permissions in Views
You can check these permissions programmatically inside your standard views using the .has_perm() method:
Python
from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from .models import Article
@login_required
def publish_article_view(request, pk):
article = get_object_or_404(Article, pk=pk)
# Check if the user has our custom model permission
if not request.user.has_perm('articles.can_publish_article'):
return redirect('permission_denied')
article.is_published = True
article.save()
return redirect('article_detail', pk=pk)
Part 2: Custom Object-Level Permissions in Django REST Framework (DRF)
When building APIs, model-level checks are often insufficient. You need to evaluate permissions against a specific object instance—for example, verifying whether the requesting user is the author of the specific article they are trying to modify.
DRF makes this seamless through the BasePermission class.
Building IsAuthorOrReadOnly
Let’s build a classic custom permission class: IsAuthorOrReadOnly. This permission allows anyone (including anonymous users) to read articles (GET, HEAD, OPTIONS), but restricts writing, updating, and deleting (POST, PUT, PATCH, DELETE) exclusively to the author of that specific article.
Python
# permissions.py
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow authors of an object to edit it.
Read-only permissions are allowed for any request.
"""
def has_permission(self, request, view):
# Allow safe methods (GET, HEAD, OPTIONS) for everyone
if request.method in permissions.SAFE_METHODS:
return True
# Write methods require the user to be authenticated
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
# Read permissions are allowed for any request
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the author of the article
return obj.author == request.user
Let’s break down the two key methods:
has_permission(request, view): Evaluates whether the request is allowed to hit the endpoint at all (global view-level check).has_object_permission(request, view, obj): Evaluates whether the request is allowed to interact with a specific database record (obj). DRF automatically invokes this method for detail-level actions (retrieve,update,destroy).
Part 3: Applying Custom Permissions to ViewSets
Integrating your custom permission class into a Django REST Framework ViewSet takes just one line of code:
Python
# views.py
from rest_framework import viewsets
from .models import Article
from .serializers import ArticleSerializer
from .permissions import IsAuthorOrReadOnly
class ArticleViewSet(viewsets.ModelViewSet):
queryset = Article.objects.all()
serializer_class = ArticleSerializer
# Apply our custom object-level permission class
permission_classes = [IsAuthorOrReadOnly]
Optimizing Querysets for Object Permissions
When using object-level permissions in list views, ensure your queryset is filtered properly so users only see objects they are authorized to access.
Python
def get_queryset(self):
# Example: Non-staff users can only see published articles or their own drafts
user = self.request.user
if user.is_staff:
return Article.objects.all()
return Article.objects.filter(models.Q(is_published=True) | models.Q(author=user))
Summary Comparison: Permission Types
| Permission Scope | Implementation Mechanism | Best Used For | Granularity |
| Global Model Permissions | Meta.permissions + user.has_perm() | Role-based feature flags, publishing rights, admin tiers. | Model-wide |
| View-Level Permissions | DRF BasePermission.has_permission() | Restricting entire endpoints to staff or premium tiers. | Endpoint-wide |
| Object-Level Permissions | DRF BasePermission.has_object_permission() | Multi-tenant apps, ownership checks (editing own posts). | Record-specific |
Production Best Practices
- Keep Permissions Stateless: Permission classes should evaluate conditions dynamically based on the incoming request and object state. Avoid caching mutable permission checks inside permission classes unless handled carefully.
- Handle Unauthenticated States Gracefully: Always verify
request.user.is_authenticatedinside your custom permission methods before attempting to access user attributes or relationships. Comparingobj.author == request.userwhenrequest.useris an AnonymousUser can lead to unexpected attribute errors or database lookups. - Combine with Throttle and Authentication Layers: Custom permissions should work in harmony with your authentication backend (such as JWT or Session auth) and throttling rules to create a defense-in-depth security architecture.
Conclusion
Custom permissions elevate your Django and Django REST Framework applications from basic data CRUD utilities into secure, read review enterprise-ready systems. Whether you are declaring custom model permissions in your database metadata or building fine-grained object-level authorization rules with BasePermission, mastering custom permissions gives you absolute control over your application’s security perimeter.