Права в Django и django-rest-framework

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Я Python Developer в компании Нетрика. В данной статье расскажу, как устроены права в Django и django-rest-framework и как мы используем их на одном из проектов.

  1. Django

В Django есть модели User, которая представляет пользователей вашей системы, и Group, которая представляет наборы пользователей. Также Django поставляется со встроенной системой прав. Она предоставляет возможность назначать права пользователям и группам пользователей.

Например, она используется в админке. Django админка использует разрешения следующим образом:

  • Доступ к просмотру объектов определённого типа есть у тех пользователей, у которых есть право на просмотр и изменение.

  • Доступ к добавлению объектов определённого типа есть у тех пользователей, у которых есть право на добавление.

  • Доступ к просмотру истории изменений и изменению объектов определённого типа есть у пользователей, у которых есть право на изменение.

  • Доступ к удалению объектов определённого типа есть у пользователей, у которых есть право на удаление.

К слову, права можно устанавливать не только по типу объекта, но так же и по конкретным объектам. Для этого можно переопределить методы has_view_permission, has_add_permission, has_change_permission и has_delete_permission класса ModelAdmin.

В моделе User есть many-to-many поля groups и user_permissions.

Когда django.contrib.auth представлен в настройке INSTALLED_APPS, то при вызове ./manage.py migrate создаются права для всех зарегистрированных поделей. Функциональность, которая создаёт разрешения, находится в сигнале post_migrate.

