Opened 2 weeks ago

Last modified 12 days ago

#35959 assigned Bug

Admin "Change password" Button Visible with Only "Can view user" Permission

Reported by: Dev Namdev Owned by: Brock Smickley
Component: contrib.admin Version: 5.1
Severity: Normal Keywords: Permissions, Admin Interface, Change Password, View User, Permission Bug
Cc: Dev Namdev Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: yes

Description

There seems to be a bug (or design oversight) in the Django admin panel where the "Change password" button is visible to users who only have the Can view user permission. According to Django's permission model, a user who can only view users should not have access to modifying user details, including changing the password.

Steps to reproduce:

  1. Create a superuser and a test user.
  2. Grant the test user only the Can view user permission.
  3. Log in to the admin panel as the test user.
  4. Navigate to the user change page for any user.
  5. Observe that the "Change password" button is visible despite the user having no permission to change user details.

Attachments (2)

Screenshot 2024-12-01 112851.png (63.6 KB ) - added by Dev Namdev 2 weeks ago.
image-20241201-113218.png (35.4 KB ) - added by Dev Namdev 2 weeks ago.

Download all attachments as: .zip

Change History (10)

by Dev Namdev, 2 weeks ago

by Dev Namdev, 2 weeks ago

Attachment: image-20241201-113218.png added

comment:1 by Sarah Boyce, 2 weeks ago

Triage Stage: UnreviewedAccepted

Thank you for the report
Although this widget was updated in #34977, this behavior existed prior to that so not a release blocker

comment:2 by Brock Smickley, 2 weeks ago

Owner: set to Brock Smickley
Status: newassigned

comment:3 by Brock Smickley, 2 weeks ago

struggling with this one but I think I figured out how to test it!

  • tests/auth_tests/test_views.py

    diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
    index 98fdfe79b7..6e1ebc2b3b 100644
    a b class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):  
    17041704            ),
    17051705            html=True,
    17061706        )
     1707        self.assertNotContains(response, '<a class="button" href="../password/">Reset password</a>')
    17071708        # Value in POST data is ignored.
    17081709        data = self.get_user_data(u)
    17091710        data["password"] = "shouldnotchange"

Note to self for later: I should also probably test to make sure that the button does show for users with permission.

Last edited 2 weeks ago by Brock Smickley (previous) (diff)

comment:4 by Brock Smickley, 2 weeks ago

I speculate that the solution involves extracting the user's permission and passing it to the context in ReadOnlyPasswordHashWidget, but I'm struggling to figure out how to do that. I was trying to figure out a way to use the auth function from context_processors.py but the template isn't rendered by a request, right?

comment:5 by Sarah Boyce, 13 days ago

This is actually quite similar to #33171
I think maybe if the user has view only permission the password field shouldn't be available (maybe updating get_fieldsets)

in reply to:  5 comment:6 by Brock Smickley, 13 days ago

Replying to Sarah Boyce:

This is actually quite similar to #33171
I think maybe if the user has view only permission the password field shouldn't be available (maybe updating get_fieldsets)

ok, I understand how to check the user's permissions from get_fieldsets but then how do I omit the password field from there? I tried messing around with the exclude option as well as the raw fieldsets tuple but I couldn't figure anything out. is there an example of something like this already in the codebase?

comment:7 by Sarah Boyce, 12 days ago

  • django/contrib/auth/admin.py

    a b  
     1import copy
    12from django.conf import settings
    23from django.contrib import admin, messages
    34from django.contrib.admin.options import IS_POPUP_VAR
    class UserAdmin(admin.ModelAdmin):  
    8283        "user_permissions",
    8384    )
    8485
     86    @staticmethod
     87    def _remove_fields_from_fieldsets(fieldsets, fields):
     88        fieldset_without_fields = []
     89        for fieldset_name, fieldset in copy.deepcopy(fieldsets):
     90            fieldset["fields"] = [f for f in fieldset["fields"] if f not in fields]
     91            fieldset_without_fields.append((fieldset_name, fieldset))
     92        return fieldset_without_fields
     93
    8594    def get_fieldsets(self, request, obj=None):
    8695        if not obj:
    8796            return self.add_fieldsets
    88         return super().get_fieldsets(request, obj)
     97        fieldsets = super().get_fieldsets(request, obj)
     98        if not self.has_change_permission(request, obj):
     99            return self._remove_fields_from_fieldsets(
     100                fieldsets=fieldsets,
     101                fields=["password"]
     102            )
     103        return fieldsets
    89104
    90105    def get_form(self, request, obj=None, **kwargs):
    91106        """
  • tests/auth_tests/test_views.py

    diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py
    index 98fdfe79b7..e9ae523293 100644
    a b class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):  
    16921692        algo, salt, hash_string = u.password.split("$")
    16931693        self.assertContains(response, '<div class="readonly">testclient</div>')
    16941694        # ReadOnlyPasswordHashWidget is used to render the field.
    1695         self.assertContains(
     1695        self.assertNotContains(
    16961696            response,
    16971697            "<strong>algorithm</strong>: <bdi>%s</bdi>\n\n"
    16981698            "<strong>salt</strong>: <bdi>%s********************</bdi>\n\n"
    class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):  
    17041704            ),
    17051705            html=True,
    17061706        )
     1707        self.assertNotContains(response,'<a class="button" href="../password/">Reset password</a>')
    17071708        # Value in POST data is ignored.
    17081709        data = self.get_user_data(u)

Something like this maybe?

comment:8 by Brock Smickley, 12 days ago

Has patch: set
Note: See TracTickets for help on using tickets.
Back to Top