September 10, 2019

DRF - Аутентификация и разрешения

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

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

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

Мы собираемся внести пару изменений в нашу модель Snippet. Сначала добавим пару полей.

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

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

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

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

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

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

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

def save(self, *args, **kwargs):
    """
    Используем библиотеку `pygments` для создания подсвеченного html кода
    """
    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(Snippet, self).save(*args, **kwargs)

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

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

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

python manage.py createsuperuser

Добавление API для моделей пользователя

Теперь, когда у нас есть несколько пользователей, нам лучше добавить представления этих пользователей в наш 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, поэтому нам нужно добавить для него явное поле.

Также добавим пару view во views.py. Хорошо было бы использовать view только для чтения, поэтому мы будем использовать generic view на основе классов 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

Добавьте следующее к шаблонам в snippets/urls.py

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

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

Прямо сейчас, если бы мы создали сниппет, не было бы способа связать пользователя, который создал сниппет, с экземпляром класса Snippet. Пользователь не отправляется как часть сериализованного представления, а является свойством запроса.

Мы пойдём путем переопределения метода .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' к списку полей во внутреннем метаклассе.

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

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

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

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

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

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

from rest_framework import permissions

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

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

Добавление логина в браузерную версию API

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

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

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

from django.conf.urls import include

И, в конце файла, добавьте шаблон, включающий views входа и выхода для API:

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

api-auth/ фактически может быть любым URL, который вы захотите использовать. Теперь, если вы снова откроете браузер и обновите страницу, вы увидите ссылку «Логин» в правом верхнем углу страницы. Если вы войдете в систему как один из созданных ранее пользователей, вы сможете снова создавать сниппеты.

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

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

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

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

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Разрешение, позволяющее только владельцам объекта редактировать его.
    """

    def has_object_permission(self, request, view, obj):
        # Разрешения на чтение разрешены для любого запроса,
        # поэтому мы всегда будем разрешать запросы GET, HEAD или OPTIONS.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Права на запись разрешены только владельцу сниппета.
        return obj.owner == request.user

Теперь мы можем добавить это пользовательское разрешение к нашей API сниппета, отредактировав свойство permission_classes во View 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."
}

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

Резюме

Теперь у нас есть достаточно детальный набор разрешений для нашего веб-API.

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