0

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?

CurlyError
  • 305
  • 2
  • 15

2 Answers2

0

Validate in your serializer instead of view. You can validate a particular field in validate_<field> or as a whole in validate function

class YourModleSerializer(serializers.ModelSerializer):
    def validate_field(self, value):
        return value
        
    def validate(self, attrs):
        validated_data = super().validate(attrs)
        return validated_data
     
Ashin Shakya
  • 690
  • 6
  • 9
0

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()
CurlyError
  • 305
  • 2
  • 15