Предположим, что у вас есть приложение foo и модель Bar в нём, проверить наличие у пользователя прав для этой модели можно следующим образом:

  • add: user.has_perm("foo.add_bar")

  • change: user.has_perm(foo.change_bar")

  • delete: user.hes_perm("foo.delete_bar")

  • view: user.has_perm("foo.view_bar")

Модель Group предназначена для категоризации пользователей, так вы можете предоставлять права или что-нибудь ещё какой-то группе пользователей. Пользователь может принадлежать произвольному количеству групп. Пользователь получает все права, которые назначены его группе.

Так же через группы можно дать набору пользователей какой-нибудь ярлык или расширенную функциональность. Например, можно отправлять email только пользователям из какой-то группы.

  1. django-rest-framework

Права в django-rest-framework определяют, следует ли удовлетворить или отклонить запрос к различным частям вашего API. Например, права могут проверять, что пользователь аутентифицирован (IsAuthenticated). Они всегда запускаются перед любым кодом вашего представления.

Где определяются права

Права в django-rest-framework всегда определяются как список классов прав. Каждое право из списка проверяется перед запуском тела вашего представления. Если хотя бы одно из них не проходит, то одно из исключений (exceptions.PermissionDenied или exceptions.NotAuthenticated) выбрасывается и код вашего представления не запускатеся.

Возвращается 403 или 401 соответственно статус ответа согласно следующим правилам:

  • Запрос был аутентифицирован, но право не предоставлено - 403 Forbidden.

  • Запрос не был аутентифицирован и класс для аутентификации с наивысшим приоритетом не использует WWW-Authenticate заголовок - 403 Forbidden.

  • Запрос не был аутентифицирован и заголовок используется - 401 Unauthorized.

Права можно задать глобально, используя настройку DEFAULT_PERMISSION_CLASSES:

REST_FRAMEWORK = {
  "DEFAULT_PERMISSION_CLASSES": [
    "rest_framework.permissions.IsAuthenticated",
  ]
}

Права можно задать для вашего представления используя permission_classes атрибут или get_permissions метод:

from rest_framework.dПрава можно задать глобально, используя настройку DEFAULT_PERMISSION_CLASSES:

ecorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def example_view(request, format=None):
  return Response()

Когда права определяются на уровне представления, то права в settings.py игнорируются.

Все права наследуются от BasePermission и могут быть объеденены в цепочки, используя побитовые операции (| (or), & (and), ~(not)).

Встроенные права

AllowAny

Это право предоставляет неограниченный доступ, не зависимо от того является ли запрос аутентифицированным или нет.

IsAuthenticated

Это право предоставляет доступ только аутентифицированным пользователя, иначе отклоняет запрос.

IsAdminUser

Это право предоставляет доступ только администраторам, т.е. пользователям, у которых поле is_staff равно True.

IsAuthenticatedOrReadOnly

Это право позволяет аутентифицированным пользователям выполнять любой тип запроса. Иначе запрос выполнится только в том случае, если его метод "безопасный" (GET, HEAD или OPTIONS).

DjangoModelPermissions

Это право связано со стандартными разрешениями django, расположенными в django.contrib.auth. Для его использования в вашем представлении должно быть объявлено либо .queryset свойство, либо .get_queryset() метод. Запрос будет разрешён только в том случае, если пользователь аутентифицирован и имеет соответствующее джанговское право для модели. Модель определяется по .get_queryset().model или .queryset.model.

  • POST запросы требуют, чтобы у пользователя было add право на модель.

  • PUT и PATCH запросы требуют, чтобы у пользователя было change право на модель.

  • DELETE запросы требуют, чтобы у пользователя было delete право на модель.

Также можно добавить свои джанговские права, переопределив право DjangoModelPermissions. Например, можно добавить право на просмотр модели (view) для GET-запросов. Для этого нужно переопределить .perms_map свойство.

DjangoModelPermissionsOrAnonReadOnly

Схоже с предыдущим, только неаутентфицированные пользователи получают доступ на чтение.

DjangoObjectPermissions

Как и DjangoModelPermissions это право связано со стандартными джанговскими правами. Оно позволяет работать с правами на уровне объектов. Но, чтобы его использовать, нужно добавить бэкенд, которых поддерживает права на уровне объектов (например, django-guardian).

Для view права можно использовать DjangoObjectPermissionsFilter класс из django-guardian, который возвращает только объекты, для которых у пользователя есть соответствующее право.

Пользовательские разрешения

Для того, чтобы реализовать своё право, нужно переопределить BasePermission и реализовать все (или один) из методов:

  • .has_permission(self, request, view)

  • .has_object_permission(self, request, view, obj)

Эти методы должны возвращать True, если запрос разрешён, иначе - False.

Если в вашем праве, нужно проверять является ли запрос "безопасным", можно использовать константу SAFE_METHODS, которая является кортежем, содержащим 'GET', 'OPTIONS' и 'HEAD'. Например:

if request.method in permissions.SAFE_METHODS:
  # read-only запрос
else:
  # запрос на запись

Метод has_object_permission вызывается, если метод has_permission пройдёт успешно.

Отметим, что generic представления проверяют права для объекта (has_object_permission) только тогда, когда запрашивается один объект. Если необходимо фильтровать список объектов, нужно фильтровать queryset отдельно в методе .get_queryset() вашего представления.

  1. Опыт использования прав на одном из проектов

Ниже расскажу про использования прав на одном из проектов. Но немного про сам проект.

Мы разрабатываем комплексное решение для автоматизации проектной деятельности, сам продукт - это по сути отечественный аналог MS Project с адаптацией под потребности российских компаний.

Особенность в том, что в продукте работают все участники проектной деятельности от топ-менеджмента до непосредственных исполнителей + смежные подразделения компаний + администратор системы. Нужно было создать достаточно сложную модель прав с учетом всей функциональности продукта и предусмотреть отдельные права для каждой роли пользователя в разных сценариях:

  • ведение проектов, программ и портфелей проектов, работа с инициативами для всех участников

  • отдельный кабинет для топ-менеджмента с контролем реализации проектов и верхнеуровневой аналитикой

  • работа с запросами на изменения и многоступенчатыми согласованиями с участием пользователей из смежных подразделений

Модели User и UserGroup

Мы не используем стандартные джанговские права. Вместо этого права представлены следующим образом:

USER_PERMISSION = Choices(
  ...
  ('project_view', 'Право на просмотр проектов'),
  ('project_add', 'Право на создание проектов'),
  ('project_change', 'Право на редактирование проектов'),
  ...
)

И храняться в модели как кастомное поле ChoiceArrayField:

class User(PermissionModelMixin, AbstractUser):
  ...
  groups = models.ManyToManyField(
    UserGroup,
    verbose_name='Группы',
    related_name='user_set',
    related_query_name='user',
    ...
  )
  permissions = ChoiceArrayField(
    models.CharField(max_length=32, blank=True, choices=USER_PERMISSION),
    blank=True,
    default=list,
    verbose_name='Права',
  )
  ...

Также мы не используем стандартную модель для групп, вместо этого у нас своя - UserGroup. Группы в моделе User хранятся как поле groups - many-to-many поле на модель UserGroup. Права и группы можно выбрать в админке на странице редактирования пользователя.

Чтобы была возможность проверять права у пользователя через метод .has_perm() (user.has_perm(USER_PERMISSION.project_add)), был добавлен свой бэкенд:

class ISUPModelBackend(ModelBackend):
  ...
  def _get_user_permissions(self, user_obj):
      return set(user_obj.permissions)

  def _get_group_permissions(self, user_obj):
      perms = sum(user_obj.groups.values_list('permissions', flat=True), [])
      perms = set(perms)

      return perms
  ...

Представления

class ProjectViewSet(...):
  ...
  permission_classes = [IsAuthenticated, ProjectPermission]
  ...

Модели и миксин PermissionModelMixin

Миксин для работы требует наличие атрибута authorization_class и определяет метод .can():

class PermissionModelMixin:
  authorization_class: Type[ModelAuthorization]

  def can(self, user: User, action: str) -> bool:
    authorization = self.authorization_class(self, user)
    can = authorization.can(action)
    return can
...

Модели наследуются от PermissionModelMixin и определяют атрибут authorization_class - класс, который наследуется от класса ModelAuthorization. Эти классы так же определяют метод .can():

class Project(..., PermissionModelMixin, ...):
  ...
  authorization_class = ProjectAuthorization
  ...

Права

Все наши права наследуются от базового класса ModelAuthorizationPermission, который в свою очередь наследуется от BasePermission, определённого в django-rest-framework.

class ModelAuthorizationPermission(BasePermission):
  authorization_action_mapping: Dict[str, str] = {}
  ...

  def has_permission(self, request, view):
    ...
    action = self.authorization_action_mapping.get(view.action)

    ...

    parent = ...

    if action:
        return parent.can(request.user, action)

    return False

  def has_object_permission(self, request, view, obj):
      action = self.authorization_action_mapping.get(view.action)

      ...

      if action:
          return obj.can(request.user, action)

      return False

Метод .can() определён в миксине PermissionModelMixin, от которого наследуются наши модели.

Каждое наше право определяет аттрибут-словарь authorization_action_mapping, который представляет собой отображение view action на права в системе.

class ProjectPermission(ModelAuthorizationPermission):
  authorization_action_mapping = {
    'create': USER_ACTIONS.project_add,
    'create_with_offer': USER_ACTIONS.project_add,
    'create_with_template': USER_ACTIONS.project_add,
    'create_with_federal_project': USER_ACTIONS.project_add,
    'update': PROJECT_ACTIONS.change,
    'destroy': PROJECT_ACTIONS.delete,
    'change_protocol': PROJECT_ACTIONS.change_protocol,
    'change_visibilities': PROJECT_ACTIONS.change_visibilities,
    'status_info': PROJECT_ACTIONS.view_status_info,
    'save_basic_plan': PROJECT_ACTIONS.save_basic_plan,
    'start': PROJECT_ACTIONS.change,
  }

Класс ModelAuthorization и наследники

Эти классы и занимаются проверкой прав.

Класс ModelAuthorization определяет метод .can():

class ModelAuthorization(metaclass=abc.ABCMeta):
  actions = abc.abstractproperty

  def __init__(self, instance: Model, user: User) -> None:
    self.instance = instance
    self.user = user

  def can(self, action: str) -> bool:
    if not self._is_ACTION_allowed(action):
        return False

    return self._has_ACTION_access(action)

  def _is_ACTION_allowed(self, action: str) -> bool:
    if action not in self.actions:
        return False

    checker_name = f'is_{action}_allowed'
    checker = getattr(self, checker_name)

    return checker()

  def _has_ACTION_access(self, action: str) -> bool:
    checker_name = f'has_{action}_access'
    checker = getattr(self, checker_name)

    return checker()

А его наследники - допустимые права и по два метода для каждого из них:

class ProjectAuthorization(ExternalModelAuthorizationMixin, ModelAuthorization):
  instance: Project
  actions = PROJECT_ACTIONS

  ...

  def is_change_allowed(self) -> bool:
    return self.instance.status in {
      PROJECT_STATUS_CHOICES.draft,
      PROJECT_STATUS_CHOICES.reopened,
    }

  def has_change_access(self) -> bool:
    is_developer = self.instance.is_developer(self.user)
    has_perm = self.user.has_perm(USER_PERMISSION.project_change)

    return is_developer or has_perm

  ...

Всё вместе

При определении модели наследуем её от PermissionModelMixin. Определяем для неё класс, наследуемый от класса ModelAuthorization. В нём для каждого права определяем два метода is_custom_permission_allowed и has_custom_permission_access. В этих методах определяем логику проверки у пользователя прав на совершение данного действия.

Определяем класс, наследуемый от класса ModelAuthorizationPermission - наше право. В нём задаём словарь - отображение action из ViewSet - на право в системе. Добавляем этот класс в соответствующий ViewSet.

Какие плюсы у данной реализации

Стандартные джанговские права позволяют проверять права на CRUD операции. Наша же реализация позволяет в ModelAuthorization проверять права отличные от CRUD.

Источник: https://habr.com/ru/articles/751586/


Интересные статьи

Интересные статьи

Приветствую всех!Предполагаю, что если вы нашли эту статью, то уже знакомы с процессом установки Docker и использования Django, поэтому не буду расписывать их детально.Я работаю на windows, поэтому ес...
Всем привет! В этой статье я расскажу как поднять простейшие вебсокеты на Django.Когда браузер клиенту нужно постоянное обновление данных с сервера, на ум сразу приходят сокеты. Но после множества про...
Верховный суд Эквадора постановил, что дикие животные имеют законные права на основании инцидента с шерстистой обезьяной, взятой из дикой природы и выращиваемой в качестве домашнего животного в течени...
Если вы являетесь регулярным читателем Хабра, то должно быть заметили что за последние несколько лет вышло немало статей о сборе персональных данных с мобильных устройств...
Мои размышления о неудаче брутфорса авторского права вызвали бурную реакцию и массу вопросов. Вопросы продолжают поступать. В процессе обсуждения возникает множество однотипных дискуссий, что отн...