Я Python Developer в компании Нетрика. В данной статье расскажу, как устроены права в Django и django-rest-framework и как мы используем их на одном из проектов.
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 только пользователям из какой-то группы.
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.decorators 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() вашего представления.
Опыт использования прав на одном из проектов
Ниже расскажу про использования прав на одном из проектов. Но немного про сам проект.
Мы разрабатываем комплексное решение для автоматизации проектной деятельности, сам продукт - это по сути отечественный аналог 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
...
Представления
В представлениях определяем атрибут permissions_classes с нашими правами:
class ProjectViewSet(...):
...
permission_classes = [IsAuthenticated, ProjectPermission]
...
Права
Все наши права наследуются от базового класса 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)
...
try:
parent = view.get_parent_instance()
except AttributeError:
parent = request.user
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,
}
Модели и миксин 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
...
Класс 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
...