Django rest framework permission_classes of ViewSet method

25,915

Solution 1

I think there is no inbuilt solution for that. But you can achieve this by overriding the get_permissions method:

from rest_framework.permissions import AllowAny, IsAdminUser

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

    permission_classes_by_action = {'create': [AllowAny],
                                    'list': [IsAdminUser]}

    def create(self, request, *args, **kwargs):
        return super(UserViewSet, self).create(request, *args, **kwargs)

    def list(self, request, *args, **kwargs):
        return super(UserViewSet, self).list(request, *args, **kwargs)

    def get_permissions(self):
        try:
            # return permission_classes depending on `action` 
            return [permission() for permission in self.permission_classes_by_action[self.action]]
        except KeyError: 
            # action is not set return default permission_classes
            return [permission() for permission in self.permission_classes]

Solution 2

I created a superclass that is derived from @ilse2005's answer. In all subsequent django views you can inherit this to achieve action level permission control.

class MixedPermissionModelViewSet(viewsets.ModelViewSet):
   '''
   Mixed permission base model allowing for action level
   permission control. Subclasses may define their permissions
   by creating a 'permission_classes_by_action' variable.

   Example:
   permission_classes_by_action = {'list': [AllowAny],
                                   'create': [IsAdminUser]}
   '''

   permission_classes_by_action = {}

   def get_permissions(self):
      try:
        # return permission_classes depending on `action`
        return [permission() for permission in self.permission_classes_by_action[self.action]]
      except KeyError:
        # action is not set return default permission_classes
        return [permission() for permission in self.permission_classes]

Solution 3

I'm probably late to answer this, but I used a mixin, as one of the commenters pointed out. Taking the answer from @Itachi, this is my mixin implementation:

class ViewSetActionPermissionMixin:
    def get_permissions(self):
        """Return the permission classes based on action.

        Look for permission classes in a dict mapping action to
        permission classes array, ie.:

        class MyViewSet(ViewSetActionPermissionMixin, ViewSet):
            ...
            permission_classes = [AllowAny]
            permission_action_classes = {
                'list': [IsAuthenticated]
                'create': [IsAdminUser]
                'my_action': [MyCustomPermission]
            }

            @action(...)
            def my_action:
                ...

        If there is no action in the dict mapping, then the default
        permission_classes is returned. If a custom action has its
        permission_classes defined in the action decorator, then that
        supercedes the value defined in the dict mapping.
        """
        try:
            return [
                permission()
                for permission in self.permission_action_classes[self.action]
            ]
        except KeyError:
            if self.action:
                action_func = getattr(self, self.action, {})
                action_func_kwargs = getattr(action_func, "kwargs", {})
                permission_classes = action_func_kwargs.get(
                    "permission_classes"
                )
            else:
                permission_classes = None

            return [
                permission()
                for permission in (
                    permission_classes or self.permission_classes
                )
            ]

And here's how to use the mixin:

class MyViewSet(ViewSetActionPermissionMixin, ModelViewSet):
    ...
    permission_action_classes = {
        "list": [AllowAny],
        "create": [IsAdminUser],
        "custom_action": [MyCustomPermission],
    }

    @action(...)
    def custom_action(self, request, *args, **kwargs):
        ...

Solution 4

I think all of the other answers are great but we shouldn't suppress the default actions' permission_classes defined in their decorators directly. So,

from rest_framework import viewsets
from rest_framework import permissions

class BaseModelViewSet(viewsets.ModelViewSet):
    queryset = ''
    serializer_class = ''
    permission_classes = (permissions.AllowAny,)

    # Refer to https://stackoverflow.com/a/35987077/1677041
    permission_classes_by_action = {
        'create': permission_classes,
        'list': permission_classes,
        'retrieve': permission_classes,
        'update': permission_classes,
        'destroy': permission_classes,
    }

    def get_permissions(self):
        try:
            return [permission() for permission in self.permission_classes_by_action[self.action]]
        except KeyError:
            if self.action:
                action_func = getattr(self, self.action, {})
                action_func_kwargs = getattr(action_func, 'kwargs', {})
                permission_classes = action_func_kwargs.get('permission_classes')
            else:
                permission_classes = None

            return [permission() for permission in (permission_classes or self.permission_classes)]

Now we could define the permission_classes in these two ways. Since we defined the default global permission_classes_by_action in the superclass, we could drop that definition for all the actions in option 2.

class EntityViewSet(BaseModelViewSet):
    """EntityViewSet"""
    queryset = Entity.objects.all()
    serializer_class = EntitySerializer
    permission_classes_by_action = {
        'create': (permissions.IsAdminUser,),
        'list': (permissions.IsAuthenticatedOrReadOnly,),
        'retrieve': (permissions.AllowAny,),
        'update': (permissions.AllowAny,),
        'destroy': (permissions.IsAdminUser,),
        'search': (permissions.IsAuthenticated,)  # <--- Option 1
    }

    @action(detail=False, methods=['post'], permission_classes=(permissions.IsAuthenticated,))  # <--- Option 2
    def search(self, request, format=None):
        pass
Share:
25,915
fodma1
Author by

fodma1

Updated on May 08, 2020

Comments

  • fodma1
    fodma1 almost 4 years

    I'm writing a rest API with the Django REST framework, and I'd like to protect certain endpoints with permissions. The permission classes look like they provide an elegant way to accomplish this. My problem is that I'd like to use different permission classes for different overridden ViewSet methods.

    class UserViewSet(viewsets.ModelViewSet):
        queryset = User.objects.all()
        serializer_class = UserSerializer
    
        def create(self, request, *args, **kwargs):
            return super(UserViewSet, self).create(request, *args, **kwargs)
    
        @decorators.permission_classes(permissions.IsAdminUser)
        def list(self, request, *args, **kwargs):
            return super(UserViewSet, self).list(request, *args, **kwargs)
    

    In the code above I'd like to allow registration (user creation) for unauthenticated users too, but I don't want to let list users to anyone, just for staff.

    In the docs I saw examples for protecting API views (not ViewSet methods) with the permission_classes decorator, and I saw setting a permission classes for the whole ViewSet. But it seems not working on overridden ViewSet methods. Is there any way to only use them for certain endpoints?

  • Phoenix
    Phoenix over 4 years
    I think It's good idea to put get_permissions into a Mixin.