3

I want users to have access only to the records that belong to them, not to any other users' records so I've created the following view:

class AddressViewSet(viewsets.ModelViewSet):

    authentication_classes = (TokenAuthentication,)
    permission_classes = [IsAuthenticated, IsOwner]
    queryset = Address.objects.all()

    def retrieve(self, request, pk):
        address = self.address_service.get_by_id(pk)
        serializer = AddressSerializer(address)
        return Response(serializer.data, status=status.HTTP_200_OK)

I want only the owner of the records to have access to all the methods in this view ie retrieve, list, etc (I'll implement the remaining methods later) so I created the following permissions.py file in my core app:

class IsOwner(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        print('here in has_object_permission...')
        return obj.user == request.user

this wasn't working, so after going through stackoverflow answers I found this one Django Rest Framework owner permissions where it indicates that has_permission method must be implemented. But as you can see in that answer, it's trying to get the id from the view.kwargs but my view.kwargs contains only the pk and not the user. How can I fix this? Do I need to implicitly pass the user id in the request url? that doesn't sound right.

Here's the test I'm using to verify a user cannot access other user's records:

def test_when_a_user_tries_to_access_another_users_address_then_an_error_is_returned(self):
        user2 = UserFactory.create()
        addresses = AddressFactory.create_batch(3, user=user2)
        address_ids = [address.id for address in addresses]
        random_address_id = random.choice(address_ids)

        url = reverse(self.ADDRESSES_DETAIL_URL, args=(random_address_id,))

        res = self.client.get(url, format='json')

        print(res.data)

Currently just using the test to check the data returned, will implement the assertions later on.

Edit

So I added has_permission method to IsOwner:

def has_permission(self, request, view):
        return request.user and request.user.is_authenticated

if I put a print statement here it gets printed, but doesn't seem to be hitting the has_object_permission method, none of the prints I added there are being displayed

MrCujo
  • 1,218
  • 3
  • 31
  • 56

3 Answers3

3

This answer was the right one for me.

It says:

The has_object_permission is not called for list views. The documentation says the following:

Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the filtering documentation for more details.

Link to documentation

Séraphin
  • 710
  • 7
  • 18
0

Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already passed.

You need to write the has_permission too in order to make your custom permission works.

Here is the official docs and mentioned it. It should works after you add in has_permission.

San Tack
  • 74
  • 4
  • in my edit you can see that I added it, yet `has_object_permission` is never invoked. By adding `print` statements in both methods I can see that `has_permission` is invoked but `has_object_permission` is not – MrCujo Mar 22 '21 at 07:21
  • show me your full permissions code if can – San Tack Mar 22 '21 at 07:54
0

As mentioned in the docs, permissions are checked on self.get_object method call.

def get_object(self):
    obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"])
    self.check_object_permissions(self.request, obj)
    return obj

Which basically is all retrieve method does in ModelViewSet

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)

Whatever it is you do in self.address_service.get_by_id(pk) should either be moved to self.get_object or call self.check_object_permissions(self.request, obj) in retrieve method.

In the basic scenario this is all you need. There's no need to overwrite retrieve method.

class AddressViewSet(viewsets.ModelViewSet):
    serializer_class = AddressSerializer
    authentication_classes = (TokenAuthentication,)
    permission_classes = [IsAuthenticated, IsOwner]
    queryset = Address.objects.all()
Tom Wojcik
  • 5,471
  • 4
  • 32
  • 44