Reputation: 3535
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?
Upvotes: 34
Views: 35374
Reputation: 10399
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):
...
Upvotes: 11
Reputation: 6070
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
Upvotes: 10
Reputation: 11429
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]
Upvotes: 63
Reputation: 459
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]
Upvotes: 14