The has_object_permission
method of a Permission on DRF obviously does not get executed on Create, since the object does not exist yet. However, there are use cases where the permission depends on a related object. For example:
class Daddy(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
class Kiddy:
title = models.CharField(max_length=12)
daddy = models.ForeignKey(Daddy, on_delete=models.CASCADE)
If we only want to allow the owner of Daddy
to create Kiddy
of that Daddy
, we would have to validate that somewhere.
I know this a really common discussion, also mentioned on this question and in many many more. It is also discussed on DRF GitHub itself where a docs update is done for that purpose, and referring to DRF docs it solves the problem here with the following sentence:
... In order to restrict object creation you need to implement the permission check either in your Serializer class or override the perform_create() method of your ViewSet class.
So, referring to DRF docs, we could do one of the following solutions:
class KiddySerializer(viewsets.ModelViewSet):
validate_daddy(self, daddy):
if not daddy.owner == self.context['request'].user:
raise ValidationError("You cannot create Kiddies of that Daddy")
return daddy
or
class KiddyViewSet(ModelViewSet):
def perform_create(self, serializer):
if (self.request.user != serializer.validated_data['daddy'].owner)
raise ValidationError("You cannot create Kiddies of that Daddy")
serializer.save()
Now, there is a problem which also brings up my question. What if I care about the information that is being shared to the user on an unauthorized request. So, in the cases where the Daddy
does not exist, the user will get:
Invalid pk \"11\" - object does not exist
and in the cases that the object exists but the user does not have access, it will return You cannot create Kiddies of that Daddy
I want to show the same message in both cases:
The Daddy does not exist or you don't have permission to use it.
It can be possible if I use a PermissionClass like below:
class OwnsDaddy(BasePermission):
def has_permission(self, request, view):
if not Daddy.objects.allowed_daddies(request.user).filter(pk=request.data['daddy']).exists():
return False
this will also work, but since the permissions are validated before serializer, if the ID of daddy passed by the user is incorrect (let's say string), a 500 error will be caused. We can prevent that by a try-except clause, but still it doesn't feel right.
So, at the end. What would be a good approach to this problem?
Creating custom field relation was the right approach for my case.
from apps.models import Daddy
from rest_framework.serializers import PrimaryKeyRelatedField
class DaddyRelatedField(PrimaryKeyRelatedField):
default_error_messages = {
"required": _("This field is required."),
"does_not_exist": _("The Daddy does not exist or you don't have permission to use it."),
"incorrect_type": _("Incorrect type. Expected pk value, received {data_type}."),
}
def get_queryset(self):
if self.read_only:
return None
queryset = Daddy.objects.all()
return queryset.allowed_daddies(self.context["request"].user)
and then on serializer
class KiddySerializer(ModelSerializer):
daddy = DaddyRelatedField()