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 anonymous 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 requests (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 following 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.