Django REST Permissions
When developing APIs with Django REST Framework, you'll quickly realize that controlling who can access what is crucial for building secure applications. This is where permissions come into play, allowing you to determine whether a request should be granted or denied access based on various factors such as authentication status or user roles.
Introduction to Permissions
Permissions in Django REST Framework determine whether a request should be allowed or denied. They run at the very beginning of the view's lifecycle, before any other code is executed. If a permission check fails, the framework returns either a 403 Forbidden
or a 401 Unauthorized
response.
Permissions work hand-in-hand with authentication. Authentication identifies the user making the request, while permissions determine what that identified user is allowed to do.
Built-in Permission Classes
Django REST Framework comes with several built-in permission classes. Let's explore them:
AllowAny
The AllowAny
permission class allows unrestricted access to the API endpoint, regardless of whether the request is authenticated or not.
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
class PublicEndpoint(APIView):
permission_classes = [AllowAny]
def get(self, request):
return Response({"message": "This is a public endpoint"})
IsAuthenticated
The IsAuthenticated
permission class denies access to any unauthenticated user. Only authenticated users can access the endpoint.
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class ProtectedEndpoint(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({"message": f"Hello, {request.user.username}!"})
IsAdminUser
The IsAdminUser
permission class only allows access to users who have is_staff=True
.
from rest_framework.permissions import IsAdminUser
from rest_framework.views import APIView
class AdminEndpoint(APIView):
permission_classes = [IsAdminUser]
def get(self, request):
return Response({"message": "Welcome, admin!"})
IsAuthenticatedOrReadOnly
This permission class allows authenticated users to perform any request, but allows unauthenticated users to only perform "safe" methods (GET, HEAD, OPTIONS).
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from rest_framework.views import APIView
class ReadOrAuthEndpoint(APIView):
permission_classes = [IsAuthenticatedOrReadOnly]
def get(self, request):
return Response({"message": "Anyone can read this"})
def post(self, request):
return Response({"message": "Only authenticated users can post"})
Setting Permissions
You can set permissions in Django REST Framework at three levels:
1. Global Default
You can set default permissions for all API views in your settings.py
file:
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
2. Per View/ViewSet
You can override the global defaults by setting permission classes on individual views:
from rest_framework.permissions import IsAuthenticated
from rest_framework.views import APIView
class ExampleView(APIView):
permission_classes = [IsAuthenticated]
# ...
3. Per Method
You can even set permissions for specific HTTP methods within a view:
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated, AllowAny
class MixedPermissionsView(APIView):
def get_permissions(self):
if self.request.method == 'GET':
return [AllowAny()]
return [IsAuthenticated()]
def get(self, request):
return Response({"message": "Public data"})
def post(self, request):
return Response({"message": "Private action completed"})
Custom Permission Classes
Building custom permission classes allows you to implement specific access control logic for your application. A custom permission class should inherit from rest_framework.permissions.BasePermission
and override either (or both) of these methods:
.has_permission(self, request, view)
- ReturnsTrue
if the request should be granted access,False
otherwise..has_object_permission(self, request, view, obj)
- ReturnsTrue
if the request should be granted access to the specific object,False
otherwise.
Let's implement a custom permission that only allows users to edit their own profile:
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
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 owner of the profile
return obj.user == request.user
Using the custom permission:
from rest_framework import viewsets
from .permissions import IsOwnerOrReadOnly
from .models import UserProfile
from .serializers import UserProfileSerializer
class UserProfileViewSet(viewsets.ModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserProfileSerializer
permission_classes = [IsOwnerOrReadOnly]
Combining Permissions
You can combine multiple permission classes using logical operators. When you specify multiple permission classes, the request must pass all of them:
from rest_framework import permissions
from rest_framework.views import APIView
from .permissions import IsOwner
class MultiplePermissionsView(APIView):
# User must be authenticated AND be the owner
permission_classes = [permissions.IsAuthenticated, IsOwner]
def get(self, request):
# ...
For more complex scenarios, you can create custom permissions that implement sophisticated logic:
from rest_framework import permissions
class IsAdminOrOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to give full access to admins,
allow owners to edit, and everyone else to view.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed for any request
if request.method in permissions.SAFE_METHODS:
return True
# Allow admin users full access
if request.user.is_staff:
return True
# Write permissions are only allowed to the owner
return obj.user == request.user
Real-World Example: Blog API
Let's create a comprehensive example of a Blog API with different permission levels:
# models.py
from django.db import models
from django.contrib.auth.models import User
class BlogPost(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
author = models.ForeignKey(User, on_delete=models.CASCADE)
created_at = models.DateTimeField(auto_now_add=True)
is_published = models.BooleanField(default=False)
def __str__(self):
return self.title
# permissions.py
from rest_framework import permissions
class IsAuthorOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow authors of a post to edit it.
"""
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
return obj.author == request.user
class CanPublishPost(permissions.BasePermission):
"""
Custom permission to only allow staff members to publish posts.
"""
def has_permission(self, request, view):
if request.method != 'PATCH' and request.method != 'PUT':
return True
# Check if the request is trying to change is_published
if 'is_published' in request.data and request.data['is_published']:
return request.user.is_staff
return True
# serializers.py
from rest_framework import serializers
from .models import BlogPost
class BlogPostSerializer(serializers.ModelSerializer):
author_name = serializers.ReadOnlyField(source='author.username')
class Meta:
model = BlogPost
fields = ['id', 'title', 'content', 'author', 'author_name', 'created_at', 'is_published']
read_only_fields = ['author']
# views.py
from rest_framework import viewsets
from rest_framework.permissions import IsAuthenticatedOrReadOnly
from .models import BlogPost
from .serializers import BlogPostSerializer
from .permissions import IsAuthorOrReadOnly, CanPublishPost
class BlogPostViewSet(viewsets.ModelViewSet):
queryset = BlogPost.objects.all()
serializer_class = BlogPostSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsAuthorOrReadOnly, CanPublishPost]
def perform_create(self, serializer):
serializer.save(author=self.request.user)
In this example:
- Anyone can view blog posts
- Only authenticated users can create posts
- Only the author can edit or delete their posts
- Only staff members can publish posts
Testing Permissions
It's important to test your permissions to ensure they're working correctly. You can do this using Django REST Framework's APITestCase
:
from django.contrib.auth.models import User
from rest_framework.test import APITestCase
from rest_framework import status
from .models import BlogPost
class BlogPostPermissionsTests(APITestCase):
def setUp(self):
# Create two users
self.user1 = User.objects.create_user(username='user1', password='password123')
self.user2 = User.objects.create_user(username='user2', password='password123')
self.admin = User.objects.create_user(username='admin', password='admin123', is_staff=True)
# Create a blog post by user1
self.blog_post = BlogPost.objects.create(
title='Test Post',
content='This is a test post',
author=self.user1
)
def test_unauthenticated_user_can_read(self):
"""Ensure unauthenticated users can read posts"""
response = self.client.get(f'/api/posts/{self.blog_post.id}/')
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_unauthenticated_user_cannot_create(self):
"""Ensure unauthenticated users cannot create posts"""
data = {'title': 'New Post', 'content': 'Content'}
response = self.client.post('/api/posts/', data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
def test_non_author_cannot_update(self):
"""Ensure non-authors cannot update posts"""
self.client.force_authenticate(user=self.user2)
data = {'title': 'Updated Title'}
response = self.client.patch(f'/api/posts/{self.blog_post.id}/', data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_author_can_update(self):
"""Ensure authors can update their posts"""
self.client.force_authenticate(user=self.user1)
data = {'title': 'Updated Title'}
response = self.client.patch(f'/api/posts/{self.blog_post.id}/', data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['title'], 'Updated Title')
def test_non_staff_cannot_publish(self):
"""Ensure non-staff users cannot publish posts"""
self.client.force_authenticate(user=self.user1)
data = {'is_published': True}
response = self.client.patch(f'/api/posts/{self.blog_post.id}/', data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
def test_staff_can_publish(self):
"""Ensure staff users can publish posts"""
self.client.force_authenticate(user=self.admin)
data = {'is_published': True}
response = self.client.patch(f'/api/posts/{self.blog_post.id}/', data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertTrue(response.data['is_published'])
Summary
Django REST Framework permissions provide a powerful and flexible system for controlling access to your API. In this tutorial, we've covered:
- Built-in permission classes (
AllowAny
,IsAuthenticated
,IsAdminUser
,IsAuthenticatedOrReadOnly
) - How to set permissions at global, view, and method levels
- Creating custom permissions by extending the
BasePermission
class - Combining multiple permissions for complex access control
- A real-world example implementing permissions in a Blog API
- Testing permission logic to ensure it works as expected
Effective use of permissions is essential for securing your API and ensuring that users can only access the resources and actions they're authorized to use.
Additional Resources
- Django REST Framework Official Documentation on Permissions
- Django's Authentication System
- Security Best Practices for REST APIs
Exercises
- Create a custom permission that only allows access during business hours (9am-5pm).
- Implement a permission system for a library API where:
- Anyone can view books
- Only librarians can add new books
- Only the user who borrowed a book can mark it as returned
- Extend the Blog API example to include a "Comments" feature with appropriate permissions.
- Create a permission that uses rate-limiting to restrict the number of requests a user can make in a specified time period.
By mastering permissions in Django REST Framework, you'll be able to build secure, robust APIs that properly control access to your application's resources.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)