Что нужно знать для управления пользователями в Django Admin

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

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

Знаете ли вы, что штатные пользователи, которые управляют другими пользователями в админке, могут редактировать свои собственные разрешения? Знаете ли вы, что они также могут стать суперпользователями? В админке Django нет ничего, что могло бы этому помешать, так что решать вам!

К концу этого руководства вы узнаете, как защитить свою систему:

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

Модель прав

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

Django имеет встроенную систему аутентификации. Система аутентификации включает пользователей, группы и разрешения.

При создании модели Django автоматически создает четыре разрешения по умолчанию для следующих действий:

  1. add: Пользователи с этим разрешением могут добавить экземпляр модели.
  2. delete: Пользователи с этим разрешением могут удалить экземпляр модели.
  3. change: Пользователи с этим разрешением могут обновлять экземпляр модели.
  4. view: Пользователи с этим разрешением могут просматривать экземпляры этой модели. Это разрешение было долгожданным, и, наконец, оно было добавлено в Django 2.1.

Имена разрешений следуют очень конкретному соглашению об именах: <app>.<action>_<modelname>.

Давайте разберемся:

  • <app> - это название приложения. Например, модель User импортируется из приложения auth. (django.contrib.auth).
  • <action> - одно из действий выше (add, delete, change, или view).
  • <modelname> - это название модели, написанное строчными буквами.

Знание этого соглашения об именах поможет вам легче управлять разрешениями. Например, имя разрешения на изменение пользователя - auth.change_user.

Как проверить разрешения

Разрешения модели предоставляются пользователям или группам. Чтобы проверить, есть ли у пользователя определенное разрешение, вы можете сделать следующее:

>>> from django.contrib.auth.models import User
>>> u = User.objects.create_user(username='haki')
>>> u.has_perm('auth.change_user')
False

Стоит отметить, что .has_perm() всегда будет возвращать True для активного суперпользователя, даже если разрешение на самом деле не существует:

>>> from django.contrib.auth.models import User
>>> superuser = User.objects.create_superuser(
...     username='superhaki',
...     email='me@hakibenita.com',
...     password='secret',
)
>>> superuser.has_perm('does.not.exist')
True

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

Как применить разрешения

Модели Django сами по себе не применяют разрешения. Единственное место, где по умолчанию устанавливаются права доступа, - это Django Admin.

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

Например, чтобы запретить пользователю без разрешений на просмотр в модели User доступ к представлению, которое показывает информацию о пользователе, выполните следующие действия:

from django.core.exceptions import PermissionDenied

def users_list_view(request):
    if not request.user.has_perm('auth.view_user'):
        raise PermissionDenied()

Если пользователь, выполняющий запрос, вошел в систему и прошел аутентификацию, то request.user будет содержать экземпляр User. Если пользователь не вошел в систему, request.user будет экземпляром AnonymousUser. Это специальный объект, используемый Django для обозначения неаутентифицированного пользователя. Использование has_perm для AnonymousUser всегда будет возвращать False.

Если у пользователя, выполняющего запрос, нет разрешения view_user, вы вызываете исключение PermissionDenied, и клиенту возвращается ответ со статусом 403.

Чтобы упростить принудительное применение разрешений в представлениях, Django предоставляет декоратор ярлыков с именем permission_required, который делает то же самое:

from django.contrib.auth.decorators import permission_required

@permission_required('auth.view_user')
def users_list_view(request):
    pass

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

{% if perms.auth.delete_user %}
<button>Delete user!</button>
{% endif %}

Некоторые популярные сторонние приложения, такие как Django rest framework, также обеспечивают полезную интеграцию с разрешениями модели Django.

Django админка и права модели

Админка Django имеет очень тесную интеграцию со встроенной системой аутентификации и, в частности, с разрешениями модели. По умолчанию админка Django обеспечивает права доступа к модели:

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

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

Реализация пользовательских бизнес-ролей в Django Admin

Одно из самых уязвимых мест в каждом приложении - это система аутентификации. В приложениях Django это модель User. Итак, чтобы лучше защитить свое приложение, вы собираетесь начать с модели User.

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

Настройка: пользовательская админка User

Чтобы предоставить настраиваемую админку для модели User, вам необходимо отменить регистрацию существующего администратора модели, предоставленного Django, и зарегистрировать одного из своих:

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

