Custom Permission Classes in Django REST Framework

Last updated July 5th, 2021

This article looks at how to build custom permission classes in Django REST Framework (DRF).

--

Django REST Framework Permissions Series:

  1. Permissions in Django REST Framework
  2. Built-in Permission Classes in Django REST Framework
  3. Custom Permission Classes in Django REST Framework (this article!)

Contents

Objectives

By the end of this article, you should be able to:

  1. Create custom permission classes
  2. Explain when to use has_permission and has_object_permission in your custom permission classes
  3. Return a custom error message when a permission is denied
  4. Combine and exclude permission classes using AND, OR, and NOT operators

Custom Permission Classes

If your application has some special requirements and the built-in permission classes don't meet those requirements, it's time to start building your own custom permissions.

Creating custom permissions allows you to set permissions based on whether the user is authenticated or not, the request method, the group that the user belongs to, object attributes, the IP address... or any of their combinations.

All permission classes, either custom or built-in, extend from the BasePermission class:

class BasePermission(metaclass=BasePermissionMetaclass):
    """
    A base class from which all permission classes should inherit.
    """

    def has_permission(self, request, view):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

    def has_object_permission(self, request, view, obj):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

BasePermission has two methods, has_permission and has_object_permission, that both return True. Permission classes override one or both of those methods to conditionally return True. If you don't override the methods, they will always return True, granting unlimited access.

For more on has_permission vs has_object_permission, be sure to check out the first article in this series, Permissions in Django REST Framework.

By convention, you should put custom permissions in a permissions.py file. This is just a convention so you don't have to do this if you need to organize your permissions differently.

As with the built-in permissions, if any of the permission classes used in a view returns False from either has_permission or has_object_permission, a PermissionDenied exception is raised. To change the error message associated with the exception, you can set a message attribute directly on your custom permission class.

With that, let's look at some examples.

Custom Permission Examples

User Properties

You may want to give different levels of access to different users, based on their properties -- i.e., are they the creators of the object or are they a staff member?

Let's say you don't want staff members to be able to edit objects. Here's how a custom permission class for that case might look like:

# permissions.py

from rest_framework import permissions


class AuthorAllStaffAllButEditOrReadOnly(permissions.BasePermission):

    edit_methods = ("PUT", "PATCH")

    def has_permission(self, request, view):
        if request.user.is_authenticated:
            return True

    def has_object_permission(self, request, view, obj):
        if request.user.is_superuser:
            return True

        if request.method in permissions.SAFE_METHODS:
            return True

        if obj.author == request.user:
            return True

        if request.user.is_staff and request.method not in self.edit_methods:
            return True

        return False

Here, the AuthorAllStaffAllButEditOrReadOnly class extends BasePermission and overrides both has_permission and has_object_permission.

has_permission:

In has_permission only one thing gets checked: If the user is authenticated. If not, the NotAuthenticated exception is raised and access is denied.

has_object_permission:

Since you should never limit the superuser's access, the first check -- request.user.is_superuser -- grants access to the superuser.

Next, we check if the request method is one of the "safe" ones -- request.method in permissions.SAFE_METHODS. Safe methods are defined in rest_framework/permissions.py:

SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')

These methods have no impact on the object; they can only read it.

At first glance, it may seem like the SAFE_METHODS check should be in the has_permission method. If you're only checking the request method then that's the place it should be. But in this case, other checks wouldn't be executed:

DRF Permissions Execution

Since we want to grant access when the method is one of the safe ones or when the user is the author of the object or when the user is a staff member, we need to check that on the same level. In other words, since we can't check for the owner on the has_permission level, we need to check everything on the has_object_permission level.

The last possibility is that the user is a staff member: They are allowed all methods but the ones we defined as edit_methods.

Finally, turn back to the class name: AuthorAllStaffAllButEditOrReadOnly. You should always try to name the permission class as informative as possible.

Keep in mind that has_object_permission is never executed for list views (regardless of the view you're extending from) or when the request method is POST (since the object doesn't exist yet).

You use the custom permission class the same way as the built-in one:

# views.py

from rest_framework import viewsets

from .models import Message
from .permissions import AuthorAllStaffAllButEditOrReadOnly
from .serializers import MessageSerializer


