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.