Django REST Framework viewset per-action permissions Django REST Framework viewset per-action permissions python python

Django REST Framework viewset per-action permissions


In DRF documentation,

Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already passed

Let's assume following permission about user object

  • List : staff only
  • Create : anyone
  • Retrieve : own self or staff
  • Update, Partial update : own self or staff
  • Destroy : staff only

permissons.py

from rest_framework import permissionsclass UserPermission(permissions.BasePermission):    def has_permission(self, request, view):        if view.action == 'list':            return request.user.is_authenticated() and request.user.is_admin        elif view.action == 'create':            return True        elif view.action in ['retrieve', 'update', 'partial_update', 'destroy']:            return True        else:            return False                                                                                                    def has_object_permission(self, request, view, obj):        # Deny actions on objects if the user is not authenticated        if not request.user.is_authenticated():            return False        if view.action == 'retrieve':            return obj == request.user or request.user.is_admin        elif view.action in ['update', 'partial_update']:            return obj == request.user or request.user.is_admin        elif view.action == 'destroy':            return request.user.is_admin        else:            return False

views.py

from .models import Userfrom .permissions import UserPermissionfrom .serializers import UserSerializerfrom rest_framework import viewsetsclass UserViewSet(viewsets.ModelViewSet):    queryset = User.objects.all()    serializer_class = UserSerializer    permission_classes = (UserPermission,)

For Django 2.0 replace is_authenticated() with is_authenticated. The method has been turned into an attribute.


You can create a custom permission class extending DRF's BasePermission.

You implement has_permission where you have access to the request and view objects. You can check request.user for the appropriate role and return True/False as appropriate.

Have a look at the provided IsAuthenticatedOrReadOnly class (and others) for a good example of how easy it is.

I hope that helps.


I personally hate this kind of Frankenstein's monster custom permissions, in my opinion, it's not very idiomatic when it comes to the Django framework.

So I came up with the following solution - it's very similar to how @list_route and @detail_route decorators work.We are relying on the fact that the methods/functions are first-class objects

First of all, I'm creating such decorator:

decorators.py

def route_action_arguments(**kwargs):    """    Add arguments to the action method    """    def decorator(func):        func.route_action_kwargs = kwargs        return func    return decorator

As you can see it adds a dictionary to the function it decorates with parameters passed as arg list

Now I created such mixin:mixins.py

class RouteActionArgumentsMixin (object):    """    Use action specific parameters to     provide:    - serializer    - permissions    """    def _get_kwargs(self):        action = getattr(self, 'action')        if not action:            raise AttributeError        print('getting route kwargs for action:' + action)        action_method = getattr(self, action)        kwargs = getattr(action_method, 'route_action_kwargs')        print(dir(kwargs))        return kwargs    def get_serializer_class(self):        try:            kwargs = self._get_kwargs()            return kwargs['serializer']        except (KeyError, AttributeError):            return super(RouteActionArgumentsMixin, self).get_serializer_class()    def get_permissions(self):        try:            kwargs = self._get_kwargs()            return kwargs['permission_classes']        except (KeyError, AttributeError):            return super(RouteActionArgumentsMixin, self).get_permissions()

Mixin does two things;when get_permissions is called, it checks which 'action' is executed, and looksup the permission_classes collection from the route_action_kwargs associated with the viewset.action_method.route_action_kwargs

when get_serializer_class is called, it does the same and picks the serializer from route_action_kwargs

Now the way we can use it:

@method_decorator(route_action_arguments(serializer=LoginSerializer), name='create')class UserViewSet (RouteActionArgumentsMixin, RequestContextMixin, viewsets.ModelViewSet):    """    User and profile managment viewset    """    queryset = User.objects.all()    serializer_class = UserSerializer    @list_route(methods=['post'])    @route_action_arguments(permission_classes=(AllowAny,), serializer=LoginSerializer)    def login(self, request):        serializer = self.get_serializer_class()(data=request.data)

For custom routs we define explicitly we can just set the @route_action_arguments explicitly on the method.

In terms of the generic viewsets and methods, we can still add them using the@method_decorator

@method_decorator(route_action_arguments(serializer=LoginSerializer), name='create')class UserViewSet (RouteActionArgumentsMixin, RequestContextMixin, viewsets.ModelViewSet):