Ограничения доступа (permissions)

Курс по Django: https://stepik.org/a/183363

Теперь, когда мы разобрались с сериализаторами и представлениями Django REST Framework, пришла пора затронуть следующий важный вопрос по ограничению доступа. В английском языке – это называется permissions.

Чтобы продемонстрировать их работу в том объеме, который я хочу вам показать, вначале внесем небольшое изменение в модель таблицы women, где последним полем определим идентификатор пользователя, который добавил запись:

class Women(models.Model):
    title = models.CharField(max_length=255)
    content = models.TextField(blank=True)
    time_create = models.DateTimeField(auto_now_add=True)
    time_update = models.DateTimeField(auto_now=True)
    is_published = models.BooleanField(default=True)
    cat = models.ForeignKey('Category', on_delete=models.PROTECT, null=True)
    user = models.ForeignKey(User, verbose_name='Пользователь', on_delete=models.CASCADE)
 
    def __str__(self):
        return self.title

Здесь параметр on_delete=models.CASCADE означает, что при удалении пользователя будут удалены и все записи из таблицы women, которые с ним связаны.

Далее, нам нужно обновить миграции:

python manage.py makemigrations

Здесь появится запрос на ввод значения по умолчанию для этого поля, т.к. оно не может принимать значение NULL. Выберем пункт 1 и введем значение 1, как значение для нового поля. Это, как раз, идентификатор администратора в БД таблицы users.

После обновления миграций, их нужно применить командой:

python manage.py migrate

Видим, что все прошло успешно и в таблице women появилось поле user_id со значением 1.

В файле women/views.py я уберу класс вьюсета WomenViewSet и буду демонстрировать работу permissions на уровне обычных классов представлений, чтобы все было понятнее:

