Permission Classes in Django-Rest-Framework

Permission Classes in Django-Rest-Framework

Imagine you're throwing a party, and you're the host.

You have different types of guests coming in, each with their own level of access to your party.

Now you establish certain rules and conditions that grant permission to some specific people:

  • Guest List: Not everyone is allowed. You have a list of names that can join your party.

  • Bouncer: You hire a bouncer, who makes sure to enforce the rules and conditions you have mentioned.

  • Access Levels: Maybe you have certain rooms specially allocated for VIP people. So only people who are labelled as "VIP" in the guest list can enter the VIP rooms and not the people who are not.
    And your bouncer will make sure of that.

Here the Bouncer is like the Permission Class in Django Rest Framework.

The bouncer checks if where the visitor is in the guest list or not (True or False) to join the party.

To access the VIP rooms, the bouncer also checks if the guy belongs to the VIP class.

This checking by the bouncer is similarly done through methods of Permission classes called has_permission() and has_object_permission(). This methods return boolean value. If it's True, then the user is granted access, else denied.

Think of permission classes like a set of keys. Some keys unlock every door in the building, while others only open specific rooms (like the VIP room or the kitchen). If you don't have the right key, you're not getting in.

When working with Django Rest Framework and Authentication, we always need certain permission classes or decorators that grants or denies permission to a user accessing a view function or class.


Built-in permission classes

IsAuthenticated

class IsAuthenticated(BasePermission):
    """
    Allows access only to authenticated users.
    """

    def has_permission(self, request, view):
        return bool(request.user and request.user.is_authenticated)

Breakdown of the code:

  • The class IsAuthenticated inherits the class BasePermission. Whenever you try to create a new custom permission class, you need to inherit this class as it contains the methods has_permission() and has_object_permission() for granting or denying permissions.

  • has_permission() : Return boolean value as said before. It has parameters: request and view

    • request contains information about the user which is an instance of the built-in User model of Django or maybe an instance of the Auth User model you have created

    • view has these properties:

      • kwargs: A dictionary of all keyword arguments passed to the view. This includes any arguments captured from the URL, such as the ID of the object being retrieved or updated.

      • request: The Django HttpRequest object for the current request. This includes information about the HTTP method, headers, user, and more.

    • This method checks if the request.user contains the User instance and if the user is authenticated. If both is True, then the user is granted permission.


What happens when permission check fails?

How permission classes are written for:

Class Based Views

class ProductView(APIView):
    authentication_classes = [TokenAuthentication]
    permission_classes = [isAuthenticated]

    def get(self, request, id):
        # code . . .

Function Based Views

@apiView(['GET'])
@authentication_classes([TokenAuthentication])
@permission_classes([isAuthenticated])
def get_product(self, request, id):
    # code . . .

Permissions in REST framework are always defined as a list of permission classes.

Before running the main body of the view each permission in the list is checked. If any permission check fails, an exceptions.PermissionDenied or exceptions.NotAuthenticated exception will be raised, and the main body of the view will not run.

Source

Response when permission is denied.

When the permission checks fail, either a "403 Forbidden" or a "401 Unauthorized" response will be returned, according to the following rules:

  • When the request was successfully authenticated, but permission was denied.
    Look into this code that demonstrate this rule:

      from rest_framework.permissions import BasePermission, IsAuthenticated
      from rest_framework.views import APIView
      from rest_framework.response import Response
      from rest_framework import status
      from rest_framework.authentication import TokenAuthentication
    
      class CustomPermission(BasePermission):
          def has_permission(self, request, view):
              return False  # deny access
    
      class TestView(APIView):
          authentication_classes = [TokenAuthentication]
          permission_classes = [IsAuthenticated, CustomPermission]
    
          def get(self, request):
              return Response({"message": "This is a protected resource"}, status=status.HTTP_200_OK)
    

    An HTTP 403 Forbidden response will be returned.

    IsAuthenticated returns True but since the has_permission() of CustomPermission returns False, access is denied.

  • The request was not successfully authenticated, and the highest priority authentication class does not use WWW-Authenticate headers.

    Look into this code that demonstrate this rule:

      from rest_framework.permissions import BasePermission
      from rest_framework.views import APIView
      from rest_framework.response import Response
      from rest_framework import status
    
      class CustomPermission(BasePermission):
          def has_permission(self, request, view):
              return False  # deny access
    
      class TestView(APIView):
          authentication_classes = [] # no authentication classes
          permission_classes = [CustomPermission]
    
          def get(self, request):
              return Response({"message": "This is a protected resource"}, status=status.HTTP_200_OK)
    

    An HTTP 403 Forbidden response will be returned.

    💡
    Authentication class uses WWW-Authenticate headers that are set in response to challenge the client for not providing authentication credentials.

    Since we have not provided any authentication class here, the response status will be 403 instead of 401 as the Permission class check fails.

  • The request was not successfully authenticated, and the highest priority authentication class does use WWW-Authenticate headers.

    Here let's assume that the user has not provided authentication credentials.

    The following code do have the authentication class included.

      from rest_framework.permissions import BasePermission
      from rest_framework.views import APIView
      from rest_framework.response import Response
      from rest_framework import status
      from rest_framework.authentication import TokenAuthentication
    
      class CustomPermission(BasePermission):
          def has_permission(self, request, view):
              return True # allow access
    
      class TestView(APIView):
          authentication_classes = [TokenAuthentication]
          permission_classes = [CustomPermission]
    
          def get(self, request):
              return Response({"message": "This is a protected resource"}, status=status.HTTP_200_OK)
    

    — An HTTP 401 Unauthorized response, with an appropriate WWW-Authenticate header will be returned.


