pythondjangodjango-rest-frameworkpermissionsdjango-permissions

Handle Django Rest Framework permission when creating related objects


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?


Solution

  • 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()