class WomenAPIList(generics.ListCreateAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
 
 
class WomenAPIUpdate(generics.RetrieveUpdateAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
 
 
class WomenAPIDestroy(generics.RetrieveDestroyAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer

Здесь класс WomenAPIList отдает список записей из таблицы women и позволяет создать новую запись; класс WomenAPIUpdate – меняет выбранную запись; класс WomenAPIDestroy – удаляет выбранную запись.

Далее, в файле drfsite/urls.py свяжем эти представления с маршрутами:

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/women/', WomenAPIList.as_view()),
    path('api/v1/women/<int:pk>/', WomenAPIUpdate.as_view()),
    path('api/v1/womendelete/<int:pk>/', WomenAPIDestroy.as_view()),
]

Все, у нас теперь любой пользователь может совершенно спокойно удалять, добавлять, изменять и читать записи из БД women, используя соответствующие URL-запросы. Как вы понимаете, это слишком большая свобода действий, т.к. любой желающий сможет удалить все с нашего сайта, используя такое API. Или изменить записи, написав, что то свое. Очевидно, такое поведение нужно ограничивать. Как раз для этого и используются permisions. В базовом варианте фреймворка Django REST, они следующие:

  • AllowAny – полный доступ;
  • IsAuthenticated – только для авторизованных пользователей;
  • IsAdminUser – только для администраторов;
  • IsAuthenticatedOrReadOnly – только для авторизованных или всем, но для чтения.

Подробно о них можно почитать на странице официальной документации:

https://www.django-rest-framework.org/api-guide/permissions/

Давайте сделаем так, чтобы добавлять записи могли только авторизованные пользователи. Воспользуемся классом IsAuthenticatedOrReadOnly и применим его к представлению WomenAPIList:

class WomenAPIList(generics.ListCreateAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
    permission_classes = (IsAuthenticatedOrReadOnly, )

Теперь, если мы не авторизованы, то при отображении запроса:

http://127.0.0.1:8000/api/v1/women/

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

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

http://127.0.0.1:8000/admin/login/

Введем логин и пароль и обновим прежний URL-запрос:

http://127.0.0.1:8000/api/v1/women/

У нас появилась форма для добавления записи, т.к. мы теперь воспринимаемся системой как авторизованные пользователи. Правда, у этой формы есть один недостаток. Добавляя запись, мы можем указать любого пользователя, который как будто добавил ее. Правильнее было бы это поле формировать автоматически, записывая в него данные текущего пользователя. Делается это очень просто. Перейдем в класс сериализатора WomenSerializer и пропишем там одну строчку (атрибут user):

class WomenSerializer(serializers.ModelSerializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
 
    class Meta:
        model = Women
        fields = "__all__"

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

Обновим страницу

http://127.0.0.1:8000/api/v1/women/

и видим, что в форме пропало поле для выбора пользователя. Теперь, любая добавленная нами запись будет связана с нашим user_id.

Отлично, на данный момент мы сделали новый функционал, где связали каждую запись таблицы women с конкретным пользователем и позволили добавлять новые записи только авторизованным пользователям. Но у нас есть еще два API-запроса: для изменения и удаления записи. Предположим, что удалять записи может только администратор. Следовательно, в классе WomenAPIDestroy применим permission IsAdminUser:

class WomenAPIDestroy(generics.RetrieveDestroyAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
    permission_classes = (IsAdminUser, )

Перейдем по адресу:

http://127.0.0.1:8000/api/v1/womendelete/9/

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

Пользовательские классы разрешений (custom permissions)

Как сделать так, чтобы удалять запись мог только администратор, но просматривать все пользователи? По умолчанию у нас нет такого класса permission. Но есть возможность создать его самостоятельно. Если мы откроем документацию, то увидим, что все permissions образуются от базового класса BasePermission, в котором определены два метода:

class BasePermission(metaclass=BasePermissionMetaclass):
    def has_permission(self, request, view):
        return True
 
 
    def has_object_permission(self, request, view, obj):
        return True

Первый метод has_permission позволяет настраивать права доступа на уровне всего запроса (от клиента), а второй метод has_object_permission – права доступа на уровне отдельного объекта (данных, записи БД).

Для запроса

http://127.0.0.1:8000/api/v1/womendelete/9/

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

Я назову этот класс IsAdminOrReadOnly и унаследую от базового BasePermission:

class IsAdminOrReadOnly(permissions.BasePermission):
    def has_permission(self, request, view):
        if request.method in permissions.SAFE_METHODS:
            return True
 
        return bool(request.user and request.user.is_staff)

Внутри класса переопределяем один метод has_permission и вначале проверяем, если запрос безопасный (GET, HEAD, OPTIONS), то доступ разрешаем – возвращаем значение True. Иначе, проверяем, что пользователь должен быть администратором. Вот эту последнюю строчку я просто взял из класса IsAdminUser, т.к. там эта проверка и реализуется.

Все, если теперь обновить страницу:

http://127.0.0.1:8000/api/v1/womendelete/9/

то для неавторизованных пользователей или не администраторов будут отображаться данные по текущей записи. Возможности удаления не будет. Если же мы войдем как администратор, то появится кнопка «DELETE» для удаления. Вот так, легко и просто мы с вами создали свой собственный класс permission и применили его к представлению WomenAPIDestroy.

И у нас с вами осталось последнее представление WomenAPIUpdate. Давайте в нем разрешим изменять запись только автору, то есть, пользователю, который ее добавил. А просматривать, по прежнему, всем пользователям. Здесь нам также понадобится свой собственный класс permission. Я его скопирую прямо из документации и он будет выглядеть так:

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.user == request.user

Смотрите, здесь переопределяем уже второй метод has_object_permission, так как нам нужен объект obj, который представляет собой запись из БД. С этой записью у нас связан пользователь – атрибут user. И мы проверяем, чтобы пользователи для записи и в запросе совпадали. Если это так, то метод вернет True, иначе (для небезопасных запросов) – False.

Пропишем этот класс в представлении WomenAPIUpdate:

class WomenAPIUpdate(generics.RetrieveUpdateAPIView):
    queryset = Women.objects.all()
    serializer_class = WomenSerializer
    permission_classes = (IsOwnerOrReadOnly, )

И при выполнении запроса:

http://127.0.0.1:8000/api/v1/women/9/

видим данные записи и форму для их изменения. Выйдем из системы. Снова войдем и теперь отображаются только данные – менять запись уже нельзя.

Глобальные настройки ограничений доступа

В заключение этого занятия отмечу, что мы можем назначать начальные глобальные ограничения доступа для всех URL разрабатываемого API в рамках DRF. Как правило, это делается в файле drfsite/settings.py, в котором прописывается ключ 'DEFAULT_PERMISSION_CLASSES' в словаре REST_FRAMEWORK:

REST_FRAMEWORK = {
    ...
 
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}

Сейчас мы указали, что для всех API-запросов ответ будет отдаваться только авторизованным пользователям. Однако, если открыть браузер и набрать:

http://127.0.0.1:8000/api/v1/women/

то мы увидим список записей. Почему так произошло? Дело в том, что в settings.py мы прописываем права доступа по умолчанию. Если в самом представлении они меняются (через атрибут permission_classes), то применяются новые.

Давайте этот атрибут в классе WomenAPIList поставим в комментарий и обновим страницу браузера. Теперь мы не получаем список данных, так как не авторизованы. Авторизуемся и повторим запрос. Видим снова весь список.

По умолчанию в словаре REST_FRAMEWORK установлены права для всех пользователей без ограничений:

REST_FRAMEWORK = {
    ...
 
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ]
}

Вот так достаточно просто в DRF можно управлять правами доступа на уровне представлений и классов, а также создавать свои классы permission.

Курс по Django: https://stepik.org/a/183363