Throughout our exploration of Django click here for info REST Framework (DRF) authentication classes, we examined how incoming HTTP requests are linked to a specific user identity, answering the fundamental question: Who is making this request?
However, establishing identity is only half of API security. Once a user is authenticated, you must determine whether they are authorized to view, create, update, or delete a specific resource. This responsibility falls entirely upon DRF permission classes.
This comprehensive guide explores how DRF permission classes operate within the request lifecycle, reviews built-in permission options, demonstrates global and view-level configurations, and walks through building custom object-level permissions.
1. How DRF Permission Classes Work
In Django REST Framework’s execution lifecycle, permission checks occur immediately after authentication succeeds (or fails).
While authentication sets request.user and request.auth, DRF permission classes evaluate those attributes against defined business rules. DRF iterates through all permission classes assigned to a view. If any single permission check fails, DRF halts execution immediately and raises an exception, returning an HTTP status code:
401 Unauthorized: If the user is unauthenticated and the permission class requires authentication.403 Forbidden: If the user is authenticated but lacks the necessary rights or privileges to access the requested resource.
Crucially, DRF permissions are evaluated strictly on the server side, ensuring that security policies cannot be bypassed by client-side manipulation.
2. Built-in Permission Classes in DRF
Django REST Framework ships with several production-ready permission classes that cover common access-control patterns out of the box:
1. AllowAny
- Behavior: Unrestricted access. It permits read and write access to any incoming request, regardless of whether the user is authenticated or anonymous.
- Use Case: Public endpoints like homepages, registration views, or public article feeds.
2. IsAuthenticated
- Behavior: Permits access only to authenticated users, rejecting any request where
request.user.is_authenticatedis false. - Use Case: User dashboards, settings panels, or private profile feeds.
3. IsAdminUser
- Behavior: Permits access only if
request.user.is_staffis set toTrue. - Use Case: Administrative panels or backend utility endpoints.
4. IsAuthenticatedOrReadOnly
- Behavior: Allows unrestricted
GET,HEAD, orOPTIONSrequests to anyone (including anonymous visitors), but restrictsPOST,PUT,PATCH, andDELETEmutations strictly to authenticated users. - Use Case: Public blogs or comment sections where anyone can read posts, but only logged-in users can write or comment.
5. DjangoModelPermissions
- Behavior: Integrates directly with Django’s built-in authorization and permission model (
django.contrib.auth). It maps HTTP verbs to database model permissions automatically:GET/HEAD/OPTIONS$\rightarrow$ requiresvieworaddpermission.POST$\rightarrow$ requiresaddpermission.PUT/PATCH$\rightarrow$ requireschangepermission.DELETE$\rightarrow$ requiresdeletepermission.
- Use Case: CRUD endpoints that mirror traditional Django admin permission structures.
3. Global vs. Per-View Configuration
You can configure permission classes globally across your entire project or override them on a per-view basis.
Global Configuration (settings.py)
To enforce a default security baseline across he said all API endpoints, define DEFAULT_PERMISSION_CLASSES in your project settings:
Python
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.TokenAuthentication',
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Per-View Configuration
Because different endpoints have different visibility requirements, you can override global permissions directly inside class-based views or ViewSets:
Python
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
class ProductCatalogView(APIView):
# Override global settings: allow public read access
permission_classes = [AllowAny]
def get(self, request):
return Response({"products": [...]})
class CheckoutView(APIView):
# Require authentication for checkout
permission_classes = [IsAuthenticated]
def post(self, request):
return Response({"status": "Checkout complete"})
4. Building Custom Permission Classes
While built-in permissions handle broad route restrictions, real-world applications frequently require granular control. For instance, you might want to allow any user to view a post, but restrict editing or deleting an article strictly to its original author. This requires object-level permissions.
To create a custom permission class, subclass BasePermission and implement one or both of the following methods:
has_permission(self, request, view): Evaluates general access to the view (route-level check).has_object_permission(self, request, view, obj): Evaluates access to a specific database model instance (object-level check).
Consider a custom permission class that ensures only the author of an article can modify or delete it:
Python
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAuthorOrReadOnly(BasePermission):
"""
Custom permission to allow only authors of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed for any safe HTTP method (GET, HEAD, OPTIONS)
if request.method in SAFE_METHODS:
return True
# Write permissions (PUT, PATCH, DELETE) are restricted to the author
return obj.author == request.user
Applying Object-Level Permissions in ViewSets
When using DRF ModelViewSet or generic API views, object-level permissions (has_object_permission) are invoked automatically during retrieval or mutation actions (retrieve, update, destroy):
Python
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
permission_classes = [IsAuthorOrReadOnly]
If an unauthorized user attempts to update someone else’s article, DRF intercepts the request during has_object_permission and returns a clean 403 Forbidden response.
5. Combining Permissions with Logical Operators
DRF allows you to combine permission classes dynamically using Python’s bitwise operators (&, |, ~), enabling complex conditional access logic without writing custom classes:
Python
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class SensitiveAdminAuditView(APIView):
# Require BOTH authentication AND staff status
permission_classes = [IsAuthenticated & IsAdminUser]
def get(self, request):
return Response({"audit_logs": [...]})
6. Best Practices for DRF Permissions
- Deny by Default: Always set a strict global permission default (such as
IsAuthenticated) in your settings. Make public endpoints explicitly opt-in usingAllowAny. - Implement Object-Level Checks for User Data: Prevent Insecure Direct Object Reference (IDOR) vulnerabilities by combining global views with custom object-level permissions ensuring users access only their own records.
- Keep Permissions Lean: Avoid heavy database queries or complex business logic inside permission classes. Keep checks fast and focused strictly on authorization metadata.
- Test Permission Boundaries: Write automated unit tests verifying that anonymous users, regular authenticated users, and resource owners receive the correct HTTP status codes (
401,403,200) across all endpoints.
Conclusion
Django REST Framework permission classes transform raw authenticated sessions into secure, granular access-control barriers. By leveraging built-in permissions for broad route safety, crafting custom object-level classes for resource ownership, my link and combining rules with logical operators, you ensure that every client interacts solely with data and features intended for them.