1

How do you add permissions to a model so that any user can add a new instance, but only a logged in user can add a particular attribute?

Django models.py:

class Ingredient(models.Model):
    name = models.CharField(max_length=100, unique=True)
    recipes = models.ManyToManyField(Recipe, related_name='ingredients', blank=True)

DRF views.py:

class IngredientViewSet(viewsets.ModelViewSet):
    queryset = Ingredient.objects.all()
    serializer_class = IngredientSerializer
    permission_classes = (IsUserForRecipeOrBasicAddReadOnly,)

DRF permissions.py:

class IsUserForRecipeOrBasicAddReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow logged in users to add an ingredient AND associate its recipe(s).
    """
    message = 'You must be logged in to add an Ingredient to a Recipe.'

    # using this method so I can access the model obj itself
    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        if request.method in permissions.SAFE_METHODS:
            return True

        # Check if we are creating, and if the recipes are included, and if they are not a user.  If so, return False
        if request.method == 'POST' and obj.recipes.all().count() > 0 and request.user.is_anonymous:
            return False
        else:
            return True

I see the appropriate calls/prints to the custom permission class, but I can still make a POST request with a list of recipe id's and it does not error with the message.

Notes -

  • I am getting two POST requests, both with the same information/print statements, and then a third to the GET (once it is added, it shows the newly created instance - which is correct behavior, but I don't know why two POSTs are going through)
chris Frisina
  • 19,086
  • 22
  • 87
  • 167

2 Answers2

3

I think a better approach would be to use 2 different serializer(one of them hase Recipes as a writable field and the other does not), then override get_serializer_class:

class yourMOdelviewset():
    ...
    ...
    def get_serializer_class(self):
        if self.action == 'create':
            if self.request.user.is_authenticated:
                return SerializerThatHasRecipesAsAWriteableField
            else:
                return SerializerThatHasNot
        return super().get_serializer_class()

p.s. Drf uses object level permission for retrieving or updating (basically there should be an object already), since in create there is no object yet, drf never checks the object level permission.

Ehsan Nouri
  • 1,990
  • 11
  • 17
2

The solution proposed by @changak is a good one. Including this as a more direct solution to the question posed. In DRF, has_object_permission is explicitly for an object already in the database, but you can use has_permission. From the docs, this excerpt explains why you don't see has_object_permission being called:

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

In has_permission, you still have access to the data, and can add a check. Assuming your IngredientSerializer has a recipes field, you can check with something like this:

class IsUserForRecipeOrBasicAddReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow logged in users to add an ingredient AND associate its recipe(s).
    """
    message = 'You must be logged in to add an Ingredient to a Recipe.'

    def has_permission(self, request, view):
        if view.action != 'create':
            # Handle everything but create at the object level.
            return True

        if not request.data.get('recipes'):
            return True

        return request.user and request.user.is_authenticated()
RishiG
  • 2,790
  • 1
  • 14
  • 27
  • any reason you can guess that the custom message isn't populating? I am getting `{"detail": "Authentication credentials were not provided."}` This appears that the has_permission message isn't getting overwritten. Thanks for your help in advance, as I am honestly copying/pasting, I'm reading the docs and code as well, trying to understand typical approaches. – chris Frisina Aug 30 '18 at 03:28
  • 1
    Sorry, I was only answering about the permissions class. I don't know anything about your routing, authentication, or test set up, so I didn't respond to the note about getting two "POST" requests... debugging the request/response is probably a different issue – RishiG Aug 30 '18 at 04:35
  • what kind of auth are you using? – RishiG Aug 30 '18 at 04:35
  • ` 'django.contrib.auth',` This is just a test case application, as I'm trying to learn. So any setup is just the basic situation from the tutorial, just slightly different schema to see what DRF is good/best at. – chris Frisina Aug 30 '18 at 18:00