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

Viewed 5307

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 passwords, email addresses, etc. However, if I grant someone the built-in auth | user | Can change user permission, they gain the ability to set the is_superuser flag on any account, including their own. (!!!)

What's the best way to remove this option for non-superuser staff? I'm sure it involves subclassing django.contrib.auth.forms.UserChangeForm and hooking it into my already-custom UserAdmin object... somehow. But I can't find any documentation on how to do this, and I don't yet understand the internals well enough.

5 Answers

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)

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.

Related