2

I am currently working on a DRF project where Admin users, Teacher users and Owner users should be able to have access to an objects detail-view. Basically all user types except those who are not the owner of the object or a teacher or an admin user. I am able to implement the separate permissions for each, but when I need to combine these permissions on a view I hit a roadblock because the user level perms are checked before object level perms. Thus I cannot use boolean operands to combine them and I have to write these ugly permission classes for my views.

My question is:

How can I implement these permissions on my detail-view in a cleaner way or, alternatively, is there a cleaner way to obtain my result?

As you will see, I violate DRY because I have an IsAdminOrOwner and IsAdminOrTeacherOrOwner perm. You will also note in my view that I overwrite get_permissions() to have appropriate permission classes for the respective request methods. Any comments on this and other implementations are welcome, I want critique so that I can improve upon it.

Here follows permissions.py:

from rest_framework import permissions
from rest_framework.permissions import IsAdminUser

class IsOwner(permissions.BasePermission):

    def has_permission(self, request, view):
        return True

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

class IsTeacher(permissions.BasePermission):

    def has_permission(self, request, view):
        return request.user.groups.filter(
            name='teacher_group').exists()

class IsAdminOrOwner(permissions.BasePermission):

    def has_object_permission(self, *args):
        is_owner = IsOwner().has_object_permission(*args)

        #convert tuple to list
        new_args = list(args)
        #remove object for non-object permission args
        new_args.pop()
        is_admin = IsAdminUser().has_permission(*new_args)

        return is_owner or is_admin

class IsAdminOrTeacherOrOwner(permissions.BasePermission):

    def has_object_permission(self, *args):
        is_owner = IsOwner().has_object_permission(*args)

        #convert tuple to list
        new_args = list(args)
        #remove object for non-object permission args
        new_args.pop()
        is_admin = IsAdminUser().has_permission(*new_args)
        is_teacher = IsTeacher().has_permission(*new_args)

        return is_admin or is_teacher or is_owner

And here follows my view:

class UserRetrieveUpdateView(APIView):
    serializer_class = UserSerializer

    def get_permissions(self):
        #the = is essential, because with each view
        #it resets the permission classes
        #if we did not implement it, the permissions
        #would have added up incorrectly
        self.permission_classes = [IsAuthenticated]

        if self.request.method == 'GET':
            self.permission_classes.append(
                IsAdminOrTeacherOrOwner)

        elif self.request.method == 'PUT':
            self.permission_classes.append(IsOwner)

        return super().get_permissions()

    def get_object(self, pk):
        try:
            user = User.objects.get(pk=pk)
            self.check_object_permissions(self.request, user)
            return user
        except User.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        user = self.get_object(pk)
        serializer = self.serializer_class(user)
        return Response(serializer.data, status.HTTP_200_OK)

    def put(self, request, pk, format=None):
        user = self.get_object(pk)
        #we use partial to update only certain values
        serializer = self.serializer_class(user,
            data=request.data, partial=True)

        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data,
                status.HTTP_200_OK)

        return Response(status=status.HTTP_400_BAD_REQUEST)
DJN
  • 51
  • 9
  • Tip to shoten code: since you're just using `get` and `put` operation why don't you use `RetrieveUpdateAPIView` instead of `APIView`. This way you won't need `get_object`, you don't have to override `get`, `put` method. It's all already there in `RetrieveUpdateAPIView` – xxbinxx Apr 20 '19 at 06:50
  • Thank you for the tip. I'll certainly look into it. In regards to the permissions, are you aware of a better way? –  DJN Apr 20 '19 at 08:55

1 Answers1

3

Permissions in DRF can be combined using the bitwise OR operator. For example, you could do this:

permission_classes = (IsAdmin | IsOwner | IsTeacher)

In this way, you don't have to define separate classes to combine them.

Ken4scholars
  • 6,076
  • 2
  • 21
  • 38
  • Hey, first, thanks for the response! I am however, aware of the fact that one can use bitwise operators, but since IsOwner is an object level permission and the rest of the permissions are user level, the bitwise does not work with them, my tests fail :( –  DJN Apr 20 '19 at 13:12
  • @DJN I don't see how that makes a difference. `(IsAdminUser | IsOwner)` should achieve the same thing that your `IsAdminOrOwner` does. – Ken4scholars Apr 20 '19 at 13:59
  • No, It does not, because the IsAdminUser is tested before the IsOwner permission as the IsOwner perm is object level and the IsAdminUser is not. –  DJN Apr 20 '19 at 19:13
  • But that's exactly what your `IsAdminOrOwner` does. Doesn't really matter which one is tested first since it's or. In the end, you did `return is_owner or is_admin` so if `is_owner` is false while `is_admin` is true then it will still pass. In any case, I don't really see any difference between those two – Ken4scholars Apr 20 '19 at 19:43
  • @Ken4scholars I am late for the party, but there is a bug in DRF that is subtle and grounds for security concerns. There are many issues similar to this: https://github.com/encode/django-rest-framework/issues/7117 – Yamuk Aug 03 '22 at 08:55