class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [AuthorAllStaffAllButEditOrReadOnly] # Custom permission class used

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

The author of the object has full access to it. A staff member, meanwhile, can delete the object, but can't edit it:

DRF Permissions Execution

And an authenticated user can view the object, but can't edit or delete it:

DRF Permissions Execution

Object Properties

Although we briefly touched on the object's property in the previous example, the emphasis was much more on the user's properties (e.g., the object's author). In this example, we'll focus on the object's properties.

How can one or more of the object's properties have an impact on the permissions?

  1. As in the previous example, you can limit the access only to the owner of the object. You can also limit access to the group the owner belongs to.
  2. Objects may have an expiration date, so you can limit the access to objects older than n to only some users.
  3. You can have DELETE implemented as a flag (so that it's not actually removed from the database). You can then prevent access to objects with a delete flag.

Let's say you want to restrict access to objects older than 10 minutes for everyone except superusers:

# permissions.py

from datetime import datetime, timedelta

from django.utils import timezone
from rest_framework import permissions

class ExpiredObjectSuperuserOnly(permissions.BasePermission):

    def object_expired(self, obj):
        expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10))
        return obj.created < expired_on

    def has_object_permission(self, request, view, obj):

        if self.object_expired(obj) and not request.user.is_superuser:
            return False
        else:
            return True

In this permission class, the has_permission method is not overridden -- so it will always return True.

Since the only important property is the object's creation time, the check happens in has_object_permission (since we don't have access to an object's properties in has_permission).

So, if a user wants to access the expired object, the exception PermissionDenied is raised:

DRF Permissions Execution

Again, as with the previous example, we could check if the user is a superuser in has_permission, but if they're not, the object's property would never get checked.

Take note of the error message. It's not very informative. The user has no idea why their access was denied. We can create a custom error message by adding a message attribute to our permission class:

class ExpiredObjectSuperuserOnly(permissions.BasePermission):

    message = "This object is expired." # custom error message

    def object_expired(self, obj):
        expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10))
        return obj.created < expired_on

    def has_object_permission(self, request, view, obj):

        if self.object_expired(obj) and not request.user.is_superuser:
            return False
        else:
            return True

Now the user sees exactly why the permission was denied:

DRF Permissions Execution

Combining and Excluding Permission Classes

Typically, when using more than one permission class, you'd define them in the view like so:

permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]

This approach combines them so that permission is granted only if all of the classes return True.

Since DRF version 3.9.0, you can also combine multiple classes using the AND (&) or OR (|) logical operators. Also, since 3.9.2, the NOT (~) operator is supported.

These operators are not limited to custom permission classes. They can also be used with the built-in ones.

Instead of creating many complicated permission classes that are similar to each other, you could create simpler classes and combine them with the aforementioned operators.

For example, you may have different permissions for different combinations of groups. Let's say you want the following permissions:

  1. Permission for groups A or B
  2. Permission for groups B or C
  3. Permission for members of both B and C
  4. Permission for all groups but A

While four permission classes doesn't seem like much this won't scale well. What if you had eight different groups -- A, B, C, D, E, F, G? It would quickly ballon to a point where it's impossible to understand and maintain.

You could simplify it and combine them with operators by first creating permission classes for groups A, B, and C. Then, you can implement them like so:

  1. permission_classes = [PermGroupA | PermGroupB]
  2. permission_classes = [PermGroupB | PermGroupC]
  3. permission_classes = [PermGroupB & PermGroupC]
  4. permission_classes = [~PermGroupA]

When OR (|) is involved, things can get a little more complicated. Errors can often fall through the cracks. For more, review the discussion on the permissions: Allow permissions to be composed pull request.

AND Operator

AND is the default behavior of permission classes, achieved by using ,:

permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]

It can also be written with &:

permission_classes = [IsAuthenticated & IsStaff & SomeCustomPermissionClass]

OR Operator

With the OR (|), when any of the permission classes return True, the permission is granted. You can use the OR operator to offer multiple possibilities in which the user gets granted permission.

Let's look at an example where either the owner of the object or a staff member can edit or delete the object.

We'll need two classes:

  1. IsStaff returns True if the user is_staff
  2. IsOwner returns True if the user is the same as the obj.author

