Custom Permission Classes in Django Rest Framework

Prithoo Medhi
7 min readMay 1, 2022
Custom Permission Classes in Django Rest Framework

Django is a full-stack web-development framework intended to build complete web applications from scratch within tight deadlines; this feature is even the main selling point of their tagline:

The web framework for perfectionists with deadlines.

What Django was not originally designed to do, however, was to make Rest APIs; which is unfortunate since APIs and especially REST APIs are what make the internet of today click (no pun intended).

Django Rest Framework

This is where Django Rest Framework comes in. It is, as described in their own documentation, “a powerful and flexible toolkit for building Web APIs”. It basically extends Django’s capabilities to include the ability to construct Rest APIs. Besides this main piece of functionality, Rest Framework also comes with many modules which further append to Django’s already impressive list of capabilities. Today, we are going to talk about Permission Classes and how a custom one can be built.

Permission Classes are tags that define what kind of user/site visitor can access a particular API and what kind of user/site visitor cannot. They work in conjunction with Authentication Classes to form Rest Framework’s API Access Control System: Authentication determines who the user is and Permission determines whether the user has the permission to do what they are trying to do.

Code block featuring an example of Authentication Classes and Permission Classes
Fig: Example of Authentication Classes and Permission Classes in an APIView class.

Authentication Classes

The only thing we need to know about Authentication Classes is that there are four main types of Authentication defined in Django RF:

  1. BasicAuthentication
  2. TokenAuthentication
  3. SessionAuthentication
  4. RemoteUserAuthentication

And that for the purposes of this discussion, we are going to only consider Token Authentication, as it most closely follows the tenets of REST; specifically the tenet of being stateless. It transmits the user identity between various modules via an encoded token like JWT which can be decoded by the appropriate modules to determine the user's identity. In Django, the token is set in the request header under the key “Authorisation” and is in the format

token <token_string>

This is the default format of the token; although custom formats can be implemented as well, that is beyond the scope of this discussion.

Permission Classes

Permission classes, as briefly stated earlier, are used to determine if the requesting user is authorised to execute the task they are trying to execute.

The most common permission classes in Django RF that a developer may come across are:

  1. IsAuthenticated: This class mainly checks if the user has been authenticated or not and only allows access if they are.
  2. IsAdminUser: This class checks if the user is an admin or superuser and only allows access if they are.

But how is a permission class constructed? Let us use the example of theIsAdminUser class and see for ourselves; we can simply navigate to the source code of Rest Framework and check. We will have to go to wherever we have installed RF and then navigate to

