Restricting Write Access to Django Admin

Django has a great auto-generated administration interface. It’s saved me countless hours of development time and makes basic data maintenance a breeze. Although it’s designed to be extensible, it’s not without it’s quirks and hurtles. Fortunately, most of these can be overcome with a little code inspection. One of these quirks is the superficial inability to make certain models read-only for specific user groups.

To understand why this may not make sense for certain applications, we have to look at Django’s two basic permissions, “staff” permission, which allows a user to login to admin, but not necessarily do anything else, and “superuser” permission, which gives the user access to all Django models.

For one application I worked on, staff users needed to be able to login to admin and read and possibly edit User records. Since the User model stores each user’s permissions, the main problem we immediately run into is that giving a user permission to edit the User model gives them blanket permission to edit anyone’s permissions. What this means is that a staff user can give themselves superuser permission, remove another user’s superuser permission, assign an incorrect group permission, and generally wreak havoc. Now, in keeping with The Zen of Admin, if you’ve given a user staff permission, then they can probably be trusted not to screw around with these permissions. However, this is essentially an honor system, which may not always give you peace of mind. It’s still a great temptation and potential opening for accidental misuse.

Fortunately, we can fix this problem by disallowing staff users from editing user permissions with a relatively simple change to Django’s UserAdmin. Specifically, we can define a custom UserAdmin, inheriting from the default UserAdmin, by defining the following in <your_app>/admin.py:


from django.contrib import admin
from django.contrib.admin.util import flatten_fieldsets
from django.contrib.auth.admin import UserAdmin as _UserAdmin
from django.core.exceptions import PermissionDenied
from django.shortcuts import get_object_or_404
 
class UserAdmin(_UserAdmin):
    _readonly_fields = [] # Default fields that are readonly for everyone.
 
    def get_readonly_fields(self, request, obj):
        readonly = list(self._readonly_fields)
        if request.user.is_staff and not request.user.is_superuser:
            if obj.is_superuser:
                # Prevent a staff user from editing anything of a superuser.
                readonly.extend(flatten_fieldsets(self.declared_fieldsets))
            else:
                # Prevent a non-superuser from editing sensitive security-related fields.
                readonly.extend(['is_staff', 'is_superuser', 'user_permissions', 'groups'])
        return readonly
 
    def user_change_password(self, request, id):
        # Disallow a non-superuser from changing the password of a superuser.
        user = get_object_or_404(self.model, pk=id)
        if not request.user.is_superuser and user.is_superuser:
            raise PermissionDenied
        return super(UserAdmin, self).user_change_password(request, id)
 
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

This prevents staff users from editing any user permissions, but still allows them to edit other user data, and prevents them from editing anything for a superuser.

Leave a Reply