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

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

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

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

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

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

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

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

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

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

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

```python
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)
```

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

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

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

```bash
python manage.py createsuperuser
```

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

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

```python
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`, основанные на общих классах.

```python
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`.

```python
from snippets.serializers import UserSerializer
```

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```python
from rest_framework import permissions
```

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

```python
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
```

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

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

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

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

```python
from django.urls import path, include
```

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

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

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

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

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

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

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

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

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

```python
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`:

```python
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]
```

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

```python
from snippets.permissions import IsOwnerOrReadOnly
```

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

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

Поскольку теперь у нас есть набор прав доступа к API, нам нужно аутентифицировать наши запросы к нему, если мы хотим редактировать какие-либо сниппеты. Мы не установили никаких [классов аутентификации](../navigaciya-po-api/authentication), поэтому сейчас применяются значения по умолчанию, а именно `SessionAuthentication` и `BasicAuthentication`.

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

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

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

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

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

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

```bash
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](5-relationships-and-hyperlinked-apis) учебника мы рассмотрим, как мы можем связать все вместе, создав конечную точку HTML для наших выделенных фрагментов, и улучшить связность нашего API, используя гиперссылки для связей внутри системы.
