Search code examples
pythondjangodjango-rest-frameworkpermissions

Custom permissions in rests framework


I need permissions for my app in DRF.

Input:

  • noauthenticated users can only read publication and images

  • authenticated users can create publications and add images

  • authorized users can edit and delete publications and images for which they are authors

  • admin user can do all actions but edit content

Models:

class Publication(models.Model):
    pub_text = models.TextField(null=True, blank=True)
    pub_date = models.DateTimeField(auto_now_add=True)
    pub_author = models.ForeignKey(User, on_delete=models.CASCADE)

class Image(models.Model):
    image = models.ImageField(upload_to='images', null=True)
    image_to_pub = models.ForeignKey(Publication, on_delete=models.CASCADE, null=True, related_name='images')

Views:

class PublicationViewSet(viewsets.ModelViewSet):
    queryset = Publication.objects.all()
    serializer_class = PublicationSerializer
    permission_classes = [PublicPermission]


class ImageViewSet(viewsets.ModelViewSet):
    queryset = Image.objects.all()
    serializer_class = ImageSerializer
    permission_classes = [ImagePermission]

Permissions:

class ImagePermission(BasePermission):

    edit_methods = ['PUT', 'PATCH']

    def has_permission(self, request, view):
        if request.method in SAFE_METHODS:
            return True
        if request.user.is_authenticated:
            return True

    def has_object_permission(self, request, view, obj):

        if request.user.is_superuser:
            return True

        if request.method in SAFE_METHODS:
            return True

        if request.user.id == Publication.objects.get(id=obj.image_to_pub_id).pub_author_id:
            return True

        if request.user.is_staff and request.method not in self.edit_methods:
            return True

        return False


class ImageAuthorPermission(BasePermission):
    def has_object_permission(self, request, view, obj):
        if request.user.id == Publication.objects.get(id=obj.image_to_pub_id).pub_author_id:
            return True
        return False

Now it works as i described above. But i'm mot sure if this is good practice.

I mean class <ImagePermission>. There are two times check if method in SAFE_METHODS.

If i delete that check out from <has_permission>, unauthenticated users do not have ReadOnly rights.

If i delet that check out from <has_object_permission>, authenticated users do not have Edit and Delete rights.

I'm sure there is beter way to customise this permissions. Isn't there?

Also i tryed to check if current user has object permission to images which related to publication for which user was author. This works but is there standard practice how to check permissions to related objects? I tryed to delete check

if request.user.id == Publication.objects.get(id=obj.image_to_pub_id).pub_author_id:

out of <ImagePermission> and combine both <ImagePermission> and <ImageAuthorPermission> classes in a <permission_classes> list. Used & and | operators, but did not get success.


Solution

  • I saw this part and implemented it.

    1. noauthenticated users can only read publication and images
    2. authenticated users can create publications and add images
    3. authorized users can edit and delete publications and images for which they are authors
    4. admin user can do all actions but edit content

    First of all, the SAFE_METHOD request must be read only, and the action such as create can only be made to authenticated users.

    For this part, use the IsAuthenticatedOrReadOnly class that DRF Permission provides as default.

    rest_framework.permission

    class IsAuthenticatedOrReadOnly(BasePermission):
        """
        The request is authenticated as a user, or is a read-only request.
        """
    
        def has_permission(self, request, view):
            return bool(
                request.method in SAFE_METHODS or
                request.user and
                request.user.is_authenticated
            )
    

    To implement No. 3 and 4, only the owner can modify and delete it, and the administrator can only modify it("PUT", "PATCH").

    from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission
    
    
    class AdminObjectPermission(BasePermission):
      
      def has_object_permission(self, request, view, obj):
        if bool(request.user and request.user.is_staff) and request.method in ["PUT", "PATCH"]:
          return True
    
    
    class PublicPermission(IsAuthenticatedOrReadOnly):
      
      def has_object_permission(self, request, view, obj):
        
        if request.user.id == obj.pub_author.id:
          return True
        
        return False
    
    
    class ImagePermission(IsAuthenticatedOrReadOnly, AdminObjectPermission):
      
      def has_object_permission(self, request, view, obj):
    
        if request.user.id == obj.image_to_pub.pub_author.id:
          return True
        
        return False
    

    For the Publication object, it is associated with the User with the pub_author field.

    if request.user.id == obj.pub_author.id

    Image objects were compared through pub_author on connected Publication objects.

    if request.user.id == obj.image_to_pub.pub_author.id

    views.py

    class PublicationViewSet(viewsets.ModelViewSet):
        queryset = Publication.objects.all()
        serializer_class = PublicationSerializer
        authentication_classes = [YourAuthenticationClass]
        permission_classes = [PublicPermission|AdminObjectPermission]
    
    
    class ImageViewSet(viewsets.ModelViewSet):
        queryset = Image.objects.all()
        serializer_class = ImageSerializer
        authentication_classes = [YourAuthenticationClass]
        permission_classes = [ImagePermission|AdminObjectPermission]
    

    additionally

    The permission check for the retrieve method failed.

    I modified the source code to allow for this part.

    from rest_framework.permissions import IsAuthenticatedOrReadOnly, BasePermission
    
    
    class AdminObjectPermission(BasePermission):
      
      def has_object_permission(self, request, view, obj):
    
        if request.method in SAFE_METHODS:
          return True
    
        if bool(request.user and request.user.is_staff) and request.method in ["PUT", "PATCH"]:
          return True
    
    
    class PublicPermission(IsAuthenticatedOrReadOnly):
      
      def has_object_permission(self, request, view, obj):
        
        if request.user.id == obj.pub_author.id:
          return True
        
        return False
    
    
    class ImagePermission(IsAuthenticatedOrReadOnly, AdminObjectPermission):
      
      def has_object_permission(self, request, view, obj):
    
        if request.user.id == obj.image_to_pub.pub_author.id:
          return True
        
        return False