Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django Rest Framework Permissions and Ownership

I have two simple models

class User(AbstractUser): 
    pass


class Vacation(Model):
    id    = models.AutoField(primary_key=True)
    owner = models.ForeignKey(User, on_delete=models.CASCADE)

I am not really sure what is the scalable way of doing user permissions for Django Rest Framework. In particular:

  • Users should only be able to see their own vacations
  • On the /vacation endpoint, user would see a filtered list
  • On the /vacation/$id endpoint, user would get a 403 if not owner
  • Users should only be able to Create/Update vacations as long as they are the owners of that object (through Foreign Key)

What is the best way to achieve this in a future-proof fashion. Say if further down the line:

  • I add a different user type, which can view all vacations, but can only create/update/delete their own
  • I add another model, where users can read, but cannot write

Thank you!

like image 751
Andrei Cioara Avatar asked Feb 19 '19 17:02

Andrei Cioara


People also ask

What is permission in Django REST framework?

Permissions are used to grant or deny access for different classes of users to different parts of the API. The simplest style of permission would be to allow access to any authenticated user, and deny access to any unauthenticated user. This corresponds to the IsAuthenticated class in REST framework.

How do I give permission in Django?

With Django, you can create groups to class users and assign permissions to each group so when creating users, you can just assign the user to a group and, in turn, the user has all the permissions from that group. To create a group, you need the Group model from django. contrib. auth.

Does Django have view permissions?

The Django admin site uses permissions as follows: Access to view objects is limited to users with the “view” or “change” permission for that type of object. Access to view the “add” form and add an object is limited to users with the “add” permission for that type of object.


2 Answers

From the docs:

Permissions in REST framework are always defined as a list of permission classes. Before running the main body of the view each permission in the list is checked. If any permission check fails an exceptions.PermissionDenied or exceptions.NotAuthenticated exception will be raised, and the main body of the view will not run.

REST framework permissions also support object-level permissioning. Object level permissions are used to determine if a user should be allowed to act on a particular object, which will typically be a model instance.

For your current need you can define your own Permission class:

class IsVacationOwner(permissions.BasePermission):
    # for view permission
    def has_permission(self, request, view):
        return request.user and request.user.is_authenticated

    # for object level permissions
    def has_object_permission(self, request, view, vacation_obj):
        return vacation_obj.owner.id == request.user.id

And add this permission to your view. For example on a viewset:

class VacationViewSet(viewsets.ModelViewSet):
    permission_classes = (IsVacationOwner,)

One thing is important to notice here, since you will respond with a filtered list for '/vacations', make sure you filter them using the request.user. Because object level permission will not be applicable for lists.

For performance reasons the generic views will not automatically apply object level permissions to each instance in a queryset when returning a list of objects.

For your future requirement, you can always set the permissions conditionally with the help of get_permissions method.

class VacationViewSet(viewsets.ModelViewSet):
    def get_permissions(self):
        if self.action == 'list':
            # vacations can be seen by anyone
            # remember to remove the filter for list though
            permission_classes = [IsAuthenticated] 

            # or maybe that special type of user you mentioned
            # write a `IsSpecialUser` permission class first btw
            permission_classes = [IsSpecialUser] 
        else:
            permission_classes = [IsVacationOwner]

        return [permission() for permission in permission_classes]

DRF has great documentation. I hope this helps you to get started and helps you to approach different use cases according to your future needs.

like image 115
mehamasum Avatar answered Oct 19 '22 20:10

mehamasum


I would suggest you to use drf-viewsets link. We are going to use vacation viewset to do this work.

our urls.py

from your_app.views import VacationViewSet
router.register('api/vacations/', VacationViewSet)

our serializers.py

from rest_framework import serializers
from your_app.models import Vacation

class VacationSerializer(serializers.ModelSerializer):

    class Meta:
        model = Vacation
        fields = ('id', 'owner',)
        read_only_fields = ('id',)

our views.py

Here we are going to overwrite viewset's retrive and list method. There are other possible way to do that but i like this most as i can able to see what is happening in code. Django model viewset inherited link of drf-mixins retrive and list method.

from rest_framework import viewsets, permissions, exceptions, status
from your_app.models import Vacation, User
from your_app.serializers import VacationSerializer 


class VacationViewSet(viewsets.ModelViewSet):
    queryset = Vacation.objects.all()
    permission_classes = [IsAuthenticated]
    serializer = VacationSerializer

    # we are going to overwrite list and retrive
    def list(self, request, *args, **kwargs):
        queryset = self.filter_queryset(self.get_queryset())
        # now we are going to filter on user 
        queryset = queryset.filter(owner=self.request.user) 
        page = self.paginate_queryset(queryset)
        if page is not None:
            serializer = self.get_serializer(page, many=True)
            return self.get_paginated_response(serializer.data)

        serializer = self.get_serializer(queryset, many=True)
        return Response(serializer.data)

    def retrieve(self, request, *args, **kwargs):
        instance = self.get_object()
        # not permitted check
        if instance.owner is not self.request.user:
             raise exceptions.PermissionDenied()
        serializer = self.get_serializer(instance)
        return Response(serializer.data)
like image 1
Shakil Avatar answered Oct 19 '22 21:10

Shakil