Django REST Framework - Separate permissions per methods

35,390

Solution 1

Permissions are applied to the entire View class, but you can take into account aspects of the request (like the method such as GET or POST) in your authorization decision.

See the built-in IsAuthenticatedOrReadOnly as an example:

SAFE_METHODS = ['GET', 'HEAD', 'OPTIONS']

class IsAuthenticatedOrReadOnly(BasePermission):
    """
    The request is authenticated as a user, or is a read-only request.
    """

    def has_permission(self, request, view):
        if (request.method in SAFE_METHODS or
            request.user and
            request.user.is_authenticated()):
            return True
        return False

Solution 2

I've come across the same problem when using CBV's, as i have fairly complex permissions logic depending on the request method.

The solution i came up with was to use the third party 'rest_condition' app listed at the bottom of this page

http://www.django-rest-framework.org/api-guide/permissions

https://github.com/caxap/rest_condition

I just split the permissions flow logic so that each branch will run, depending on the request method.

from rest_condition import And, Or, Not

class MyClassBasedView(APIView):

    permission_classes = [Or(And(IsReadOnlyRequest, IsAllowedRetrieveThis, IsAllowedRetrieveThat),
                             And(IsPostRequest, IsAllowedToCreateThis, ...),
                             And(IsPutPatchRequest, ...),
                             And(IsDeleteRequest, ...)]

So the 'Or' determines which branch of the permissions should run depending on the request method and the 'And' wraps the permissions relating to the accepted request method, so all must pass for permission to be granted. You can also mix 'Or', 'And' and 'Not' within each flow to create even more complex permissions.

The permission classes to run each branch simply look like this,

class IsReadyOnlyRequest(permissions.BasePermission):

    def has_permission(self, request, view):
        return request.method in permissions.SAFE_METHODS


class IsPostRequest(permissions.BasePermission):

    def has_permission(self, request, view):
        return request.method == "POST"


... #You get the idea

Solution 3

Update 30 March 2020: My original solution only patched object permissions, not request permissions. I've included an update below to make this work with request permissions as well.

I know this is an old question but I recently ran into the same problem and wanted to share my solution (since the accepted answer wasn't quite what I needed). @GDorn's answer put me on the right track, but it only works with ViewSets because of the self.action

I've solved it creating my own decorator:

def method_permission_classes(classes):
    def decorator(func):
        def decorated_func(self, *args, **kwargs):
            self.permission_classes = classes
            # this call is needed for request permissions
            self.check_permissions(self.request)
            return func(self, *args, **kwargs)
        return decorated_func
    return decorator

Instead of setting the permission_classes property on the function, like the built-in decorator does, my decorator wraps the call and sets the permission classes on the view instance that is being called. This way, the normal get_permissions() doesn't need any changes, since that simply relies on self.permission_classes.

To work with request permissions, we do need to call check_permission() from the decorator, because the it's orginally called in initial() so before the permission_classes property is patched.

Note The permissions set through the decorator are the only ones called for object permissions, but for request permissions they are in addition to the class wide permissions, because those are always checked before the request method is even called. If you want to specify all permissions per method only, set permission_classes = [] on the class.

Example use case:

from rest_framework import views, permissions

class MyView(views.APIView):
    permission_classes = (permissions.IsAuthenticatedOrReadOnly,)  # used for default APIView endpoints
    queryset = MyModel.objects.all()
    serializer_class = MySerializer


    @method_permission_classes((permissions.IsOwnerOfObject,))  # in addition to IsAuthenticatedOrReadOnly
    def delete(self, request, id):
        instance = self.get_object()  # ...

Hope this helps someone running into the same problem!

Solution 4

I ran into this problem and really wanted to use the @permission_classes decorator to mark some custom view methods with specific permissions. I ended up coming up with a mixin:

class PermissionsPerMethodMixin(object):
    def get_permissions(self):
        """
        Allows overriding default permissions with @permission_classes
        """
        view = getattr(self, self.action)
        if hasattr(view, 'permission_classes'):
            return [permission_class() for permission_class in view.permission_classes]
        return super().get_permissions()

An example use case:

from rest_framework.decorators import action, permission_classes  # other imports elided

class MyViewset(PermissionsPerMethodMixin, viewsets.ModelViewSet):
    permission_classes = (IsAuthenticatedOrReadOnly,)  # used for default ViewSet endpoints
    queryset = MyModel.objects.all()
    serializer_class = MySerializer

    @action(detail=False, methods=['get'])
    @permission_classes((IsAuthenticated,))  # overrides IsAuthenticatedOrReadOnly
    def search(self, request):
        return do_search(request)  # ...

Solution 5

This question is about APIView instances, but for anyone landing here looking for a per-method permissions override using the @action decorator within ViewSets:


class SandwichViewSet(ModelViewSet):
  permission_classes = [IsAuthenticated]

  @action(..., permission_classes=[CanSeeIngredients])
  def retrieve__ingredients(self, request):
    ...
Share:
35,390
José L. Patiño
Author by

José L. Patiño

Updated on April 01, 2021

Comments

  • José L. Patiño
    José L. Patiño about 3 years

    I am writing an API using Django REST Framework and I am wondering if can specify permissions per method when using class based views.

    Reading the documentation I see that is quite easy to do if you are writing function based views, just using the @permission_classes decorator over the function of the views you want to protect with permissions. However, I don't see a way to do the same when using CBVs with the APIView class, because then I specify the permissions for the full class with the permission_classes attribute, but that will be applied then to all class methods (get, post, put...).

    So, is it possible to have the API views written with CBVs and also specify different permissions for each method of a view class?

  • José L. Patiño
    José L. Patiño over 10 years
    Sorry for the delay. Thanks, Kevin. Your answer was perfect. There is the IsAuthenticatedOrReadOnly permission class, which can use SAFE_METHODS.
  • Oleg Belousov
    Oleg Belousov over 10 years
    Very nice, will also work for a POST - only API, say for creation of leads by third - party affiliates, but to prevent the listing of the entire lead list?
  • Pithikos
    Pithikos over 4 years
    Feel free to also try this: github.com/Pithikos/rest-framework-roles
  • Viktor Vostrikov
    Viktor Vostrikov about 3 years
    Does this work? I could not make it work, it simply ignores. I debugged, and it does override permissions classes, but my endpoint is still can be reached by another user. Should I somehow call super, or something else? @Adam
  • Viktor Vostrikov
    Viktor Vostrikov about 3 years
    Ok, It does work. You need to call self.get_object() though, to trigger permission.
  • Joshua Swain
    Joshua Swain almost 3 years
    DRF has added functionality similar to this: django-rest-framework.org/api-guide/permissions/… "Provided they inherit from rest_framework.permissions.BasePermission, permissions can be composed using standard Python bitwise operators ... Note: t supports & (and), | (or) and ~ (not)." So you can write permissions like permission_classes = [IsAlienFromSpace & IsFriendly, IsAuthenticated | ReadOnly]
  • Séraphin
    Séraphin almost 3 years
    In Django 3+ (don't know about previous versions), request.user.is_authenticated() is actually request.user.is_authenticated. is_authenticated is not (any more?) a method but a bool