Учебник 4: Аутентификация и разрешения

В настоящее время наш 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(Snippet, self).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

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

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

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

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

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

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

from rest_framework import permissions

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

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

Добавление входа в браузерный API

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

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

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

from django.urls import path, include

И в конце файла добавьте шаблон для включения представлений входа и выхода для просматриваемого 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, нам необходимо аутентифицировать наши запросы к нему, если мы хотим редактировать какие-либо сниппеты. Мы не установили никаких authentication classes <../api-guide/authentication>`** , поэтому сейчас применяются значения по умолчанию, а именно ``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, а также конечные точки для пользователей системы и для созданных ими фрагментов кода.

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

Вернуться на верх