В уроке 8 по созданию API на Django REST Framework, мы рассмотрим использование прав доступа групп пользователей и аутентификацию по сессиям.
Предыдущая часть и урок по DRF правам доступа: Django REST Framework создание API: Урок 7, часть 1, права доступа пользователей
Аутентификация по сессии - предназначена в основном для тестировании API в браузере, Вы передаете пароль и логин единожды, получая уникальный идентификатор, который перемещается в заголовок Cookies.
Такой вид аутентификации вызывает некоторые сложности, например:
- Вам нужно использовать одну сессию в нескольких доменах. Во многих случаях это будет невозможно сделать;
- Если у вас несколько клиентов в т.ч. настольные и мобильные приложения. Сессии могут хранить не только данные сессии, но и об приложении. Они могут быть не нужны и с ними будет сложнее работать;
- Токены являются лучшим способом защиты от CSRF атак.
Перейдем к практической работе.
Давайте из предыдущего урока дадим одно разрешение пользователю на просмотр статей в админ-панеле:

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


И все, что я могу делать в административной панели, так это просматривать статьи, так как я указал именно такое разрешение в правах пользователя. И чтобы данные разрешения работали также в API, как и в админ панели, нам необходимо добавить следующее:
Как пример я воспользуюсь представлением детальной страницы с возможностью обновления и удаления ArticleRetrieveUpdateDestroyAPIView
modules/blog/views/articles.py
class ArticleRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
"""
Представление для получения, обновления, редактирования одной статьи (GET, PUT, DELETE)
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
lookup_field = 'slug'
permission_classes = [permissions.DjangoModelPermissions]
authentication_classes = [authentication.SessionAuthentication]
Пояснение:
- Я добавил
DjangoModelPermissions
, для того, чтобы работали наши разрешения заданные пользователю на основе Django. - Также добавил
SessionAuthentication
, для правильной работы аутентификации по сессиям для проверки API в браузере.
Необязательно для тех, кто делает все в файле views.py.
Добавлю импорты в __init__.py:
from modules.blog.views.articles import *
__all__ = '__all__'
И добавлю обработку представлений в urls.py
from django.urls import path
from modules.blog import views
urlpatterns = [
path('articles/', views.ArticleListAPIView.as_view(), name='api_articles_list'),
path('articles/create/', views.ArticleCreateAPIView.as_view(), name='api_articles_create'),
path('articles/<str:slug>/', views.ArticleRetrieveUpdateDestroyAPIView.as_view(), name='api_articles_detail_update_delete'),
]
И давайте попробуем перейти на страницу детальной статьи с нашего пользователя, у которого есть право только просматривать статьи: http://127.0.0.1:8000/api/articles/test-article-4/

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

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


Проверим сначала в админ-панели с пользователя с группой Admin:

Теперь проверим на той же странице наши возможности через API: http://127.0.0.1:8000/api/articles/test-article-4/

И как видите, все работает так, как это необходимо, и разрешение пользователя и разрешения группы вместе складываются, ибо в примере я указал пользователю право на просмотр статьи, а в группе указал права на редактирование, удаление и добавление статьи.
Теперь рассмотрим возможность создания кастомного разрешения. Для этого нам необходимо улучшить нашу модель Article, добавив в нее FK ссылающийся на пользователя, добавившего статью.
Для тех, кто забыл, как мы делали модель, прошу перейти в этот урок: Django REST Framework создание API: Урок: 2, добавление модуля «Блог» и моделей
modules/blog/models/articles.py
from django.db import models
from django.core.validators import FileExtensionValidator
from django.contrib.auth.models import User
class Article(models.Model):
"""
Модель "Статьи"
"""
title = models.CharField(verbose_name='Заголовок', max_length=255)
slug = models.SlugField(verbose_name='URL', max_length=255, blank=True)
short_description = models.TextField(max_length=300, verbose_name='Краткое описание')
full_description = models.TextField(verbose_name='Полное описание')
created_at = models.DateTimeField(verbose_name='Дата добавления', auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(verbose_name='Дата обновления', auto_now=True, db_index=True)
thumbnail = models.ImageField(
verbose_name='Превью поста',
blank=True,
upload_to='images/thumbnails/',
validators=[FileExtensionValidator(
allowed_extensions=('png', 'jpg', 'webp', 'jpeg', 'gif'))
]
)
is_published = models.BooleanField(default=True, verbose_name='Опубликовано')
is_fixed = models.BooleanField(default=False, verbose_name='Зафиксировано')
# Связи по ключам
category = models.ForeignKey(to='Category', on_delete=models.PROTECT, null=True, verbose_name='Категория')
created_by = models.ForeignKey(User, verbose_name='Добавил', on_delete=models.PROTECT)
class Meta:
ordering = ('-is_fixed', '-created_at',)
verbose_name = 'Статья'
verbose_name_plural = 'Статьи'
db_table = 'app_articles'
def __str__(self):
return self.title
Пояснение:
- Добавил поле
created_by
, указав на модель User. - Модель User импортирую из
django.contrib.auth.models
Проведем миграции:
(venv) PS C:\Users\Razilator\Desktop\Projects\App\backend> py manage.py makemigrations
It is impossible to add a non-nullable field 'created_by' to article without specifying a default. This is because the database needs something to populate existing rows.
Please select a fix:
1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
2) Quit and manually define a default value in models.py.
Select an option: 1
Please enter the default value as valid Python.
The datetime and django.utils.timezone modules are available, so it is possible to provide e.g. timezone.now as a value.
Type 'exit' to exit this prompt
>>> 1
Migrations for 'blog':
modules\blog\migrations\0002_article_created_by.py
- Add field created_by to article
(venv) PS C:\Users\Razilator\Desktop\Projects\App\backend> py manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0002_article_created_by... OK
(venv) PS C:\Users\Razilator\Desktop\Projects\App\backend>
Для фиксации проблемы, я выбрал первый вариант и поставил 1, чтобы за пользователя использовался в статьях пользователь с ID - 1.
Создам файл permissions.py в нашем модуле "Blog", пример есть в документации: Permissions - Django REST framework
С следующим содержимым:
modules/blog/permissions.py
from rest_framework import permissions
class IsCreatedByOrReadOnly(permissions.BasePermission):
"""
Разрешение на уровне объекта, чтобы разрешить его редактирование только владельцам объекта.
Предполагается, что экземпляр модели имеет атрибут created_by связанный с моделью User.
"""
def has_object_permission(self, request, view, obj):
# Разрешение на чтение разрешено для любого запроса, поэтому мы всегда будем разрешать запросы GET, HEAD или OPTIONS.
if request.method in permissions.SAFE_METHODS:
return True
# Экземпляр должен иметь атрибут с именем `created_by`
return obj.created_by == request.user
Пояснение:
- В данном случае, мы создали разрешение для автора статьи на уровне объекта, т.е, если мы отключим ему разрешение на редактирование статей, и уберем такое право из его группы, но он будет автором статьи, то он сможет редактировать только свою собственную статью.
Давайте проверим это в деле, в наше представление добавим кастомное разрешение:
modules/blog/views/articles.py
from modules.blog.models import Article
from modules.blog.serializers import ArticleSerializer
from rest_framework import generics, permissions, authentication
from modules.blog.permissions import IsCreatedByOrReadOnly
class ArticleRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView):
"""
Представление для получения, обновления, редактирования одной статьи (GET, PUT, DELETE)
"""
queryset = Article.objects.all()
serializer_class = ArticleSerializer
lookup_field = 'slug'
permission_classes = [IsCreatedByOrReadOnly]
authentication_classes = [authentication.SessionAuthentication]
Уберем все разрешения, группу, и установим для записи 4 автора, на котором и будем проверять кастомное разрешение:


И проверим страницу с суперпользователя: http://127.0.0.1:8000/api/articles/test-article-4:

И теперь проверим эту же страницу, являясь автором статьи:

Как видите, наше кастомное разрешение сработало, и мы можем управлять нашей статьей, являясь администратором.