Классы пользовательских прав доступа в Django REST Framework

Серия "Права доступа в Django REST Framework":

  1. Права доступа в Django REST Framework
  2. Встроенные классы разрешений в Django REST Framework
  3. Пользовательские классы прав доступа в Django REST Framework (эта статья!)

Цели

К концу этой статьи вы должны уметь:

  1. Создание пользовательских классов разрешений
  2. Объясните, когда следует использовать has_permission и has_object_permission в ваших пользовательских классах разрешений
  3. Возврат пользовательского сообщения об ошибке при отказе в разрешении
  4. Объединять и исключать классы разрешений с помощью операторов AND, OR и NOT

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

Если к вашему приложению предъявляются особые требования, а встроенные классы разрешений не отвечают этим требованиям, самое время начать создавать собственные пользовательские разрешения.

Создание пользовательских разрешений позволяет устанавливать разрешения на основе того, аутентифицирован пользователь или нет, метода запроса, группы, к которой принадлежит пользователь, атрибутов объекта, IP-адреса... или любой их комбинации.

Все классы разрешений, как пользовательские, так и встроенные, расширяются от класса BasePermission:

class BasePermission(metaclass=BasePermissionMetaclass):
    """
    A base class from which all permission classes should inherit.
    """

    def has_permission(self, request, view):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

    def has_object_permission(self, request, view, obj):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

У BasePermission есть два метода, has_permission и has_object_permission, которые оба возвращают True. Классы разрешений переопределяют один или оба этих метода, чтобы безусловно возвращали True. Если вы не переопределите эти методы, они всегда будут возвращать True, предоставляя неограниченный доступ.

Для получения дополнительной информации о has_permission в сравнении с has_object_permission, обязательно ознакомьтесь с первой статьей из этого цикла, Разрешения в Django REST Framework.

По правилам, пользовательские разрешения следует помещать в файл permissions.py. Это просто соглашение, так что вы можете не делать этого, если вам нужно организовать разрешения по-другому.

Как и в случае со встроенными разрешениями, если любой из классов разрешений, используемых в представлении, возвращает False из has_permission или has_object_permission, возникает исключение PermissionDenied. Чтобы изменить сообщение об ошибке, связанное с исключением, вы можете установить атрибут message непосредственно в вашем пользовательском классе разрешения.

Давайте рассмотрим несколько примеров.

Примеры пользовательских разрешений

Свойства пользователя

Вы можете захотеть предоставить разные уровни доступа разным пользователям, основываясь на их свойствах - например, являются ли они создателями объекта или сотрудниками?

Допустим, вы не хотите, чтобы сотрудники могли редактировать объекты. Вот как может выглядеть пользовательский класс разрешений для этого случая:

# permissions.py

from rest_framework import permissions


class AuthorAllStaffAllButEditOrReadOnly(permissions.BasePermission):

    edit_methods = ("PUT", "PATCH")

    def has_permission(self, request, view):
        if request.user.is_authenticated:
            return True

    def has_object_permission(self, request, view, obj):
        if request.user.is_superuser:
            return True

        if request.method in permissions.SAFE_METHODS:
            return True

        if obj.author == request.user:
            return True

        if request.user.is_staff and request.method not in self.edit_methods:
            return True

        return False

Здесь класс AuthorAllStaffAllButEditOrReadOnly расширяет BasePermission и переопределяет как has_permission, так и has_object_permission.

has_permission:

В has_permission проверяется только одна вещь: аутентифицирован ли пользователь. Если нет, то возникает исключение NotAuthenticated и доступ запрещается.

has_object_permission:

Поскольку вы никогда не должны ограничивать доступ суперпользователя, первая проверка -- request.user.is_superuser -- предоставляет доступ суперпользователю.

Далее мы проверяем, является ли метод запроса одним из "безопасных" -- request.method in permissions.SAFE_METHODS. Безопасные методы определены в rest_framework/permissions.py:

