Permission checks in DRF viewsets are not working right
Asked Answered
W

3

12

I am implementing an API where I have nested structures.

Lets say it is a zoo and I can call GET /api/cage/ to get a list of cages GET /api/cage/1/ to get cage ID 1, but then I can GET /api/cage/1/animals/ to get a list of animals in that cage.

The problem I am having is with permissions. I should only be able to see animals in the cage if I can see the cage itself. I should be able to see the cage itself if has_object_permission() returns True in the relevant permission class.

For some reason, has_object_permission() gets called when I do GET /api/cage/1/, but has_permission() gets called when I call GET /api/cage/1/animals/. And with has_permission() I don't have access to the object to check the permissions. Am I missing something? How do I do this?

My cage viewset looks more or less like this

class CageViewSet(ModelViewSet):
    queryset = Cage.objects.all()
    serializer_class = CageSerializer
    permission_classes = [GeneralZooPermissions, ]
    authentication_classes = [ZooTicketCheck, ]

    def get_queryset(self):
        ... code to only list cages you have permission to see ...

    @detail_route(methods=['GET'])
    def animals(self, request, pk=None):
        return Request(AnimalSerializer(Animal.objects.filter(cage_id=pk), many=True).data)

My GeneralZooPermissions class looks like this (at the moment)

class GeneralZooPermissions(BasePermission):
    def has_permission(self, request, view):
        return True

    def has_object_permission(self, request, view, obj):
        return request.user.has_perm('view_cage', obj)

It seems like this is a bug in DRF. Detailed routes do not call the correct permission check. I have tried reporting this issue to DRF devs, but my report seems to have disappeared. Not sure what to do next. Ideas?

The issue I posted with DRF is back and I got a response. Seems like checking only has_permission() and not has_object_permission() is the intended behavior. This doesn't help me. At this point, something like this would have to be done:

class CustomPermission(BasePermission):
    def has_permission(self, request, view):
        """we need to do all permission checking here, since has_object_permission() is not guaranteed to be called"""
        if 'pk' in view.kwargs and view.kwargs['pk']:
            obj = view.get_queryset()[0]
            # check object permissions here
        else:
            # check model permissions here

    def has_object_permission(self, request, view, obj):
        """ nothing to do here, we already checked everything """
        return True
Willett answered 11/4, 2016 at 15:39 Comment(0)
W
26

OK, so after reading a bunch of DRF's code and posting an issue at the DRF GitHub page.

It seems that has_object_permission() only gets called if your view calls get_object() to retrieve the object to be operated on.

It makes some sense since you would need to retrieve the object to check permissions anyway and if they did it transparently it would add an extra database query.

The person who responded to my report said they need to update the docs to reflect this. So, the idea is that if you want to write a custom detail route and have it check permissions properly you need to do

class MyViewSet(ModelViewSet):
    queryset = MyModel.objects.all()
    ....
    permission_classes = (MyCustomPermissions, )
    
        @detail_route(methods=['GET', ])
        def custom(self, request, pk=None):
            my_obj = self.get_object() # do this and your permissions shall be checked
            return Response('whatever')
Willett answered 23/5, 2016 at 17:29 Comment(1)
You are a saviour. Thank you. Worth noting that they updated the docs: django-rest-framework.org/api-guide/permissions/…Dysphagia
P
0

In my case I didn't address requests correctly so my URL was api/account/users and my mistake was that I set URL in frontend to api/account/ and thats not correct!

Paranoia answered 16/7, 2022 at 9:5 Comment(0)
O
-1

If you want to define permissions while doing another method that doesn't call the get_object() (e.g. a POST method), you can do overriding the has_permission method. Maybe this answer can help (https://mcmap.net/q/1007450/-django-drf-permissions-on-create-related-objects)

Another thing you can do is use the check_object_permissions inside your POST method, that way you can call your has_object_permission method:

@action(detail=True, methods=["POST"])
def cool_post(self, request, pk=None, *args, **kwargs):
    your_obj = self.get_object()
    self.check_object_permissions(request, your_obj)
Oxblood answered 19/12, 2021 at 16:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.