Django REST Framework viewset per-action permissions
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.
Solution 1:
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.
Solution 2:
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.
Solution 3:
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):