DRF Мне нужно, чтобы разрешения на уровне объекта / экземпляра были предоставлены, но разрешения на уровне представления / модели были отклонены

У меня довольно простой сервер и API с двумя типами пользователей, User и Admin. Я создал группы и разрешения в Django. Администратор должен все обрабатывать, а Пользователь должен только просматривать и редактировать себя.

Я использовал DjangoModelPermissions в DRF, и он работает должным образом для разрешений на уровне представления / модели. Это выглядит следующим образом.

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']

Для разрешений на уровне объекта/экземпляра я добавил пользовательское разрешение, которое содержит только метод has_object_permission() и проверяет, является ли запрашивающий пользователь объектом, к которому осуществляется доступ. Это выглядит следующим образом:

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

Теперь, поскольку в DRF сначала проверяются разрешения на уровне представления/модели, и, если они разрешены, могут быть проверены разрешения на уровне объекта /экземпляра, любому пользователю, скажем, пользователю с id=4, всегда разрешено просматривать все другие пользовательские данные при доступе к списку пользователей, т.е.: api/users/. Это связано с тем, что этот параметр должен быть разрешен, чтобы пользователь мог получить доступ к своим собственным данным (т.е.: api/users/4/).

Мне удалось "решить" это, добавив проверку в list() метод пользователя ModelViewSet, так что, если пользователь попытается получить доступ к api/users/, он просто будет заблокирован, примерно так:

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)

Итак, это работает, но выглядит как-то халтурно. Есть ли какой-нибудь другой, более правильный способ сделать это?

Что вы можете сделать здесь, так это поработать с FilterBackend для фильтрации, тогда это будет выглядеть следующим образом:

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)

и зарегистрируйте это как:

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

    # no override of list necessary

При извлечении будет возвращен список, содержащий только текущего пользователя api/users.

У меня была идея, что фильтрация на самом деле является частью того, что могут делать классы разрешений, но, возможно, это не в фреймворке Django REST, а в другом фреймворке.

Другой вариант - проверить действие в разрешении:

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'

Это предотвратит запуск .list() для пользователей, не являющихся администраторами, и, таким образом, вернет сообщение 403 "Отказано в разрешении".

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