Search code examples
djangodjango-rest-frameworkdjango-permissions

Permission denied for ..../<id>/ CRUD ModelViewSet endpoints


I am trying to create a ModelViewSet for supporting CRUD operations on my address model. I am able to successfully use LIST and CREATE endpoints, but all the other 4 endpoints which require/id/ in their URL return me the error -

{ "detail": "You do not have permission to perform this action." }

The address model should support the operations only by the Users who are marked NOT SELLER in the DB, hence the ~IsSeller custom permission is applied.

The issue goes away when I remove this custom permission from permissions classes, but i need to keep this restriction of only non-seller users to be able to add the address.

permissions.py

from rest_framework.permissions import BasePermission

class IsSeller(BasePermission):

    def has_permission(self, request, view):
        return (request.user and request.user.is_seller)

Views.py

class ManageAddressViewSet(viewsets.ModelViewSet):
    serializer_class = AddressSerializer
    permission_classes = [permissions.IsAuthenticated, (~IsSeller)]
    queryset = Address.objects.all()

    def get_queryset(self):
        return self.queryset.filter(user=self.request.user)

    def perform_create(self, serializer):
        return serializer.save(user=self.request.user)

Serializers.py

class AddressSerializer(serializers.ModelSerializer):
    class Meta:
        model = Address
        fields = [
            'id', 'name', 'line_1',
            'line_2', 'city', 'state',
            'pincode', 'type'
        ]
        read_only_fields = ['id']

urls.py

from django.urls import path, include
from rest_framework.routers import DefaultRouter

from user import views

router = DefaultRouter()
router.register('address', views.ManageAddressViewSet)

urlpatterns = [
  path('create/', views.CreateUserView.as_view(), name='create'),
  path('authenticate/', views.CreateTokenView.as_view(), name='authenticate'),
  path('my-profile/', views.ManageUserView.as_view(), name='my-profile'),
  path('address/', include(router.urls)),

Models.py

from django.db import models

from django.contrib.auth.models import (
    AbstractBaseUser,
    PermissionsMixin,
    BaseUserManager
)

from django.conf import settings

ADDRESS_CHOICES = (
    ('WORK', 'Work',),
    ('HOME', 'Home',),
    ('Other', 'Other',),
)


class UserManager(BaseUserManager):

    def create_user(self, email, password=None, **other_fields):
        if not email:
            raise (ValueError("Email not provided"))
        user = self.model(
            email=self.normalize_email(email),
            **other_fields
        )
        user.set_password(password)
        user.save(using=self._db)
        return user

    def create_superuser(self, email, password):
        user = self.create_user(email, password)
        user.is_staff = True
        user.is_superuser = True
        user.save(using=self._db)
        return user


class User(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField(max_length=255, unique=True)
    name = models.CharField(max_length=255)
    number = models.CharField(max_length=15, unique=True)
    is_active = models.BooleanField(default=True)
    is_staff = models.BooleanField(default=False)
    is_seller = models.BooleanField(default=False)

    USERNAME_FIELD = 'email'

    objects = UserManager()

class Address(models.Model):
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE
    )
    name = models.CharField(max_length=255)
    line_1 = models.CharField(max_length=255)
    line_2 = models.CharField(max_length=255, blank=True)
    city = models.CharField(max_length=255)
    state = models.CharField(max_length=255)
    pincode = models.CharField(max_length=6)
    type = models.CharField(
        max_length=20,
        choices=ADDRESS_CHOICES,
        default='HOME'
    )

    def __str__(self):
        return self.name

Endpoints

GET(working) ​/api​/user​/address​/address​/

POST(working) ​/api​/user​/address​/address​/

GET(permission error) ​/api​/user​/address​/address​/{id}​/

PUT(permission error) ​/api​/user​/address​/address​/{id}​/

PATCH(permission error) ​/api​/user​/address​/address​/{id}​/

DELETE(permission error) ​/api​/user​/address​/address​/{id}​/


Solution

  • It's most likely to do with the combined facts that you have not implemented the has_object_permission method, and are also using the ~ negation.

    See here for a discussion on the DRF repo on this very issue: https://github.com/encode/django-rest-framework/issues/6598

    I suggest you implement the extra method, most likely just returning the same result as the has_permission method, i.e.

        def has_object_permission(self, request, view, obj):
            return self.has_permission(request, view)