Writing your own Permission Class

Let's say there is a django app that has two types of users

  • Student

  • Teacher

Now obviously there will be different APIs and view logic for different student and teachers.

For example, teacher can grade students on subjects, but the opposite does not work.

Below is a Class Based View that allows teacher to grade students:

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from rest_framework.authentication import TokenAuthentication
from rest_framework import status
from .permissions import isTeacher
from .models import Score

class GradeStudentView(APIView):
    authentication_classes = [TokenAuthentication]
    permission_classes = [IsAuthenticated, isTeacher]

    def post(self, request):

        # Assuming the request contains data for the student's grade
        # Example JSON data: {"student_id": 123, "grade": "A"}
        student_id = request.data.get('student_id')
        grade = request.data.get('grade')

        # Model that stores grades and student id
        score = Score(grade=grade, student=student_id)        
        score.save()

        return Response({"message": f"Grade {grade} successfully assigned to student with ID {student_id}"},
                        status=status.HTTP_201_CREATED)

In the above code the list of permission classes checks whether the Class Based View is being accessed by user who is authenticated and also a Teacher.

It contains a custom permission class called isTeacher.

The following code contains the isTeacher Class that inherits BasePermission class as it includes method like has_permission() as discussed before:

from rest_framework.permissions import BasePermission
from .models import Teacher

class isTeacher(BasePermission):

    message = "Only Teacher can grade his/her students."

    def has_permission(self, request, view):

        try:
            teacher = Teacher.objects.get(user=request.user)
            return True # This user is registered as Teacher, so access is allowed

        except Teacher.DoesNotExist:
            return False # This user is not a teacher so access is denied

Now what's happening here?

  • If you try to access the view as a student, has_permission() method of isTeacher will check if there exists an instance of Teacher

  • It checks by searching for an instance by matching the user field of Teacher model with the request.user (which contains the User instance).

  • For better understanding, let's assume the Teacher model looks like this:

      from django.db import models
      from django.contrib.auth.models import User
      import uuid
    
      class Teacher(models.Model):
    
          id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
          user = models.OneToOneField(
              User, related_name='teacher', on_delete=models.CASCADE)
    
          # more fields...
    
  • In the above code, user = models.OneToOneField(...) defines a one-to-one relationship between two models in Django -> User and Teacher.

  • And as you know the request.user is an instance of User model and user field of any teacher instance is also an instance of User model, so we are trying to find the teacher instance with user=request.user.

  • If such instance exists, then the user is a Teacher, else a Student.

  • And if it's a student, an exception is raised: exceptions.PermissionDenied which results in a response with the HTTP status code "403 Forbidden" along with the message set in the custom permission class above : "Only Teacher can grade his/her students."

Understanding the use of permission classes can help you build secure and robust application. Hope you guys enjoyed reading it!

The End... :)

Comment about the next concept or topic you would like to know.

➕ Follow for more such content.

👍 Like if you found this article useful :)

💬 Feedback would be appreciated.

🔗 Share if possible :D

Peace ✌️

Follow me for more such content :)

Did you find this article valuable?

Support Mayukh Bhowmick by becoming a sponsor. Any amount is appreciated!