Django REST Framework viewset per-action permissions
Asked Answered
T

7

61

Is there a best practice to assign a different permission to each action of a given APIView or ViewSet?

Let's suppose I defined some permissions classes such as 'IsAdmin', 'IsRole1', 'IsRole2', ..., and I want to grant different permissions to the single actions (e.g. a user with Role1 can create or retrieve, a user with Role2 can update, and only an Admin can delete).

How can I structure a class based view in order to assign a permission class to the 'create', 'list', 'retrieve', 'update', 'delete' actions? I'm trying to do so to have a class that can be reused for different tables that have the same permission pattern.

Typhoid answered 11/10, 2013 at 8:9 Comment(0)
I
21

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.

Interlocution answered 11/10, 2013 at 8:35 Comment(0)
F
99

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 permissions

class 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 User
from .permissions import UserPermission
from .serializers import UserSerializer
from rest_framework import viewsets


class 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.

Flexure answered 8/12, 2015 at 18:1 Comment(4)
view.action seems so much more intuitive than request.method. +1Abstractionism
My condition was - List : Anyone Create : Authenticated User Retrieve : Anyone Update, Partial update : own self or superadmin Destroy : own self For the above scenario i got it working by removing the condition for update, partial_update, destroy from has_permission().Carminecarmita
request.user.is_authenticated is a bool not a function. +1Foyer
The answer below https://mcmap.net/q/321914/-django-rest-framework-viewset-per-action-permissions is simple and correctRosner
I
21

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.

Interlocution answered 11/10, 2013 at 8:35 Comment(0)
G
14

From the DRF documentation, you can now do this instead :

The decorator allows you to override any viewset-level configuration such as permission_classes, serializer_class, filter_backends...:

@action(detail=True, methods=['post'], permission_classes=[IsAdminOrIsSelf])
def set_password(self, request, pk=None):
Gelb answered 30/5, 2022 at 16:27 Comment(0)
G
9

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):
Gerigerianna answered 21/12, 2016 at 22:57 Comment(1)
> action decorator replaces list_route and detail_routeSylvia
J
4

Django has a permissions class called DjangoObjectPermissions which uses Django Guardian as an authentication backend.

When you have Django guardian active in your settings you just add permission_classes = [DjandoObjectPermissions] to your view and it does permission authentication automatically, so you can 'CRUD' based on the permission set to a particular django.contrib.auth group or user.

See a gist with an example.

You can set Django Guardian as your authentication backed

Joyous answered 15/10, 2013 at 21:22 Comment(0)
C
3

There's an example of how this can be done in the DRF documentation on viewsets:

def get_permissions(self):
    """
    Instantiates and returns the list of permissions that this view requires.
    """
    if self.action == 'list':
        permission_classes = [IsAuthenticated]
    else:
        permission_classes = [IsAdminUser]
    return [permission() for permission in permission_classes]

It is not really clear from the docs that this is the best way to do it - this piece of code is used as an example of how to introspect actions, not how to implement permissions classes per action. It is also not mentioned at all when permissions are discussed.

However I do think this is a bit cleaner than implementing the same logic inside a custom permission. This way you can keep your permissions a bit more granular and descriptive.

Cochleate answered 14/7, 2022 at 12:10 Comment(0)
P
2

RestFramework's class-based views have methods for each HTTP verb (ie : HTTP GET => view.get() etc). You just have to use django.contrib.auth's permissions, users, groups and decorators as documented.

Partridge answered 11/10, 2013 at 8:21 Comment(1)
django.contrib.auth's decorators aren't always that useful when using DRF's generic views. Quite often you don't implement the HTTP methods at all — so there's nothing to decorate. (And implementing them, or dispatch, just to decorate them is no fun.) Better to use DRF's own permissions system in this case. django-rest-framework.org/api-guide/permissions.htmlInterlocution

© 2022 - 2024 — McMap. All rights reserved.