Django REST Framework Permissions in Depth

Posted by Harald Nezbeda on Mon 11 November 2019

There are two concepts that need to be separated when talking about access to a REST API build with Django REST Framework. First there are the authentication classes, which are responsible for replacing the anonymouse user on the current request with a real user object based on the registered authentication backends.

The permission classes will be used once the authentication has passed. Each permission class has to pass in order to allow the user to access a specific endpoint. If a permission class fails it can result in a 403 HTTP response if a permission is missing or a 401 if the user is not set on the requets (user is not authenticated).

By default permission classes and the authentication classes can be provided in the REST_FRAMEWORK settings:

# Rest framework
REST_FRAMEWORK = {
    ...
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
        'core.rest.permissions.BaseModelPermissions'
    ],
    'DEFAULT_AUTHENTICATION_CLASSES': [
          'rest_framework.authentication.SessionAuthentication',
        ],
    ...
}

Alternatively you can define them inside of a BaseView and inherit them to the child views. This is also what needs to be configured in order to replace authentication or permission classes on a certain view.

from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import IsAuthenticated

from core.rest.permissions import BaseModelPermissions


class BaseModelViewSet(viewsets.ModelViewSet):

    permission_classes = [
        IsAuthenticated,
        BaseModelPermissions,
    ]

    authentication_classes = [
        SessionAuthentication,
    ]

    def get_queryset(self):
        return self.queryset.viewable()

There are several built-in permission classes in Django Rest Framework an can be used without complex configuration:

  • AllowAny
  • IsAuthenticated
  • IsAdminUser
  • IsAuthenticatedOrReadOnly
  • DjangoModelPermissions
  • DjangoModelPermissionsOrAnonReadOnly
  • DjangoObjectPermissions – requires django-guardian or another object-level permission backend.

Custom Object Permissions

You can create a custom implementation of a permission class by extending the BasePermission class and implementing the has_permission and the has_object_permission methods.

For SnyPy and most of my projects I need to restrict access to objects based on the assigned permissions. Also there are use cases where the user has the permission to view an object but he can edit or delete it only if certain conditions apply.

The core.rest.permissions.BaseModelPermissions used above is an extension of the DjangoModelPermissions. I used this approach to add object level permissions to the SnyPy REST API:

class BaseModelPermissions(DjangoModelPermissions):
    perms_map = {
        'GET': ['%(app_label)s.view_%(model_name)s'],
        'OPTIONS': [],
        'HEAD': [],
        'POST': ['%(app_label)s.add_%(model_name)s'],
        'PUT': ['%(app_label)s.change_%(model_name)s'],
        'PATCH': ['%(app_label)s.change_%(model_name)s'],
        'DELETE': ['%(app_label)s.delete_%(model_name)s'],
    }

    def has_object_permission(self, request, view, obj):
        has_permission = super().has_permission(request, view)

        if has_permission and view.action == 'retrieve':
            return self._queryset(view).viewable().filter(pk=obj.pk).exists()

        if has_permission and view.action == 'update':
            return self._queryset(view).editable().filter(pk=obj.pk).exists()

        if has_permission and view.action == 'partial_update':
            return self._queryset(view).editable().filter(pk=obj.pk).exists()

        if has_permission and view.action == 'destroy':
            return self._queryset(view).deletable().filter(pk=obj.pk).exists()

        return False

The main advantage in this approach is that the users permissions will be verified in order to access a certain endpoint as this in covered by the DjangoModelPermissions. The has_object_permission is extended to use model querysets. Each model of the application is defining a custom manager with a custom queryset that extends the folowing BaseQuerySet:

from django.db import models


class BaseQuerySet(models.QuerySet):

    def viewable(self):
        return self.all()

    def editable(self):
        return self.viewable()

    def deletable(self):
        return self.editable()

And by using this we can granular define what is viewable, editable or deletable

class SnippetLabelQuerySet(BaseQuerySet):

    def viewable(self):
        from snippets.models import Snippet

        return self.filter(
            snippet__in=Snippet.objects.viewable().values_list('pk', flat=True)
        )

    def editable(self):
        from snippets.models import Snippet

        return self.filter(
            snippet__in=Snippet.objects.editable().values_list('pk', flat=True)
        )

This approach was sufficient for use case of SnyPy. You may also check out the third party package list in the DRF docs if a more complex use case needs to be covered.