Permissions and Access Control

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

  1. Introduction to Permissions
  2. How Permissions Work in DRF
  3. Permission Workflow in an API Request
  4. Why Permissions Matter
  5. Applying Permission Classes
  6. Built-in Permission Classes
  7. Using AllowAny for Public APIs
  8. Restricting Access with IsAuthenticated
  9. Administrative Access with IsAdminUser
  10. Model-Based Permissions with DjangoModelPermissions
  11. Object-Level Permissions
  12. Creating a Custom Permission Class
  13. Example: IsAuthorOrReadOnly
  14. Combining Multiple Permissions
  15. Setting Permissions at Different Levels
  16. Permissions and Authentication
  17. Advanced Custom Permission Examples
  18. Debugging and Testing Permissions
  19. Common Mistakes with Permissions
  20. Best Practices for Secure Access Control
  21. 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:

  1. DRF authenticates the user (using authentication classes).
  2. DRF evaluates all permissions attached to the view.
  3. If all permission checks pass, the request proceeds.
  4. 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:

  1. The client sends a request (with or without credentials).
  2. DRF runs the authentication classes to identify the user.
  3. Then DRF runs permission classes to decide whether the user is allowed to proceed.
  4. If permission is granted, the view executes and returns a response.
  5. 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:

  • AllowAny
  • IsAuthenticated
  • IsAdminUser
  • IsAuthenticatedOrReadOnly
  • DjangoModelPermissions
  • DjangoObjectPermissions

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_book
  • books.change_book
  • books.delete_book
  • books.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, or DELETE.

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:

  1. The user must be authenticated.
  2. 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

  1. Forgetting to Set Authentication Classes
    Without authentication, permissions like IsAuthenticated always fail.
  2. Not Returning Boolean Values
    has_permission() and has_object_permission() must return True or False.
  3. Ignoring Object-Level Permissions
    If you only define has_permission(), you may forget to restrict per-object access.
  4. Incorrect Permission Order
    DRF requires all permissions in the list to pass — one failing denies access.
  5. Global vs Local Conflicts
    Remember that local permission settings override global ones.

20. Best Practices for Secure Access Control

  1. Start with the Principle of Least Privilege
    Grant the minimum access necessary for each user role.
  2. Use Built-in Permissions When Possible
    Classes like IsAuthenticated and IsAdminUser are well-tested and secure.
  3. Keep Custom Permissions Simple and Focused
    Avoid overcomplicating logic in a single permission class.
  4. Document Your Access Rules
    Clearly specify which roles can access which endpoints.
  5. Combine Authentication and Permission Checks
    Ensure every sensitive endpoint has both authentication and permission enforcement.
  6. Test Access Scenarios Regularly
    Write automated tests to verify that unauthorized access is always denied.
  7. Avoid Using AllowAny in Production
    Unless an endpoint is truly public, restrict access appropriately.
  8. Use IsAuthenticatedOrReadOnly for Read-Only Public APIs
    This balances openness with protection for write operations.
  9. Integrate Role-Based Permissions When Needed
    For complex apps, consider using Django groups or custom roles for scalable permission management.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *