Как предотвратить эскалацию разрешений в администраторе Django при предоставлении разрешения на изменение пользователя?

У меня есть сайт django с большой клиентской базой. Я хотел бы дать нашему отделу обслуживания клиентов возможность изменять обычные учетные записи пользователей, делать такие вещи, как изменение паролей, адресов электронной почты и т. д. Однако, если я предоставлю кому-то встроенное разрешение auth | user | Can change user, он получит возможность установить флаг is_superuser по любому поводу, в том числе и по своему. (!!!)

Каков наилучший способ удалить эту опцию для сотрудников, не являющихся суперпользователями? Я уверен, что это включает в себя создание подкласса django.contrib.auth.forms.UserChangeForm и подключение его к моему уже настроенному объекту UserAdmin... каким-то образом. Но я не могу найти никакой документации о том, как это сделать, и я еще недостаточно хорошо понимаю внутренности.


person David Eyk    schedule 19.02.2010    source источник


Ответы (5)


они получают возможность устанавливать флаг is_superuser для любой учетной записи, включая свою собственную. (!!!)

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

Я уверен, что это связано с подклассом django.contrib.auth.forms.UserChangeForm.

Ну, не обязательно. Форма, которую вы видите на странице изменений администратора django, динамически создается приложением администратора и основана на UserChangeForm, но этот класс едва добавляет проверку регулярного выражения в поле username.

и подключить его к моему уже пользовательскому объекту UserAdmin...

Пользовательский UserAdmin - это то, что вам нужно. По сути, вы хотите изменить свойство fieldsets на что-то вроде этого:

class MyUserAdmin(UserAdmin):
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        # Removing the permission part
        # (_('Permissions'), {'fields': ('is_staff', 'is_active', 'is_superuser', 'user_permissions')}),
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
        # Keeping the group parts? Ok, but they shouldn't be able to define
        # their own groups, up to you...
        (_('Groups'), {'fields': ('groups',)}),
    )

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

class MyUserAdmin(UserAdmin):
    staff_fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        # No permissions
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
        (_('Groups'), {'fields': ('groups',)}),
    )

    def change_view(self, request, *args, **kwargs):
        # for non-superuser
        if not request.user.is_superuser:
            try:
                self.fieldsets = self.staff_fieldsets
                response = super(MyUserAdmin, self).change_view(request, *args, **kwargs)
            finally:
                # Reset fieldsets to its original value
                self.fieldsets = UserAdmin.fieldsets
            return response
        else:
            return super(MyUserAdmin, self).change_view(request, *args, **kwargs)
person Clément    schedule 19.02.2010
comment
Поскольку я использую группы для управления разрешениями, я также удалил раздел «Группы» из staff_fieldsets. - person David Eyk; 19.02.2010
comment
Спасибо! Это мне очень помогло! Тем не менее, Django 1.1.2, похоже, не понравился символ «_», который у вас был перед личной информацией и остальными. - person Tyug; 22.10.2010
comment
@Tyug: _ — это обычный псевдоним импорта для ugettext / ugettext_lazy, как вы можете видеть в примерах кода на docs.djangoproject.com/en/1.1/topics/i18n/internationalization : from django.utils.translation import ugettext as _ - person Clément; 23.10.2010
comment
Пользователь, не являющийся суперпользователем, по-прежнему может изменить пароль суперпользователя и войти в систему со своей учетной записью. Чтобы запретить это, я также удалил поле пароля из staff_fieldsets, написал обертку вокруг UserAdmin.user_change_password, которая запрещает изменять пароль суперпользователей для не-суперпользователей, и, наконец, добавил ссылку на "password/" в первых наборах полей description. - person jensq; 07.06.2011
comment
Скрытие полей недостаточно. Пользователь с достаточными знаниями в области веб-разработки сможет создавать свои собственные POST-запросы по соответствующему URL-адресу с помощью wget. - person Ioan Alexandru Cucu; 15.07.2013
comment
@DavidEyk и Клеман, могу ли я показать флажок суперпользователя при добавлении нового пользователя? Является ли это возможным ? - person Deepanshu Goyal; 21.10.2013
comment
Это больше не работает. Я на Django 1.7, так что, возможно, что-то изменилось. Файл /home/vivekv/.environments/timetracker/local/lib/python2.7/site-packages/django/contrib/auth/forms.py в clean_password 154. return self.initial[password] Тип исключения: KeyError at /admin /auth/user/2/ Значение исключения: u'password' - person vivekv; 07.12.2014

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