SAFE_METHODS = ('GET', 'HEAD', 'OPTIONS')

Эти методы не влияют на объект; они могут только читать его.

На первый взгляд может показаться, что проверка SAFE_METHODS должна быть в методе has_permission. Если вы проверяете только метод запроса, то именно там она и должна быть. Но в этом случае другие проверки не будут выполняться:

DRF Permissions Execution

Поскольку мы хотим предоставить доступ, когда метод является одним из безопасных или , когда пользователь является автором объекта или , когда пользователь является сотрудником, нам нужно проверить это на том же уровне. Другими словами, поскольку мы не можем проверить владельца на уровне has_permission, нам нужно проверить все на уровне has_object_permission.

Последняя возможность заключается в том, что пользователь является сотрудником: Им разрешены все методы, кроме тех, которые мы определили как edit_methods.

Наконец, вернитесь к имени класса: AuthorAllStaffAllButEditOrReadOnly. Вы всегда должны стараться называть класс разрешения как можно более информативно.

Помните, что has_object_permission никогда не выполняется для списочных представлений (независимо от того, из какого представления вы расширяетесь) или когда метод запроса POST (поскольку объект еще не существует).

Вы используете пользовательский класс разрешения так же, как и встроенный:

# views.py

from rest_framework import viewsets

from .models import Message
from .permissions import AuthorAllStaffAllButEditOrReadOnly
from .serializers import MessageSerializer


class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [AuthorAllStaffAllButEditOrReadOnly] # Custom permission class used

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

Автор объекта имеет к нему полный доступ. В то же время сотрудник может удалить объект, но не может его редактировать:

DRF Permissions Execution

А авторизованный пользователь может просматривать объект, но не может редактировать или удалять его:

DRF Permissions Execution

Свойства объекта

Хотя в предыдущем примере мы вкратце коснулись свойств объекта, акцент был сделан на свойствах пользователя (например, автора объекта). В этом примере мы сосредоточимся на свойствах объекта.

Как одно или несколько свойств объекта могут влиять на разрешения?

  1. Как и в предыдущем примере, вы можете ограничить доступ только для владельца объекта. Можно также ограничить доступ группой, к которой принадлежит владелец.
  2. Объекты могут иметь дату истечения срока действия, поэтому вы можете ограничить доступ к объектам старше n только некоторым пользователям.
  3. Вы можете реализовать DELETE как флаг (так что объект фактически не удаляется из базы данных). Тогда можно запретить доступ к объектам с флагом delete.

Допустим, вы хотите ограничить доступ к объектам старше 10 минут для всех, кроме суперпользователей:

# permissions.py

from datetime import datetime, timedelta

from django.utils import timezone
from rest_framework import permissions

class ExpiredObjectSuperuserOnly(permissions.BasePermission):

    def object_expired(self, obj):
        expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10))
        return obj.created < expired_on

    def has_object_permission(self, request, view, obj):

        if self.object_expired(obj) and not request.user.is_superuser:
            return False
        else:
            return True

В этом классе разрешений метод has_permission не переопределен - поэтому он всегда будет возвращать True.

Поскольку единственным важным свойством является время создания объекта, проверка происходит в has_object_permission (поскольку в has_permission у нас нет доступа к свойствам объекта).

Таким образом, если пользователь захочет получить доступ к объекту с истекшим сроком действия, то возникнет исключение PermissionDenied:

DRF Permissions Execution

Опять же, как и в предыдущем примере, мы можем проверить, является ли пользователь суперпользователем в has_permission, но если нет, то свойство объекта никогда не будет проверено.

Обратите внимание на сообщение об ошибке. Оно не очень информативно. Пользователь не знает, почему ему было отказано в доступе. Мы можем создать собственное сообщение об ошибке, добавив атрибут message к нашему классу разрешения:

