When building APIs, one of the most crucial aspects of application design is access control — deciding who can perform which actions. Django REST Framework (DRF) provides a flexible and powerful permission system that integrates tightly with Django’s authentication framework.
This post will walk through everything you need to know about permissions in DRF: how they work, the built-in permission classes, how to define custom permissions, and how to apply them effectively in your projects.
By the end of this guide, you will have a solid understanding of how to protect your APIs from unauthorized access while maintaining flexibility for different user roles.
Table of Contents
- Introduction to Permissions
- How Permissions Work in DRF
- Permission Workflow in an API Request
- Why Permissions Matter
- Applying Permission Classes
- Built-in Permission Classes
- Using
AllowAnyfor Public APIs - Restricting Access with
IsAuthenticated - Administrative Access with
IsAdminUser - Model-Based Permissions with
DjangoModelPermissions - Object-Level Permissions
- Creating a Custom Permission Class
- Example:
IsAuthorOrReadOnly - Combining Multiple Permissions
- Setting Permissions at Different Levels
- Permissions and Authentication
- Advanced Custom Permission Examples
- Debugging and Testing Permissions
- Common Mistakes with Permissions
- Best Practices for Secure Access Control
- Final Thoughts
1. Introduction to Permissions
In the Django REST Framework, permissions define what actions users can perform on API endpoints.
Authentication verifies who the user is, while permissions determine what the user can do.
For instance:
- A non-logged-in user might be able to view posts but not create them.
- An author may edit their own post but not others’.
- Admin users might have unrestricted access to all operations.
Permissions enforce these distinctions to ensure data integrity and application security.
2. How Permissions Work in DRF
Permissions are checked after authentication but before the actual view logic executes.
When a request reaches a view:
- DRF authenticates the user (using authentication classes).
- DRF evaluates all permissions attached to the view.
- If all permission checks pass, the request proceeds.
- If any permission fails, DRF returns a 403 Forbidden or 401 Unauthorized response.
This layered approach ensures that every request is validated before accessing or modifying data.
3. Permission Workflow in an API Request
Let’s visualize how permissions fit into the request cycle:
- The client sends a request (with or without credentials).
- DRF runs the authentication classes to identify the user.
- Then DRF runs permission classes to decide whether the user is allowed to proceed.
- If permission is granted, the view executes and returns a response.
- If permission is denied, DRF returns an error message automatically.
Thus, permissions act as a security gatekeeper in your API architecture.
4. Why Permissions Matter
Without proper permissions, your API may:
- Expose sensitive data to the wrong users.
- Allow unauthorized modification or deletion of records.
- Compromise user privacy and system integrity.
Implementing robust permission logic ensures that only the right people can access or modify specific data.
It’s not only a best practice but a critical security requirement in any production-grade REST API.
5. Applying Permission Classes
Permissions in DRF are applied through the permission_classes attribute on your views or viewsets.
Example using a ViewSet:
from rest_framework import viewsets, permissions
from .models import Book
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [permissions.IsAuthenticated]
Here, only authenticated users can access any action (list, create, retrieve, update, delete) on the BookViewSet.
If an unauthenticated user attempts access, DRF automatically returns:
{
"detail": "Authentication credentials were not provided."
}
6. Built-in Permission Classes
DRF provides several prebuilt permission classes to handle common scenarios.
You can import them directly from rest_framework.permissions:
from rest_framework import permissions
Common built-in classes include:
AllowAnyIsAuthenticatedIsAdminUserIsAuthenticatedOrReadOnlyDjangoModelPermissionsDjangoObjectPermissions
Each class represents a different type of access control logic.
7. Using AllowAny for Public APIs
AllowAny is the most permissive class. It allows unrestricted access to everyone, whether authenticated or not.
Example:
from rest_framework import permissions
class PublicViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all()
serializer_class = BookSerializer
permission_classes = [permissions.AllowAny]
This is ideal for public endpoints such as landing pages, public listings, or documentation APIs.
However, it should be used carefully, as it completely disables access restrictions.
8. Restricting Access with IsAuthenticated
IsAuthenticated ensures that only logged-in users can access the API.
Example:
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAuthenticated]
This is useful for:
- User dashboards
- Profile pages
- APIs where each request must be tied to a known user
When a request lacks valid credentials, DRF returns a 401 Unauthorized error.
9. Administrative Access with IsAdminUser
IsAdminUser restricts access to users with is_staff=True in Django’s User model.
Example:
class AdminOnlyViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.IsAdminUser]
This is useful for:
- Admin dashboards
- Management tools
- Sensitive APIs meant for internal staff only
If a regular user attempts access, they receive:
{
"detail": "You do not have permission to perform this action."
}
10. Model-Based Permissions with DjangoModelPermissions
DjangoModelPermissions ties API permissions to Django’s built-in model-level permissions (add, change, delete, view).
Example:
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.DjangoModelPermissions]
For this to work, users must have model permissions like:
books.add_bookbooks.change_bookbooks.delete_bookbooks.view_book
These are usually managed through the Django admin interface or assigned via groups.
11. Object-Level Permissions
While DjangoModelPermissions control global model access, sometimes you need object-level control — for example, allowing a user to edit only their own objects.
DRF supports this by implementing the method has_object_permission() in custom permission classes.
12. Creating a Custom Permission Class
To create a custom permission, subclass BasePermission and override has_permission() or has_object_permission().
Example structure:
from rest_framework.permissions import BasePermission
class CustomPermission(BasePermission):
def has_permission(self, request, view):
# Check global permission
return True
def has_object_permission(self, request, view, obj):
# Check object-level permission
return obj.owner == request.user
Once defined, attach it to your view:
permission_classes = [CustomPermission]
13. Example: IsAuthorOrReadOnly
A common real-world use case is allowing all users to read data but only the author to modify it.
from rest_framework.permissions import BasePermission
class IsAuthorOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['GET', 'HEAD', 'OPTIONS']:
return True
return obj.author == request.user
This means:
- Anyone can view (
GET,HEAD,OPTIONS). - Only the author can
PUT,PATCH, orDELETE.
Apply it in a viewset:
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthorOrReadOnly]
This approach is perfect for blog posts, comments, or any user-generated content.
14. Combining Multiple Permissions
You can combine multiple permission classes by listing them together:
permission_classes = [permissions.IsAuthenticated, IsAuthorOrReadOnly]
In this case:
- The user must be authenticated.
- They must also be the author to modify the object.
All permissions in the list must pass for access to be granted.
15. Setting Permissions at Different Levels
You can define permissions globally or locally.
Global Permissions (in settings.py)
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
All views will require authentication unless overridden locally.
Local Permissions (in the view)
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [permissions.AllowAny]
This overrides global settings for this specific view.
16. Permissions and Authentication
Permissions depend on the authentication system. If authentication fails, permission checks are skipped, and DRF immediately denies access.
Ensure you have appropriate authentication classes set, such as:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication',
]
}
Without authentication, even IsAuthenticated cannot function correctly.
17. Advanced Custom Permission Examples
Here are a few more real-world examples of custom permission logic.
Example 1: Only Staff Can Delete
class IsStaffOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in ['DELETE']:
return request.user.is_staff
return True
Example 2: User Can Access Only Their Own Profile
class IsSelf(BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user
Example 3: Custom Role-Based Access
If your User model has roles:
class IsManager(BasePermission):
def has_permission(self, request, view):
return hasattr(request.user, 'role') and request.user.role == 'manager'
These examples show how easy it is to integrate custom business rules into DRF’s permission system.
18. Debugging and Testing Permissions
Testing permissions ensures your access control logic behaves as expected.
You can use DRF’s API test client:
from rest_framework.test import APIClient, APITestCase
class PermissionTests(APITestCase):
def setUp(self):
self.client = APIClient()
self.url = '/api/books/'
def test_unauthenticated_access_denied(self):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 401)
For debugging, you can print or log user attributes in your custom permissions to ensure the logic is running correctly.
19. Common Mistakes with Permissions
- Forgetting to Set Authentication Classes
Without authentication, permissions likeIsAuthenticatedalways fail. - Not Returning Boolean Values
has_permission()andhas_object_permission()must returnTrueorFalse. - Ignoring Object-Level Permissions
If you only definehas_permission(), you may forget to restrict per-object access. - Incorrect Permission Order
DRF requires all permissions in the list to pass — one failing denies access. - Global vs Local Conflicts
Remember that local permission settings override global ones.
20. Best Practices for Secure Access Control
- Start with the Principle of Least Privilege
Grant the minimum access necessary for each user role. - Use Built-in Permissions When Possible
Classes likeIsAuthenticatedandIsAdminUserare well-tested and secure. - Keep Custom Permissions Simple and Focused
Avoid overcomplicating logic in a single permission class. - Document Your Access Rules
Clearly specify which roles can access which endpoints. - Combine Authentication and Permission Checks
Ensure every sensitive endpoint has both authentication and permission enforcement. - Test Access Scenarios Regularly
Write automated tests to verify that unauthorized access is always denied. - Avoid Using
AllowAnyin Production
Unless an endpoint is truly public, restrict access appropriately. - Use
IsAuthenticatedOrReadOnlyfor Read-Only Public APIs
This balances openness with protection for write operations. - Integrate Role-Based Permissions When Needed
For complex apps, consider using Django groups or custom roles for scalable permission management.
Leave a Reply