Django REST Framework viewset per-action permissions

前端 未结 5 980
忘掉有多难
忘掉有多难 2020-12-13 00:38

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 permis

相关标签:
5条回答
  • 2020-12-13 00:48

    Django has a persmissions 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 http://django-guardian.readthedocs.org/en/latest/installation.html

    0 讨论(0)
  • 2020-12-13 00:49

    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.

    0 讨论(0)
  • 2020-12-13 00:49

    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,)
    

    EDIT

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

    0 讨论(0)
  • 2020-12-13 00:54

    I personally hate this kind of frankenmonster custom permissions, in my opinion, it's not very idiomatic when it comes to 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 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):
    
    0 讨论(0)
  • 2020-12-13 01:05

    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.

    0 讨论(0)
提交回复
热议问题