# Отменить регистрацию предоставленного администратора модели
admin.site.unregister(User)

# Зарегистрируйте собственного администратора модели на основе UserAdmin по умолчанию
@admin.register(User)
class CustomUserAdmin(UserAdmin):
    pass

Ваш CustomUserAdmin расширяет UserAdmin Django. Вы сделали это, чтобы воспользоваться всей работой, уже проделанной разработчиками Django.

На этом этапе, если вы войдете в систему администратора Django по адресу http://127.0.0.1:8000/admin/auth/user/, вы увидите, что администратор пользователя не изменился:

Django bare boned user admin

Расширяя UserAdmin, вы можете использовать все встроенные функции, предоставляемые админкой Django.

Запрет обновления полей

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

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

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

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    readonly_fields = [
        'date_joined',
    ]

Когда поле добавляется в readonly_fields, его нельзя будет редактировать в форме изменения по умолчанию для администратора. Когда поле помечено как только для чтения, Django отобразит элемент ввода как отключенный.

Но что, если вы хотите запретить только некоторым пользователям обновлять поле?

Условно запретить обновление полей

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

Допустим, вы хотите запретить пользователям, не являющимся суперпользователями, изменять имя пользователя. Для этого вам нужно изменить форму изменения, созданную Django, и отключить поле имени пользователя в зависимости от текущего пользователя:

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        is_superuser = request.user.is_superuser

        if not is_superuser:
            form.base_fields['username'].disabled = True

        return form

Давайте разберемся:

  • Чтобы внести изменения в форму, вы переопределяете get_form(). Эта функция используется Django для создания формы изменения по умолчанию для модели.
  • Чтобы условно отключить поле, вы сначала получаете форму по умолчанию, созданную Django, а затем, если пользователь не является суперпользователем, отключите поле имени пользователя.

Теперь, когда не суперпользователь пытается редактировать пользователя, поле имени пользователя будет отключено. Любая попытка изменить имя пользователя через Django Admin потерпит неудачу. Когда суперпользователь пытается отредактировать пользователя, поле имени пользователя будет редактируемым и будет вести себя так, как ожидалось.

Запретить пользователям, не являющимся суперпользователями, предоставлять права суперпользователя

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

Основываясь на предыдущем примере, чтобы запретить пользователям, не являющимся суперпользователями, стать суперпользователями, вы добавляете следующее ограничение:

from typing import Set

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        is_superuser = request.user.is_superuser
        disabled_fields = set()  # type: Set[str]

        if not is_superuser:
            disabled_fields |= {
                'username',
                'is_superuser',
            }

        for f in disabled_fields:
            if f in form.base_fields:
                form.base_fields[f].disabled = True

        return form

В дополнение к предыдущему примеру вы внесли следующие дополнения:

  1. Вы инициализировали пустой набор disabled_fields, который будет содержать поля для отключения. set - это структура данных, которая содержит уникальные значения. В этом случае имеет смысл использовать набор, потому что вам нужно отключить поле только один раз. Оператор | = используется для выполнения обновления OR на месте. Дополнительные сведения о наборах смотрите в разделе set в Python.

  2. Затем, если пользователь является суперпользователем, вы добавляете в набор два поля (имя пользователя из предыдущего примера и is_superuser). Они не позволят пользователям, не являющимся суперпользователями, стать суперпользователями.

  3. Наконец, вы перебираете поля в наборе, помечаете их все как отключенные и возвращаете форму.

Двухэтапная форма администратора пользователя Django

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

Этот двухэтапный процесс уникален для модели User. Чтобы приспособиться к этому уникальному процессу, вы должны убедиться, что поле существует, прежде чем пытаться отключить его. В противном случае вы можете получить KeyError. В этом нет необходимости, если вы настраиваете администраторов других моделей.

Предоставление разрешений только с помощью групп

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

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

from typing import Set

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        is_superuser = request.user.is_superuser
        disabled_fields = set()  # type: Set[str]

        if not is_superuser:
            disabled_fields |= {
                'username',
                'is_superuser',
                'user_permissions',
            }

        for f in disabled_fields:
            if f in form.base_fields:
                form.base_fields[f].disabled = True

        return form

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

