Права доступа в Django REST Framework

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

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

Цели

К концу этой статьи вы должны быть в состоянии объяснить:

  1. Как работают разрешения DRF
  2. Сходства и различия между has_permission и has_object_permission
  3. Когда использовать has_permission и has_object_permission

Права в DRF

В DRF, разрешения, наряду с аутентификацией и ограничением, используются для предоставления или запрета доступа различных классов пользователей к различным частям API.

Аутентификация и авторизация работают рука об руку. Аутентификация всегда выполняется перед авторизацией.

Если аутентификация - это процесс проверки личности пользователя (пользователя, от которого пришел запрос, токена, которым он был подписан), то авторизация - это процесс проверки наличия у пользователя запроса необходимых разрешений для его выполнения (является ли он суперпользователем, является ли создателем объекта).

Процесс авторизации в DRF охватывается разрешениями.

Вид разрешений

APIView имеет два метода, которые проверяют наличие разрешений:

  1. check_permissions проверяет, должен ли запрос быть разрешен на основе данных запроса
  2. check_object_permissions проверяет, должен ли запрос быть разрешен на основе комбинации данных запроса и объекта
# rest_framework/views.py

class APIView(View):
    # other methods
    def check_permissions(self, request):
        """
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

    def check_object_permissions(self, request, obj):
        """
        Check if the request should be permitted for a given object.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_object_permission(request, self, obj):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )

При поступлении запроса выполняется аутентификация. Если аутентификация не успешна, выдается ошибка NotAuthenticated. После этого в цикле проверяются разрешения, и если какое-либо из них не проходит, то выдается ошибка PermissionDenied. Наконец, выполняется проверка дросселирования запроса.

check_permissions вызывается до выполнения обработчика представления, а check_object_permissions не выполняется, пока вы не вызовете его явным образом. Например:

class MessageSingleAPI(APIView):

    def get(self, request, pk):
        message = get_object_or_404(Message.objects.all(), pk=pk)
        self.check_object_permissions(request, message) # explicitly called
        serializer = MessageSerializer(message)
        return Response(serializer.data)

При использовании ViewSets и Generic Views, check_object_permissions вызывается после извлечения объекта из базы данных для всех детальных представлений.

# rest_framework/generics.py