try:
    self.readonly_fields = self.staff_self_readonly_fields
    response = super(MyUserAdmin, self).change_view(request, object_id, form_url, extra_context, *args, **kwargs)
finally:
    # Reset fieldsets to its original value
    self.fieldsets = UserAdmin.fieldsets

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

class UserAdmin(BaseUserAdmin):
    staff_fieldsets = (
        (None, {'fields': ('username')}),
        ('Personal info', {'fields': ('first_name', 'last_name', 'email')}),
        # No permissions
        ('Important dates', {'fields': ('last_login', 'date_joined')}),
    )
    staff_readonly_fields = ('username', 'first_name', 'last_name', 'email', 'last_login', 'date_joined')

    def get_fieldsets(self, request, obj=None):
        if not request.user.is_superuser:
            return self.staff_fieldsets
        else:
            return super(UserAdmin, self).get_fieldsets(request, obj)

    def get_readonly_fields(self, request, obj=None):
        if not request.user.is_superuser:
            return self.staff_readonly_fields
        else:
            return super(UserAdmin, self).get_readonly_fields(request, obj)
person dkmita    schedule 30.03.2017

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

class MyUserAdmin(UserAdmin):
    model = User
    staff_self_fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        # No permissions
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )

    staff_other_fieldsets = (
        (None, {'fields': ('username', )}),
        (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
        # No permissions
        (_('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )

    staff_self_readonly_fields = ('last_login', 'date_joined')

    def change_view(self, request, object_id, form_url='', extra_context=None, *args, **kwargs):
        # for non-superuser
        if not request.user.is_superuser:
            try:
                if int(object_id) != request.user.id:
                    self.readonly_fields = User._meta.get_all_field_names()
                    self.fieldsets = self.staff_other_fieldsets
                else:
                    self.readonly_fields = self.staff_self_readonly_fields
                    self.fieldsets = self.staff_self_fieldsets

                response = super(MyUserAdmin, self).change_view(request, object_id, form_url, extra_context, *args, **kwargs)
            except:
                logger.error('Admin change view error. Returned all readonly fields')

                self.fieldsets = self.staff_other_fieldsets
                self.readonly_fields = ('first_name', 'last_name', 'email', 'username', 'password', 'last_login', 'date_joined')
                response = super(MyUserAdmin, self).change_view(request, object_id, form_url, extra_context, *args, **kwargs)
            finally:
                # Reset fieldsets to its original value
                self.fieldsets = UserAdmin.fieldsets
                self.readonly_fields = UserAdmin.readonly_fields
            return response
        else:
            return super(MyUserAdmin, self).change_view(request, object_id, form_url, extra_context, *args, **kwargs)
person Unicorn    schedule 09.03.2016

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

(для Джанго 1.11)

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

class RestrictedUserAdmin(UserAdmin):
    model = User

    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(RestrictedUserAdmin, self).formfield_for_dbfield(db_field, **kwargs)
        user = kwargs['request'].user
        if not user.is_superuser:
            if db_field.name == 'groups':
                field.queryset = field.queryset.filter(id__in=[i.id for i in user.groups.all()])
            if db_field.name == 'user_permissions':
                field.queryset = field.queryset.filter(id__in=[i.id for i in user.user_permissions.all()])
            if db_field.name == 'is_superuser':
                field.widget.attrs['disabled'] = True
        return field

admin.site.unregister(User)
admin.site.register(User, RestrictedUserAdmin)

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

person Eric Woudenberg    schedule 21.02.2019

Полный код для django 1.1 (ограничен базовой информацией о пользователе для сотрудников (не суперпользователей))

from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _


class MyUserAdmin(UserAdmin):
   my_fieldsets = (
       (None, {'fields': ('username', 'password')}),
       (_('Personal info'), {'fields': ('first_name', 'last_name', 'email')}),
   )

   def change_view(self, request, object_id, extra_context=None):
       # for non-superuser
       print 'test'
       if not request.user.is_superuser:
           self.fieldsets = self.my_fieldsets
           response = UserAdmin.change_view(self, request, object_id,
extra_context=None)
           return response
       else:
           return UserAdmin.change_view(self, request, object_id,
extra_context=None)


admin.site.unregister(User)
admin.site.register(User, MyUserAdmin)
person bopajdowski    schedule 12.08.2010