permissions.py -> IsAdminUser class (line #140)

Here, we see that the class is defined as

class IsAdminUser(BasePermission):
def has_permission(self, request, view):
return bool(request.user and request.user.is_staff)

Here, we can see that it is just a python class extending the BasePermission class defined above (line #101). Inside, it simply contains a function named has_permission that takes the class object, the request and the view/API as the arguments and returns a boolean value for the following logical statement(s):

request.user and request.user.is_staff

So that means that all it takes for someone to create a custom permission class is to create a new class extending the BasePermission class, define a function named has_permission, pass those three arguments and return the logical statement.

Let us take a moment to understand what this means, using an example.

Use Case

Suppose we have implemented a custom user model by extending AbstractUser found in django.contrib.auth.models and added a new field to it called user_tier which can have the values tier_1, tier_2, tier_3 and tier_4. It would look something like this:

class User(AbstractUser):
"""
Custom user model.
"""
TIER_CHOICE = (
('tier_1', 'Tier 1'),
('tier_2', 'Tier 2'),
('tier_3', 'Tier 3'),
('tier_4', 'Tier 4'),
)
user_tier = models.CharField(
max_length=10,
choices=TIER_CHOICE,
default='tier_4',
)

Also, suppose we have an API called XYZView, and we want access to it restricted only to users who are Tier 2 and above. Not even superusers should be able to access it if they are not a high enough tier (think premium features in a freemium mobile game).

The API would look something like this:

class XYZView(APIView):
def get(self, request):
res = do_something()
return Response(resp, status=200)

As it stands now, any user can access this API, even unregistered users; we simply have no mechanism to determine the identity of the requesting user. Let us rectify this first by adding an authentication_class tag to the API.

class XYZView(APIView):
authentication_classes = (TokenAuthentication,)
def get(self, request):
res = do_something()
return Response(resp, status=200)

As you can see, we have added an authentication_classes tag with the value set as a tuple whose only value is TokenAuthentication; as discussed earlier we will only consider token-based authentication for this discussion.
As for why it is enclosed in a tuple instead of the more common enclosure of a list is that this is just good practice; lists are mutable during program execution, tuples are not. They cannot be modified during execution, even accidentally, by a poorly-worded statement.

Now we have a mechanism to determine the identity of the requesting user, allowing us to set permissions or to put it simply: deny the request if the user does not meet certain criteria, which is in this case that they must be tier_1 or tier_2.

Now, of course, we can simply add an if statement to the beginning of the API such that:

if request.user.user_tier != ‘tier_1’ and request.user.user_tier != ‘tier_2’
return Response(
{
“message”: “access unauthorised”
},
status=401
)

That is a perfectly valid implementation. But what if you want to enforce this condition on multiple APIs? You would have to write/copy-paste this code multiple times i.e, for each enforcement. Django follows the principle of D.R.Y. which means that writing the same, un-encapsulated piece of code over and over again would go against the design pattern, hampering future maintainability.

This is where our custom permission class comes in. Let us build one.

Building a Custom Permission Class

First, create a new python package named auth . We will use it to store all of our custom auth code and such.

Inside the auth module, create a new file named custom_permissions.py; this is where we will define our new permission class.

Your folder tree should look something like this…

project/
├── auth/
│ ├── __init__.py
│ └── custom_permissions.py
├── core/
│ ├── __init__.py
│ └── settings.py
├── manage.py

Now inside custom_permissions, add the following line to the import block:
from rest_framework.permissions import BasePermission . This will import the BasePermission class that we discussed earlier, that we need to set as the parent for our new permission class.

Now define the new permission class in the file as follows:

class IsTier2OrAbove(BasePermission):
'''
Allows access only to users who are Tier 2 or above.
'''
def has_permission(self, request, view):
tier = request.user.user_tier
is_true = ((tier == "tier_2") or (tier == "tier_1"))
return bool(is_true and request.user.is_authenticated)

There, we have constructed our own custom permission class.
It first retrieves the tier of the requesting user, then checks if it is Tier 2 or above and then returns that check value as a boolean. You might notice we have also added request.user.is_authenticated in the end; this is just good practice in my own, personal opinion and you should be able to completely omit it in your own implementation. Now, all we have to do is implement it in our API.

Go to the file where you have defined the API and in the import block at the top, add the following line:
from auth.custom_permissions import IsTier2OrAbove
This will import our new permission class. You might remember that auth was the name of our Python package we created to store the custom authorisation code and custom_permissions was the name of the python file inside that package where we had defined our custom permission class. IsTier2OrAbove was the name of our custom permission class.

Now, all we have to do is add the new permission to the API using the permission_classes tag.

from auth.custom_permissions import IsTier2OrAboveclass XYZView(APIView):
authentication_classes = (TokenAuthentication,)
permission_classes = (IsTier2OrAbove,)
def get(self, request):
res = do_something()
return Response(resp, status=200)

There, we have implemented our new permission class to the API which will ensure that only users Tier 2 and above can access this API.

And this is not the only use case. Suppose you want an API to only be accessible to a user account which has been activated for at least 30 days, or a user who has at least x amount of engagement or activities, or maybe you want only users who have specified access to the API in the database to have access to it. All you have to do in each case is to write a new permission class for each and add it to the API in the tag.

Another more common use case would be to use user groups; suppose you only want users belonging to a specific user group to be able to access an API: you simply add a user to the group and in the permission class, you simply check if the requesting user is part of that particular group.

Summary

In this article, we discussed how user authentication and user permissions are ideally handled. We read the source code to figure out how permission classes are constructed. We then used the knowledge gained to construct our own custom permission class that allowed access to an API based on the value of the user_tier field for a particular user object.

This is basically how custom permissions can be constructed and implemented inside Django Rest Framework.

One thing to note is that this was just a basic overview of the subject based on personal preferences and if you want to read further, we would suggest reading the source code (found here) or the official documentation (found here).

With this, I bid you goodbye for now and do let it be known if this article was of any help to you.

--

--

Prithoo Medhi

Perennial idiot, occasional freelancer and sporadic poster!