Skip to main content

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.

python
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.

python
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.

python
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).

python
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:

python
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:

python
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:

python
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) - Returns True if the request should be granted access, False otherwise.
  • .has_object_permission(self, request, view, obj) - Returns True 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:

python
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:

python
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:

python
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:

python
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:

python
# 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
python
# 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
python
# 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']
python
# 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:

  1. Anyone can view blog posts
  2. Only authenticated users can create posts
  3. Only the author can edit or delete their posts
  4. 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:

python
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:

  1. Built-in permission classes (AllowAny, IsAuthenticated, IsAdminUser, IsAuthenticatedOrReadOnly)
  2. How to set permissions at global, view, and method levels
  3. Creating custom permissions by extending the BasePermission class
  4. Combining multiple permissions for complex access control
  5. A real-world example implementing permissions in a Blog API
  6. 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

Exercises

  1. Create a custom permission that only allows access during business hours (9am-5pm).
  2. 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
  3. Extend the Blog API example to include a "Comments" feature with appropriate permissions.
  4. 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! :)