Custom permission into generic views
Introduction
I was writing a test code for testing DeleteAccountView
that is built with DestoryAPIView
.
To be specific, I was testing if another user tried to delete account of owner’s account, it will throw 403 Forbidden error.
In order to do this, I created custom permission called IsOwnerOrAdmin
and connected to my view.
Relevant codes
permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODSclass IsOwnerOrAdmin(BasePermission):message = "Forbidden"def has_permission(self, request, view) -> bool:return request.user.is_authenticateddef has_object_permission(self, request, view, obj):return obj.id == request.user.id or request.user.is_admin
views.py
from rest_framework import genericsclass DeleteAccountView(generics.DestroyAPIView):"""Delete user account- Only owner and admin can delete user's account"""permission_classes = [IsOwnerOrAdmin]serializer_class = PublicUserSerializerdef get_object(self, id=None):user_to_delete = User.objects.get(id=self.kwargs.get("id"))return user_to_delete
tests/views.py
# Omitteddef test_permission(self):"""Test permission"""# Fail case: anonymous user# Omitted# Fail case: another userself.auth.set_client_credentials(self.client, self.another_user)wrong_response_two = self.client.delete(self.get_delete_account_api_uri(self.user.id))self.assertEqual(wrong_response_two.status_code, 403)self.assertEqual(wrong_response_two.data["detail"].code, "permission_denied")
Problem
Test code was actually passing 204 status code, which is not correct.
Analysis
DetailAccountView
was not properly checking object-level permissions.
According to Abdul Aziz Barkat’s response from Stackoverflow and note from DRF’s documentation about custom permission,
when using generic views with object-level permissions, I should have ensured that get_object()
triggers the object-level checks.
However, overriding get_object()
method caused bypassing DRF’s permission checking mechanism, which means check_object_permissions
method was not called.
Solution
I followed what DRF suggested on its Object level permissions wrote down.
I switched from generic views to viewset as I didn’t need to worry about manually triggering object-level permission because ViewSet
class already handles this properly out of the box.
Also I used lookup_field
and lookup_url_kwarg
to tell DRF how to find object and removed get_object()
method.
views.py
class UserViewSet(viewsets.ModelViewSet):queryset = User.objects.all()serializer_class = PublicUserSerializerlookup_field = "id"def get_permissions(self):if self.action in ["list", "retrieve", "create"]:permission_classes = [AllowAny]else:permission_classes = [IsOwnerOrAdmin]return [permission() for permission in permission_classes]