DRF I need object/instance-level premissions granted but view/model-level permissions denied

I have a rather simple backend and API with two user types, User and Admin. I have created groups and permissions in Django. The Admin should CRUD everything and the User should only view and edit itself.

I've used DjangoModelPermissions in DRF and it works as expected for view/model-level permissions. This looks like this.

class MyAppModelPermissions(permissions.DjangoModelPermissions):
    def __init__(self):
        self.perms_map = copy.deepcopy(self.perms_map)
        self.perms_map['GET'] = ['%(app_label)s.view_%(model_name)s']

For object/instance-level permissions, I added a Custom Permission that just has the has_object_permission() method and checks if the requesting user is the object being accessed. It looks like this:

class UserAccessPermission(permissions.BasePermission):

    def has_object_permission(self, request, view, obj):
        if (request.user == obj) or (get_user_role(request.user) in ("admin",)):
            return True
        else:
            return False

Now, since in DRF, view/model-level permissions are checked first, and if allowed, object/instance-level permissions can be checked, it is allways allowed for any user, say, user with id=4, to view all other user's data when accessing the list of users, ie: api/users/. That's because that perimssion needs to be allowed for a user to be able to access its own data (ie: api/users/4/).

I've managed to "resolve" this by adding a check in the list() method of the User ModelViewSet, so that if a user tries to access api/users/ it just gets blocked, like so:

class UserViewset(viewsets.ModelViewSet):

    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [MyAppModelPermissions & UserAccessPermission]

    def list(self, request, *args, **kwargs):
        if not (utils.get_user_role(request.user) == "admin"):
            content = {"detail": "No permissions."}
            return Response(content, status=status.HTTP_403_FORBIDDEN)
        return super().list(request, *args, **kwargs)

So, this works, but seems to be hacky. Is there any other more proper way to do this?

What you can do here is work with a FilterBackend to filter, this then looks like:

class MeOrAdminFilter(BaseFilterBackend):
    def filter_queryset(self, request, queryset, view):
        if get_user_role(request.user) == 'admin':
            return queryset
        else:
            return queryset.filter(pk=request.user.pk)

and register this as:

class UserViewset(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer
    permission_classes = [MyAppModelPermissions & UserAccessPermission]
    filter_backends = [MeOrAdminFilter]

    # no override of list necessary

This will return a list with only the current user when you fetch api/users.

I had the idea that filtering was actually part of of the things permission classes could do, but perhaps this is not in the Django REST framework, but another framework.

Another option is to inspect the action in the permission:

class UserAccessPermission(permissions.BasePermission):
    def has_permission(self, request, view):
        if view.action == 'list':
            return get_user_role(request.user) == 'admin'
        else:
            # retrieve, etc. is allowed for everyone
            return True

    def has_object_permission(self, request, view, obj):
        return request.user == obj or get_user_role(request.user) == 'admin'

This will prevent .list() to run for non-admin users, and thus return a 403 Permission Denied.

Вернуться на верх