Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

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.

like image 962
David Eyk Avatar asked Feb 19 '10 15:02

David Eyk


People also ask

How do I restrict access to parts of Django admin?

Django admin allows access to users marked as is_staff=True . To disable a user from being able to access the admin, you should set is_staff=False . This holds true even if the user is a superuser. is_superuser=True .

How do I give permission to user in Django?

If you have a set number of user types, you can create each user type as a group and give the necessary permissions to the group. Then, for every user that is added into the system and into the required group, the permissions are automatically granted to each user.

How does Django define custom permissions?

Django Admin Panel : In Admin Panel you will see Group in bold letter, Click on that and make 3-different group named level0, level1, level3 . Also, define the custom permissions according to the need. By Programmatically creating a group with permissions: Open python shell using python manage.py shell.

Does Django have superuser permissions?

A Django superuser, is its name implies, means it's a user with 'super' permissions. By extension, this means a superuser has access to any page in the Django admin, as well as permissions to Create, Read, Update and Delete any type of model record available in the Django admin.


2 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) 
like image 177
Clément Avatar answered Oct 30 '22 21:10

Clément


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) 
like image 27
dkmita Avatar answered Oct 30 '22 21:10

dkmita