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