class GenericAPIView(views.APIView):
    # other methods
    def get_object(self):
        """
        Returns the object the view is displaying.

        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)  # HERE

        return obj

Разрешение проверяется на все разрешения, и если хоть одно из них возвращает значение False, возникает ошибка PermissionDenied.

Классы разрешений

Разрешения в DRF определяются как список классов разрешений. Вы можете либо создать свой собственный, либо использовать один из семи встроенных классов. Все классы разрешений, как пользовательские, так и встроенные, расширяются от класса BasePermission:

class BasePermission(metaclass=BasePermissionMetaclass):

    def has_permission(self, request, view):
        return True

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

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

Вернитесь к методам check_permissions и check_object_permissions из начала статьи:

  • check_permissions вызывает has_permission для каждого из разрешений
  • check_object_permissions вызывает has_object_permission для каждого из разрешений

has_permission

has_permission используется для принятия решения о том, разрешен ли запросу и пользователю доступ к определенному представлению

Например:

  • Разрешен ли метод запроса?
  • Аутентифицирован ли пользователь?
  • Является ли пользователь администратором или суперпользователем?

has_permission обладает знаниями о запросе, но не об объекте запроса.

Как объяснялось в начале, has_permission (вызываемый check_permissions) выполняется до выполнения обработчика представления, без его явного вызова.

has_object_permission

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

Например:

  • Кто создал объект?
  • Когда он был создан?
  • К какой группе принадлежит объект?

Помимо знаний о запросе, has_object_permission также обладает данными об объекте запроса. Метод выполняется после того, как объект будет извлечен из базы данных.

В отличие от has_permission, has_object_permission не всегда выполняется по умолчанию:

  • При использовании APIView необходимо явно вызвать check_object_permission, чтобы выполнить has_object_permission для всех классов разрешений.
  • При использовании ViewSets (как ModelViewSet) или Generic Views (как RetrieveAPIView), has_object_permission выполняется через check_object_permission внутри метода get_object из коробки.
  • has_object_permission никогда не выполняется для списочных представлений (независимо от того, из какого представления вы расширяетесь) или когда метод запроса POST (поскольку объект еще не существует).
  • Когда любой has_permission возвращает False, has_object_permission не проверяется. Запрос немедленно отклоняется.

has_permission или has_object_permission

В чем разница между has_permission и has_object_permission в Django REST Framework?

DRF Permissions Execution

Опять же, для:

  • Списочные представления, выполняется только has_permission и запрос либо предоставляется, либо отказывается в доступе. Если в доступе отказано, объекты никогда не будут получены.
  • Подробные представления, has_permission выполняется, а затем толькоесли разрешение предоставлено, has_object_permission выполняется после получения объекта.

Встроенные классы разрешений DRF

Что касается встроенных классов разрешений DRF, то все они переопределяют has_permission, в то время как только DjangoObjectPermissions переопределяет has_object_permission:

Permission class has_permission has_object_permission
AllowAny
IsAuthenticated
IsAuthenticatedOrReadOnly
IsAdminUser
DjangoModelPermissions
DjangoModelPermissionsOrAnonReadOnly
DjangoObjectPermissions by extending DjangoModelPermissions

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

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

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

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

Правильное использование

Давайте рассмотрим небольшой пример:

from rest_framework import permissions


class AuthorOrReadOnly(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_permission мы отказываем в разрешении только неаутентифицированным пользователям. В этот момент у нас нет доступа к объекту, поэтому мы не знаем, является ли пользователь, сделавший запрос, автором нужного объекта.
  2. Если пользователь аутентифицирован, то после получения объекта вызывается has_object_permission, где мы проверяем, является ли автор объекта тем же, что и пользователь.

Результаты:

  List view Detail view
has_permission Grants permission to the authenticated user Grants permission to the authenticated user
has_object_permission Has no impact Grants permission to the author of the object
Result Access granted for the authenticated users Access granted to the owner of the object if they are authenticated

Неправильное использование

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

from rest_framework import permissions

class AuthenticatedOnly(permissions.BasePermission):

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

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

Подробный просмотр для неаутентифицированного пользователя:

DRF Browsable API Response detail view

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

И вид списка для неаутентифицированного пользователя:

DRF Browsable API Response list view

Что происходит?

  1. Представление списка проверяет только has_permission. Так как у пользовательского класса его нет, он проверяет has_permission из BasePermission, который безоговорочно возвращает True.
  2. В детальном представлении сначала проверяется has_permission (опять же, всегда True). Затем проверяется has_object_permission, который запрещает доступ неаутентифицированным пользователям.

Вот почему в этом примере неаутентифицированные запросы не имеют доступа к подробным представлениям, но имеют доступ к представлениям списков.

  List view Detail view
has_permission Uses the default function that grants permission without any condition Uses the default function that grants permission without any condition
has_object_permission Has no impact Grants permission to the authenticated user
Result Permission is always granted Permission is granted for the authenticated users

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

Заключение

Все разрешения, пользовательские или встроенные, в Django REST Framework используют либо has_permission, либо has_object_permission, либо оба для ограничения доступа к конечным точкам API.

Хотя has_permission не имеет ограничений на то, когда его можно использовать, у него нет доступа к нужному объекту. Поэтому это скорее "общая" проверка прав доступа, чтобы убедиться, что запрос и пользователь могут получить доступ к представлению. С другой стороны, поскольку has_object_permission имеет доступ к объекту, критерии могут быть гораздо более конкретными, но у него есть много ограничений на то, когда он может быть использован.

Помните, что если вы не переопределите методы, они всегда будут возвращать True, предоставляя неограниченный доступ. Только has_permission влияет на доступ к представлению списка, в то время как оба они влияют на доступ к представлению деталей.

Знание и понимание того, как работают оба этих метода, особенно важно при создании пользовательских классов разрешений.

Серия "Разрешения фреймворка Django REST":

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