Пишем декоратор для проверки прав доступа по нахождению в группе

У встроенной в Django системы аутентификации имеется большое количество полезных заготовок уже "из коробки". В том числе набор декораторов для проверки наличия у пользователя определенных прав на те или иные действия.

Для тех, кому не терпится перейти к написанию кода - велком ниже. С остальными мы прежде пробежимся по уже имеющимся фичам.

Встроенные декораторы обработки прав доступа

Посмотрим, какие именно декораторы предоставляет нам модуль django.contrib.auth.

login_required

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

Для class-based (CBV) вью имеется отдельный миксин LoginRequiredMixin, живущий в django.contrib.auth.mixins.

staff_member_required

Дает добро тем авторизированным пользователям сайте, которым разрешено использовать так или иначе функционал встроенной админки Django. Staff - не админ,, скорее - менеджер определенной контентной части веб-сайта. Обладающий хоть и расширенными, но все равно ограниченными правами.

user_passes_test

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

from django.contrib.auth.decorators import user_passes_test
  
def lastname_check(user):
    return user.lastname.startswith('Putin')
  
@user_passes_test(lastname_check)
def blabla_view(request):
    print('Nadoel!!!')


permission_required

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

Проверка по членству в группе

По до конца не ясным для меня причинам в Django отсутствуем декоратор для выдачи прав юзеру, состоящему в какой-то определенной группе. Причины есть, ибо запросы о добавлении такого декоратора поступали от пользователей фреймворка  еще в прошлой пятилетке. Самая очевидная - зависимость вашего кода от существования конкретных пользовательских групп с конкретными названиями. Напомню, что по дефолту в Django никаких групп не создается. Их добавляете, при необходимости, вы сами. С другой стороны - встроенный декоратор permission_required может создавать точно такие же хардкордные зависимости.

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

Наш новый декоратор group_required

Код будем писать в файле utils.py, который расположим в основном каталоге проекта. Там, где у нас лежит settings.py. Для нового функционала импортируем нужные приблуды.

Прежде, заимеем утилиту six из модуля django.utils для совместимости декоратора с Python разных версий.

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

Ну и, конечно, универсальный user_passes_test. Результат "прогона" через него и станет основой проверки.

# project_name/utils.py
from django.utils import six
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import user_passes_test
   
def group_required(group, login_url=None, raise_exception=False):
    """
    Декоратор для вью. Разрешает доступ в случае нахождения пользователя 
    в одной из указанных групп.
    """
    def check_perms(user):
        if isinstance(group, six.string_types):
            groups = (group, )
        else:
            groups = group
        # Проверяем членство пользователя в нужной группе
        # по его имени.
        if user.groups.filter(username__in=groups).exists():
            return True
        # Если в интересующей нас группе такое имя не фигурирует -
        # кидаемся в пользователя 403-й ошибкой.
        if raise_exception:
            raise PermissionDenied
        # As the last resort, show the login form
        return False
    # Возвращаем окончательный вердикт декоратора касаемо
    # проверки запрошенных прав.
    return user_passes_test(check_perms)

Использование

# some_app/views.py
...
from project_name.utils import group_required
    
# Если хотим проверять по нахождению в одной из нескольких,
# то передаем первым позиционным аргументом декоратору
# кортеж с нужными группами.
@group_required(('group_one', 'group_two'))
def my_some_app_view():
    ...
   
# Если по одной группе, то передаем просто строку.
@group_required('group_one') 
def my_some_app_view(): 
    ...