How do I prevent permission escalation in Django admin when granting “user change” permission?

后端 未结 5 1970
天命终不由人
天命终不由人 2020-12-23 21:49

I have a django site with a large customer base. I would like to give our customer service department the ability to alter normal user accounts, doing things like changing p

相关标签:
5条回答
  • 2020-12-23 22:28

    Great thanks to Clément. What I came up with when doing the same for my site is that I needed additionally to make all fields readonly for users you other than self. So basing on Clément's answer I addeed readonly fields and password field hiding when viewing not self

    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)
    
    0 讨论(0)
  • 2020-12-23 22:32

    they gain the ability to set the is_superuser flag on any account, including their own. (!!!)

    Not only this, they also gain the ability to give themselves any permissions one-by-one, same effect...

    I'm sure it involves subclassing django.contrib.auth.forms.UserChangeForm

    Well, not necessarily. The form you see in the change page of django's admin is dynamically created by the admin application, and based on UserChangeForm, but this class barely adds regex validation to the username field.

    and hooking it into my already-custom UserAdmin object...

    A custom UserAdmin is the way to go here. Basically, you want to change the fieldsets property to something like that :

    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',)}),
        )
    

    But the problem here is that this restriction will apply to all users. If this is not what you want, you could for example override change_view to behave differently depending on the permission of the users. Code snippet :

    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)
    
    0 讨论(0)
  • 2020-12-23 22:33

    Full code for django 1.1 (limited to basic user information for staff (not superusers))

    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)
    
    0 讨论(0)
  • 2020-12-23 22:39

    The below part of the accepted answer has a race condition where if two staff users try to access the admin form at the same time, one of them may get the superuser form.

    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
    

    To avoid this race condition (and in my opinion improve the overall quality of the solution), we can override the get_fieldsets() and get_readonly_fields() methods directly:

    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)
    
    0 讨论(0)
  • 2020-12-23 22:46

    This approach was put together from several helpful tips on the web. In this case we are modifying UserAdmin so that, for non-superuser staff with user add/change permission, the only permissions and groups they can grant another user are the ones the staff member already has.

    (for Django 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)
    

    This should likewise be done for GroupAdmin if a user is given permission to change groups.

    0 讨论(0)
提交回复
热议问题