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

django custom permissions
django admin permissions
django admin custom permissions
django permissions
django model permissions
django admin permissions view only
django admin staff permissions
django admin view permission

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.

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)

Prevent permission escalation in Django admin when granting “user , Prevent permission escalation in Django admin when granting “user change” permissions. admin.py. from django.contrib import admin. Django Admin and Model Permissions. Django admin has a very tight integration with the built-in authentication system, and model permissions in particular. Out of the box, Django admin is enforcing model permissions: If the user has no permissions on a model, then they won’t be able to see it or access it in the admin.

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)

What You Need to Know to Manage Users in Django Admin – Real , Implement Custom Business Roles in Django Admin Grant Permissions Only Using Groups; Prevent Non-Superusers From Editing Their Protect against permission escalation by preventing users from editing their own� In this tutorial, you protected your system by making the following adjustments in Django Admin: You protected against permission escalation by preventing users from editing their own permissions. You kept permissions tidy and maintainable by only forcing users to manage permissions only using groups.

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)

How do I prevent permission escalation in Django admin , How do I prevent permission escalation in Django admin when granting "user if I grant someone the built-in auth | user | Can change user permission, they� Django admin site: access, filtering and restricting¶ Tags: django. I’m going to show you how to filter what’s shown in django’s admin based on the current request, so for instance limiting the list of objects to what the current user has permission to see. Django’s build-in admin site is pretty great.

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)

#23559 (Staff (not superusers) should not manage perms of Users , The usual format is: [https://github.com/django/django/pull/#### PR] . to prevent staff user from editing other users who possibly have any permission: Refs #23559 -- warned about consequences of letting users edit User model in admin. How do the Django admin permissions work. Django admin CRUD permissions. By default, the Django admin allows access to users with superuser and staff permissions -- in case you've never heard of the terms Django superuser, Django staff or Django permissions, see the previous chapter which describes Django user management.

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.

Django security releases issued: 2.2.8 and 2.1.15 | Weblog, CVE-2019-19118: Privilege escalation in the Django admin. potential privilege escalation in a way that would avoid a recurrence of similar� This permission class only allows Django Admin Users(i.e user.is_staff = True) to access the api resource and all other users will not be able access the API resource. IsAuthenticatedOrReadOnly ¶ This permission class allows authenticated users to perform read and write operations on the API resource.

[KEYCLOAK-528] Privilege escalation to takeover 'realm-admin , In a multi-tenant situation you may want to keep control over the roles that tenant admins are able to grant to the users they are managing. If you have an app called 'drivers' with the Car model then one permission would be 'drivers.delete_car'. The permissions that Django automatically creates will be create, change, and delete.Read permission is not included in CRUD operation.Django decided to change CRUD's 'update' to 'change' for some reason.

CVE - Search Results, This could lead to local escalation of privilege by preventing user notification, with (To resolve this, the Django admin is adjusted to require edit permissions on� Overview. Django provides an authentication and authorization ("permission") system, built on top of the session framework discussed in the previous tutorial, that allows you to verify user credentials and define what actions each user is allowed to perform.

Django admin CRUD permissions, Note Granting a delete permission to a user on a given model, also requires granting the change permission to fulfill the delete action in the Django admin. This is� from django.contrib import auth from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand class Command (BaseCommand): help = 'Get a list of all permissions available in the system.' def handle (self, * args, ** options): permissions = set # We create (but not persist) a temporary superuser and use it to

Comments
  • Since I'm using Groups to manage permissions, I also removed the Groups section from staff_fieldsets.
  • Thanks! This helped me a lot! However, Django 1.1.2 didn't seem to like the '_' you had before Personal info and the rest.
  • @Tyug : the _ is the usual import alias for ugettext / ugettext_lazy, as you can see in code examples on docs.djangoproject.com/en/1.1/topics/i18n/internationalization : from django.utils.translation import ugettext as _
  • The non-superuser is still able to change a superusers' password and log in with her account. To disallow this, I removed the password field as well from the staff_fieldsets, wrote a wrapper around UserAdmin.user_change_password that disallows changing the password of superusers for non-superusers, and finally added a link to "password/" in the first fieldsets' description.
  • Hiding the fields is not sufficient. An user with sufficient web develpment knowledge will be able to craft his own POST requests at the appropriate url by using wget.