Как написать собственный класс разрешений для проверки разрешений на уровне объекта перед разрешениями на уровне API? - PullRequest
1 голос
/ 03 февраля 2020

TL; DR

Как написать собственный класс разрешений для проверки разрешений на уровне объектов (has_object_permission) перед разрешениями на уровне API (has_permission)?

Определения

Предположим, мы создаем инструмент для онлайн-аннотаций. У нас есть Изображения, принадлежащие Проектам и Пользователям, работающим над комментариями к изображениям. У пользователей есть поле role - Гости имеют доступ только для чтения, Разработчики могут изменять поля Изображение, а Владельцы могут изменять как Проекты, так и Изображения.

Модели:

class Image(models.Model):
    data = JSONField()
    project = models.ForeignKey(Project)

class Project(models.Model):
    pass

class User(AbstractUser):
    ROLE_GUEST, ROLE_DEVELOPER, ROLE_OWNER = range(3)
    ROLE_CHOICES = [(ROLE_GUEST, "Guest"), (ROLE_DEVELOPER, "Developer"), (ROLE_OWNER, "Owner")]
    role = models.IntegerField(default=ROLE_GUEST, choices=ROLE_CHOICES)

Предположим, что мы будем sh ограничивать доступ к некоторым проектам и внутри них. Мы добавляем m2m модель UserProjectRole:

class UserProjectRole(models.Model):
    user = models.ForeignKey(User)
    project = models.ForeignKey(Project)
    project_role = ... # same as in User

Теперь мы будем sh настраивать разрешения таким образом, чтобы, если пользователь запрашивает доступ к конечным точкам API для какого-либо проекта:

  • если нет UserProjectRole с этим пользователем и этим проектом, разрешения по умолчанию равны его глобальному role
  • , если существует такое UserProjectRole, разрешения используют его project_role вместо role

Проблема

Мы реализуем пользовательский класс разрешений:

# permissions.py
from rest_framework import permissions

class AtLeastDeveloper(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user.is_staff or request.user.role >= User.ROLE_DEVELOPER

class AtLeastOwner(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user.is_staff or request.user.role >= User.ROLE_OWNER

class InProjectPermissions(permissions.BasePermission):
    def has_object_permission(self, request, view, obj):
         if isinstance(obj, Project):
             project = obj
         elif isinstance(obj, Image):
             project = obj.project
         else:
             raise ValueError(obj)
         upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
         if upr is None:
             return False
         if isinstance(obj, Project):
             return upr.role == User.ROLE_OWNER
         return upr.role >= User.ROLE_DEVELOPER
# views.py

class ProjectGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
     permission_classes = [IsAuthenticated, AtLeastOwner, InProjectPermissions]
     ...

class ImageGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
     permission_classes = [IsAuthenticated, AtLeastDeveloper, InProjectPermissions]
     ...

Проблема в том, что has_permission вызывается до has_object_permissions (как указано в https://www.django-rest-framework.org/api-guide/permissions/#custom - разрешения ). Поэтому, если глобального пользователя role недостаточно, ему будет отказано в доступе (IsAtLeastSomething.has_permissions вернет значение False и, следовательно, InProjectPermissions.has_object_permissions не будет выполнено).

Пожалуйста, сообщите, как написать пользовательский класс полномочий для проверки разрешений на уровне объекта перед разрешениями на уровне API.

Ответы [ 2 ]

1 голос
/ 03 февраля 2020

TLDR; - Попробуйте переопределить check_object_permissions(request, obj)

Согласно документации DRF:

Перед запуском основной части представления каждого разрешения в списке проверено. Если какая-либо проверка разрешений не проходит, исключение.PermissionDenied или exceptions.NotAuthenticated будет сгенерировано, и основная часть представления не будет запущена.

Это означает, что все разрешения, которые мы упомянули в permission_classes ДОЛЖЕН пройти. Если кто-либо из них также потерпит неудачу, он не будет аутентифицироваться.

  1. Так что для наших пользовательских нужд мы можем переопределить check_object_permissions(request, obj) и написать нашу собственную аутентификацию.
0 голосов
/ 04 февраля 2020

Итак, вот что я придумал.

Я мог , как предполагает @Hafnernuss, получить доступ к рассматриваемому объекту с помощью view.get_object(id) (взяв id из request.GET / POST), но это именно то, чего я хотел избежать: ручная работа с внутренними структурами.

@ umair-mohammad предложила переопределить view.check_object_permissions, но, опять же, я хотел избегайте, потому что это означало, что все мои представления должны быть унаследованы от одного базового класса. В большинстве случаев это нормально, и спасибо за ваш ответ - это действительно решает проблему - но я действительно хотел разделить эту логику c на permissions.py, не касаясь представлений.

Итак, давайте вместо этого переопределяем базовый класс разрешений:

class AbstractPermission(permissions.BasePermission):
    def has_permission(self, request, view):
        return True

    def has_permission_by_global_role(self, request, view):
        raise NotImplementedError()

    def has_object_permission(self, request, view, obj):
        if isinstance(obj, Project):
             project = obj
         elif isinstance(obj, Image):
             project = obj.project
         else:
             raise ValueError(obj)

         # if user has global role big enough -- permit
         if self.has_permission_by_global_role(request, view):
              return True

         # otherwise -- assess his 'local' role (as before)
         upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
         if upr is None:
             return False
         if isinstance(obj, Project):
             return upr.role == User.ROLE_OWNER
         return upr.role >= User.ROLE_DEVELOPER

Затем мы наследуем наши классы AtLeastSomething, изменяя has_permission на has_permission_by_global_role:

class AtLeastOwner(AbstractPermission):
    def has_permission_by_global_role(self, request, view):
        return request.user.is_staff or request.user.role >= User.ROLE_OWNER

...

Таким образом, в views.py мы только необходимо указать allow_classes на основе разрешенной глобальной роли - в нашем случае это AtLeastOwner для Project и AtLeastDeveloper для изображений.

Я полагаю, что это наиболее pythoni c подход к этому. Далее можно сделать это чище, предоставив каждой модели что-то вроде .allowed_roles() метода.

...