class ExpiredObjectSuperuserOnly(permissions.BasePermission):

    message = "This object is expired." # custom error message

    def object_expired(self, obj):
        expired_on = timezone.make_aware(datetime.now() - timedelta(minutes=10))
        return obj.created < expired_on

    def has_object_permission(self, request, view, obj):

        if self.object_expired(obj) and not request.user.is_superuser:
            return False
        else:
            return True

Теперь пользователь видит, почему именно ему было отказано в разрешении:

DRF Permissions Execution

Объединение и исключение классов разрешений

Обычно, когда используется более одного класса разрешений, вы определяете их в представлении следующим образом:

permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]

Этот подход объединяет их таким образом, что разрешение выдается только в том случае, если все классы возвращают True.

Начиная с версии DRF 3.9.0, вы также можете объединять несколько классов с помощью логических операторов AND (&) или OR (|). Также, начиная с версии 3.9.2, поддерживается оператор NOT (~).

Эти операторы не ограничиваются пользовательскими классами разрешений. Их можно использовать и со встроенными.

Вместо того чтобы создавать множество сложных классов разрешений, похожих друг на друга, вы можете создать более простые классы и объединить их с помощью вышеупомянутых операторов.

Например, у вас могут быть разные разрешения для разных комбинаций групп. Допустим, вам нужны следующие разрешения:

  1. Разрешение для групп А или В
  2. Разрешение для групп B или C
  3. Разрешение для членов обеих групп B и C
  4. Разрешение для всех групп, кроме A

Хотя четыре класса разрешений не кажутся чем-то большим, это не очень хорошо масштабируется. Что, если у вас будет восемь различных групп - A, B, C, D, E, F, G? Это быстро раздуется до невозможности понимания и поддержки.

Можно упростить задачу и объединить их с операторами, создав сначала классы разрешений для групп A, B и C. Затем можно реализовать их следующим образом:

  1. permission_classes = [PermGroupA | PermGroupB]
  2. permission_classes = [PermGroupB | PermGroupC]
  3. permission_classes = [PermGroupB & PermGroupC]
  4. permission_classes = [~PermGroupA]

Когда в дело вступает OR (|), все может стать немного сложнее. Ошибки могут часто проскакивать мимо. Чтобы узнать больше, ознакомьтесь с обсуждением разрешений: Allow permissions to be composed pull request.

Оператор AND

AND - это поведение классов разрешений по умолчанию, достигаемое с помощью ,:

permission_classes = [IsAuthenticated, IsStaff, SomeCustomPermissionClass]

Также можно записать &:

permission_classes = [IsAuthenticated & IsStaff & SomeCustomPermissionClass]

Оператор OR

При использовании OR (|), когда любой из классов разрешений возвращает True, разрешение предоставляется. Вы можете использовать оператор OR, чтобы предложить несколько вариантов, при которых пользователь получает разрешение.

Давайте рассмотрим пример, в котором либо владелец объекта, либо сотрудник может редактировать или удалять объект.

Нам понадобятся два класса:

  1. IsStaff возвращает True, если пользователь is_staff
  2. IsOwner возвращает True, если пользователь тот же, что и obj.author

Код:

class IsStaff(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.user.is_staff:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        if request.user.is_staff:
            return True
        return False


class IsOwner(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.user.is_authenticated:
            return True
        return False

    def has_object_permission(self, request, view, obj):
        if obj.author == request.user:
            return True
        return False

Здесь много лишнего, но это необходимо.

Почему?

  1. Для охватывающих представлений списка

    Опять же, представление списка не проверяет наличие has_object_permission. Однако каждое из созданных разрешений должно быть отдельным . Не следует создавать класс разрешения, который необходимо объединить с другим классом разрешения, чтобы охватить представление списка. IsOwner ограничивает доступ аутентифицированных пользователей в has_permission - так что если IsOwner является единственным используемым классом, доступ к API все равно контролируется.

  2. Оба метода по умолчанию возвращают True

    При использовании OR, если вы не предоставите метод has_object_permission, пользователь будет иметь доступ к объекту, даже если он не должен этого делать.

    Примечания:

    • если вы опустите has_permission в классе IsOwner, любой сможет видеть или создавать в списке.

    • если опустить has_object_permission на IsStaff и объединить его с IsOwner на or, то либо одно, либо другое вернет True. Таким образом, зарегистрированный пользователь, не являющийся ни владельцем, ни сотрудником, сможет изменить содержимое.

Теперь, когда мы хорошо разработали классы разрешений, их легко комбинировать:

from rest_framework import viewsets

from .models import Message
from .permissions import IsStaff, IsOwner
from .serializers import MessageSerializer


class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [IsStaff | IsOwner] # or operator used

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

Здесь мы позволяем либо сотруднику, либо владельцу объекта изменить или удалить его.

Единственное требование IsOwner к представлениям списка - это аутентификация пользователя. Это означает, что аутентифицированный пользователь, не являющийся сотрудником, сможет создавать объекты.

Оператор NOT

Оператор NOT приводит к результату, прямо противоположному определенному классу разрешения. Другими словами, разрешение предоставляется всем пользователям, кроме тех, которые относятся к классу разрешений.

Допустим, у вас есть три группы пользователей:

  1. Технологии
  2. Management
  3. Финансы

Каждая из этих групп должна иметь доступ к конечным точкам API, предназначенным только для их конкретной группы.

Вот класс разрешения, который предоставляет доступ только членам группы Finances:

class IsFinancesMember(permissions.BasePermission):

    def has_permission(self, request, view):
        if request.user.groups.filter(name="Finances").exists():
            return True

Теперь, допустим, у вас есть новое представление, предназначенное для всех пользователей, не входящих в группу "Финансы". Для этого можно использовать оператор NOT:

from rest_framework import viewsets

from .models import Message
from .permissions import IsFinancesMember
from .serializers import MessageSerializer


class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [~IsFinancesMember] # using not operator

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

Таким образом, только члены группы "Финансы" не будут иметь доступа.

Будьте осторожны! Если вы используете только оператор NOT, всем остальным будет разрешен доступ, включая неаутентифицированных пользователей! Если это не то, что вы хотели сделать, вы можете исправить это, добавив еще один класс, например, так:

permission_classes = [~IsFinancesMember & IsAuthenticated]

Скобки

Внутри permission_classes вы также можете использовать круглые скобки (()), чтобы контролировать, какое выражение будет разрешено первым.

Быстрый пример:

class MessageViewSet(viewsets.ModelViewSet):

    permission_classes = [(IsFinancesMember | IsTechMember) & IsOwner] # using parentheses

    queryset = Message.objects.all()
    serializer_class = MessageSerializer

В этом примере сначала будет решена проблема (IsFinancesMember | IsTechMember). Затем результат этого решения будет использован с & IsOwner - например, ResultsFromFinancesOrTech & IsOwner. Это означает, что доступ будет предоставлен пользователю, который входит в группы Tech OR Finances и является владельцем объекта.

Заключение

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

При использовании пользовательских классов разрешений вы должны переопределить один или оба из следующих методов:

  • has_permission
  • has_object_permission

Если разрешение не дано в методе has_permission, неважно, что написано в has_object_permission - разрешение будет отклонено. Если вы не переопределите один из них (или оба), вам нужно учесть, что по умолчанию метод всегда будет возвращать True.

Вы можете объединять и исключать классы разрешений с помощью операторов AND, OR и NOT. Вы даже можете определить порядок, в котором разрешения разрешаются, с помощью круглых скобок.

Серия "Права доступа в Django REST Framework":

  1. Права доступа в Django REST Framework
  2. Встроенные классы разрешений в Django REST Framework
  3. Пользовательские классы прав доступа в Django REST Framework (эта статья!)
Вернуться на верх