Аутентификация/права доступа

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

  • Фрагменты кода всегда связаны с создателем.

  • Создавать сниппеты могут только авторизованные пользователи.

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

  • Неаутентифицированные запросы должны иметь полный доступ только для чтения.

Добавление информации в нашу модель

Мы собираемся внести несколько изменений в наш класс модели Snippet. Во-первых, добавим пару полей. Одно из этих полей будет использоваться для представления пользователя, создавшего фрагмент кода. Другое поле будет использоваться для хранения выделенного HTML-представления кода.

Добавьте следующие два поля к модели Snippet в models.py.

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()

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

Нам понадобятся дополнительные импорты:

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

И теперь мы можем добавить метод .save() в наш класс модели:

def save(self, *args, **kwargs):
    """
    Use the `pygments` library to create a highlighted HTML
    representation of the code snippet.
    """
    lexer = get_lexer_by_name(self.language)
    linenos = 'table' if self.linenos else False
    options = {'title': self.title} if self.title else {}
    formatter = HtmlFormatter(style=self.style, linenos=linenos,
                              full=True, **options)
    self.highlighted = highlight(self.code, lexer, formatter)
    super().save(*args, **kwargs)

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

rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate

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

python manage.py createsuperuser

Добавление конечных точек для наших моделей User

Теперь, когда у нас есть несколько пользователей для работы, нам лучше добавить представления этих пользователей в наш API. Создать новый сериализатор очень просто. В serializers.py добавьте:

from django.contrib.auth.models import User

class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']

Поскольку 'snippets' является обратным отношением к модели User, оно не будет включено по умолчанию при использовании класса ModelSerializer, поэтому нам нужно добавить явное поле для него.

Мы также добавим пару представлений в views.py. Мы хотим использовать представления только для чтения для пользовательских представлений, поэтому мы будем использовать представления ListAPIView и RetrieveAPIView, основанные на общих классах.

from django.contrib.auth.models import User


class UserList(generics.ListAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer


class UserDetail(generics.RetrieveAPIView):
    queryset = User.objects.all()
    serializer_class = UserSerializer

Не забудьте также импортировать класс UserSerializer.

from snippets.serializers import UserSerializer

Наконец, нам нужно добавить эти представления в API, ссылаясь на них из URL conf. Добавьте следующее к шаблонам в snippets/urls.py.

path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),

Связывание сниппетов с пользователями

Сейчас, если мы создаем сниппет кода, нет возможности связать пользователя, создавшего сниппет, с экземпляром сниппета. Пользователь не передается как часть сериализованного представления, а является свойством входящего запроса.

Мы решаем эту проблему путем переопределения метода .perform_create() в наших представлениях фрагментов, что позволяет нам изменять способ сохранения экземпляра и обрабатывать любую информацию, которая подразумевается во входящем запросе или запрашиваемом URL.

В классе представления SnippetList добавьте следующий метод:

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

Теперь методу create() нашего сериализатора будет передано дополнительное поле 'owner' вместе с провалидированными данными из запроса.

Обновление нашего сериализатора

Теперь, когда сниппеты связаны с пользователем, который их создал, давайте обновим наш SnippetSerializer, чтобы отразить это. Добавьте следующее поле в определение сериализатора в serializers.py:

owner = serializers.ReadOnlyField(source='owner.username')

Примечание: Убедитесь, что вы также добавили 'owner' в список полей во внутреннем классе Meta.

В этом поле происходит нечто весьма интересное. Аргумент source управляет тем, какой атрибут используется для заполнения поля, и может указывать на любой атрибут сериализованного экземпляра. Он также может принимать точечную нотацию, показанную выше, в этом случае он будет перебирать заданные атрибуты, подобно тому, как это используется в языке шаблонов Django.

Поле, которое мы добавили, представляет собой нетипизированный класс ReadOnlyField, в отличие от других типизированных полей, таких как CharField, BooleanField и т.д.. Нетипизированное ReadOnlyField всегда только для чтения, оно будет использоваться для сериализованных представлений, но не будет использоваться для обновления экземпляров модели при их десериализации. Мы могли бы также использовать здесь CharField(read_only=True).

Добавление необходимых разрешений к представлениям

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

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

Сначала добавьте следующий импорт в модуль views

from rest_framework import permissions

Затем добавьте следующее свойство к обоим классам представления SnippetList и SnippetDetail.

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

Добавление входа в Web-интерфейс API

Если вы откроете браузер и перейдете к Web-интерфейсу API, то обнаружите, что больше не можете создавать новые фрагменты кода. Для этого нам нужно иметь возможность войти в систему как пользователь.

Мы можем добавить представление входа для использования с Web-интерфейсом API, отредактировав URLconf в нашем файле urls.py на уровне проекта.

Добавьте следующий импорт в верхней части файла:

from django.urls import path, include

И в конце файла добавьте шаблон для включения представлений входа и выхода для Web-интерфейса API.

urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]

Часть шаблона 'api-auth/' может быть любым URL, который вы хотите использовать.

Теперь, если вы снова откроете браузер и обновите страницу, вы увидите ссылку 'Login' в правом верхнем углу страницы. Если вы войдете в систему как один из пользователей, созданных ранее, вы снова сможете создавать фрагменты кода.

После создания нескольких фрагментов кода перейдите к конечной точке '/users/' и обратите внимание, что представление включает список идентификаторов фрагментов, связанных с каждым пользователем, в поле 'snippets' каждого пользователя.

Разрешения на уровне объекта

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

Для этого нам понадобится создать пользовательское разрешение.

В приложении snippets создайте новый файл permissions.py.

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

Теперь мы можем добавить это пользовательское разрешение в конечную точку экземпляра сниппета, отредактировав свойство permission_classes в классе представления SnippetDetail:

permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

Не забудьте также импортировать класс IsOwnerOrReadOnly.

from snippets.permissions import IsOwnerOrReadOnly

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

Аутентификация в API

Поскольку теперь у нас есть набор прав доступа к API, нам нужно аутентифицировать наши запросы к нему, если мы хотим редактировать какие-либо сниппеты. Мы не установили никаких классов аутентификации, поэтому сейчас применяются значения по умолчанию, а именно SessionAuthentication и BasicAuthentication.

Когда мы взаимодействуем с API через веб-браузер, мы можем войти в систему, и тогда сессия браузера обеспечит необходимую аутентификацию для запросов.

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

Если мы попытаемся создать сниппет без аутентификации, мы получим ошибку:

http POST http://127.0.0.1:8000/snippets/ code="print(123)"

{
    "detail": "Authentication credentials were not provided."
}

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

http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"

{
    "id": 1,
    "owner": "admin",
    "title": "foo",
    "code": "print(789)",
    "linenos": false,
    "language": "python",
    "style": "friendly"
}

Резюме

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

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

Last updated