Code:

class IsStaff(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.user.is_staff:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        if request.user.is_staff:
            return True
        return False


class IsOwner(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.user.is_authenticated:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        if obj.author == request.user:
            return True
        return False

There's quite a bit of redundancy here, but it's necessary.

Why?

  1. For covering list views

    Again, the list view doesn't check for has_object_permission. However, each of the created permissions needs to be standalone. You shouldn't create a permission class that needs to be combined with another permission class to cover the list view. IsOwner limits access to authenticated users in has_permission -- so that if the IsOwner is the only class used, access to API is still controlled.

  2. Both methods by default return True

    When using OR, if you don't provide the has_object_permission method, the user will have access to the object, even though they shouldn't.

    Notes:

    • if you omit has_permission on the IsOwner class, anyone will be able to see or create on the list.

    • if you omit has_object_permission on IsStaff and combine it with IsOwner with or, either one or the other will return True. That way, a registered user that's neither an owner nor a staff member, would be able to change the content.

Now, when we have our permission classes well designed, it's easy to combine them:

from rest_framework import viewsets

from .models import Message
from .permissions import IsStaff, IsOwner
from .serializers import MessageSerializer


class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [IsStaff | IsOwner] # or operator used

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

Here, we allow either a staff member or the owner of the object to change or delete it.

The only requirement IsOwner has for the list views, is that the user is authenticated. That means that the authenticated user that's not a staff member, will be able to create objects.

NOT Operator

The NOT operator results in the exact opposite to the defined permission class. In other words, permission is granted to all users except the ones from the permission class.

Let's say you have three groups of users:

  1. Tech
  2. Management
  3. Finances

Each of these groups should have access to API endpoints meant only for their specific group.

Here's a permission class that grants access to only members of the Finances group:

class IsFinancesMember(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.user.groups.filter(name="Finances").exists():
            return True

Now, say you have a new view that's meant for all users who are not part of the Finances group. You can use the NOT operator to implement this:

from rest_framework import viewsets

from .models import Message
from .permissions import IsFinancesMember
from .serializers import MessageSerializer


class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [~IsFinancesMember] # using not operator

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

So, only members of the Finances group won't have access.

Be careful! If you only use the NOT operator, everybody else will be allowed access, including unauthenticated users! If that's not what you meant to do, you can fix that by adding another class like so:

permission_classes = [~IsFinancesMember & IsAuthenticated]

Parentheses

Inside permission_classes you can also use parentheses (()) to control which expression gets resolved first.

Quick example:

class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [(IsFinancesMember | IsTechMember) & IsOwner] # using parentheses

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

In this example, (IsFinancesMember | IsTechMember) will be resolved first. Then, the result of that will be used with & IsOwner -- e.g., ResultsFromFinancesOrTech & IsOwner. This means that a user that's either a member of the Tech OR Finances groups AND is the owner of the object will be granted access.

Conclusion

Despite a wide range of built-in permission classes, there will be situations where they won't fit your needs. That's when custom permission classes come in handy.

With custom permission classes you must override one or both of the following methods:

  • has_permission
  • has_object_permission

If permission isn't granted in the has_permission method, it doesn't matter what's written in has_object_permission -- the permission is denied. If you don't override one (or both) of them, you need to take into account that by default, the method will always return True.

You can combine and exclude permission classes with the AND, OR, and NOT operators. You can even decide the order in which permissions resolve in with parenthesis.

--

Django REST Framework Permissions Series:

  1. Permissions in Django REST Framework
  2. Built-in Permission Classes in Django REST Framework
  3. Custom Permission Classes in Django REST Framework (this article!)

Špela Giacomelli (aka GirlLovesToCode)

Špela Giacomelli (aka GirlLovesToCode)

GirlThatLovesToCode is passionate about learning new things -- both for herself and for teaching others. She's a fan of Python and Django and wants to know everything there is about those two. When she’s not writing code or a blog, she’s probably trying something new, reading, or spending time outside with her family.

Share this tutorial

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.

Featured Course

Test-Driven Development with Django, Django REST Framework, and Docker

In this course, you'll learn how to set up a development environment with Docker in order to build and deploy a RESTful API powered by Python, Django, and Django REST Framework.