Запретить пользователям, не являющимся суперпользователями, изменять свои собственные разрешения

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

from typing import Set

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    def get_form(self, request, obj=None, **kwargs):
        form = super().get_form(request, obj, **kwargs)
        is_superuser = request.user.is_superuser
        disabled_fields = set()  # type: Set[str]

        if not is_superuser:
            disabled_fields |= {
                'username',
                'is_superuser',
                'user_permissions',
            }

        # Запретить пользователям, не являющимся суперпользователями,
        # редактировать свои собственные разрешения
        if (
            not is_superuser
            and obj is not None
            and obj == request.user
        ):
            disabled_fields |= {
                'is_staff',
                'is_superuser',
                'groups',
                'user_permissions',
            }

        for f in disabled_fields:
            if f in form.base_fields:
                form.base_fields[f].disabled = True

        return form

Аргумент obj - это экземпляр объекта, с которым вы в данный момент работаете:

  • Когда obj равно None, форма используется для создания нового пользователя.
  • Если obj не равно None, форма используется для редактирования существующего пользователя.

Чтобы проверить, работает ли пользователь, выполняющий запрос, над собой, вы сравните request.user с obj. Поскольку это пользователь admin, obj либо экземпляр User, либо None. Когда пользователь, выполняющий запрос, request.user, равен obj, это означает, что пользователь обновляет себя. В этом случае вы отключите все конфиденциальные поля, которые можно использовать для получения разрешений.

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

Переопределение разрешения

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

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

Чтобы запретить штатным пользователям удалять экземпляр модели, независимо от их разрешений, вы можете сделать следующее:

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    def has_delete_permission(self, request, obj=None):
        return False

Как и в случае с get_form(), obj - это экземпляр, с которым вы сейчас работаете:

  • Если obj равно None, пользователь запросил представление списка.
  • Если obj не равно None, пользователь запросил изменение представления определенного экземпляра.

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

  • Предотвращение изменений в рабочее время
  • Реализация разрешений на уровне объекта

Ограничить доступ к настраиваемым действиям

 

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

Чтобы проиллюстрировать это, добавьте удобное действие администратора, чтобы отметить нескольких пользователей как активных:

 

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    actions = [
        'activate_users',
    ]

    def activate_users(self, request, queryset):
        cnt = queryset.filter(is_active=False).update(is_active=True)
        self.message_user(request, 'Activated {} users.'.format(cnt))
    activate_users.short_description = 'Activate Users'  # type: ignore

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

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

Администратор Django использует внутреннюю функцию для получения действий. Чтобы скрыть activate_users() от пользователей без разрешения на изменение, переопределите get_actions():

from django.contrib import admin
from django.contrib.auth.models import User
from django.contrib.auth.admin import UserAdmin

@admin.register(User)
class CustomUserAdmin(UserAdmin):
    actions = [
        'activate_users',
    ]

    def activate_users(self, request, queryset):
        assert request.user.has_perm('auth.change_user')
        cnt = queryset.filter(is_active=False).update(is_active=True)
        self.message_user(request, 'Activated {} users.'.format(cnt))
    activate_users.short_description = 'Activate Users'  # type: ignore

    def get_actions(self, request):
        actions = super().get_actions(request)
        if not request.user.has_perm('auth.change_user'):
            del actions['activate_users']
        return actions

get_actions() возвращает OrderedDict. Ключ - это имя действия, а значение - функция действия. Чтобы настроить возвращаемое значение, вы переопределяете функцию, получаете исходное значение и, в зависимости от разрешений пользователя, удаляете настраиваемое действие activate_users из dict. На всякий случай вы также подтверждаете права пользователя в действии.

Для штатных пользователей без разрешений change_user() действие activate_users не будет отображаться в раскрывающемся списке действий.

Заключение

Админка Django - отличный инструмент для управления проектом Django. Многие команды полагаются на него, чтобы оставаться продуктивным при управлении повседневными операциями. Если вы используете администратор Django для выполнения операций с моделями, важно знать разрешения. Методы, описанные в этой статье, полезны для любого администратора модели, а не только для модели User.

В этом руководстве вы защитили свою систему, внося следующие изменения в Django Admin:

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

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

https://realpython.com/manage-users-in-django-admin/

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