[1.8.x] Fixed #24334 -- Allowed admin password reset to work with non-digit custom user model primary key.

Thanks Loic for help and Simon for review.

Backport of fdf20093e0 from master
This commit is contained in:
Tim Graham 2015-02-12 12:14:19 -05:00
parent bd80fa6b0f
commit 8fc4840289
3 changed files with 67 additions and 3 deletions

View File

@ -2,6 +2,7 @@ from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.contrib import admin, messages from django.contrib import admin, messages
from django.contrib.admin.options import IS_POPUP_VAR from django.contrib.admin.options import IS_POPUP_VAR
from django.contrib.admin.utils import unquote
from django.contrib.auth import update_session_auth_hash from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import ( from django.contrib.auth.forms import (
AdminPasswordChangeForm, UserChangeForm, UserCreationForm, AdminPasswordChangeForm, UserChangeForm, UserCreationForm,
@ -10,9 +11,9 @@ from django.contrib.auth.models import Group, User
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.db import transaction from django.db import transaction
from django.http import Http404, HttpResponseRedirect from django.http import Http404, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.encoding import force_text
from django.utils.html import escape from django.utils.html import escape
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
from django.views.decorators.csrf import csrf_protect from django.views.decorators.csrf import csrf_protect
@ -79,7 +80,7 @@ class UserAdmin(admin.ModelAdmin):
def get_urls(self): def get_urls(self):
return [ return [
url(r'^(\d+)/password/$', self.admin_site.admin_view(self.user_change_password), name='auth_user_password_change'), url(r'^(.+)/password/$', self.admin_site.admin_view(self.user_change_password), name='auth_user_password_change'),
] + super(UserAdmin, self).get_urls() ] + super(UserAdmin, self).get_urls()
def lookup_allowed(self, lookup, value): def lookup_allowed(self, lookup, value):
@ -123,7 +124,12 @@ class UserAdmin(admin.ModelAdmin):
def user_change_password(self, request, id, form_url=''): def user_change_password(self, request, id, form_url=''):
if not self.has_change_permission(request): if not self.has_change_permission(request):
raise PermissionDenied raise PermissionDenied
user = get_object_or_404(self.get_queryset(request), pk=id) user = self.get_object(request, unquote(id))
if user is None:
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
'name': force_text(self.model._meta.verbose_name),
'key': escape(id),
})
if request.method == 'POST': if request.method == 'POST':
form = self.change_password_form(user, request.POST) form = self.change_password_form(user, request.POST)
if form.is_valid(): if form.is_valid():

View File

@ -18,6 +18,7 @@ from django.contrib.sessions.middleware import SessionMiddleware
from django.contrib.sites.requests import RequestSite from django.contrib.sites.requests import RequestSite
from django.core import mail from django.core import mail
from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy from django.core.urlresolvers import NoReverseMatch, reverse, reverse_lazy
from django.db import connection
from django.http import HttpRequest, QueryDict from django.http import HttpRequest, QueryDict
from django.middleware.csrf import CsrfViewMiddleware from django.middleware.csrf import CsrfViewMiddleware
from django.test import ( from django.test import (
@ -30,6 +31,7 @@ from django.utils.http import urlquote
from django.utils.six.moves.urllib.parse import ParseResult, urlparse from django.utils.six.moves.urllib.parse import ParseResult, urlparse
from django.utils.translation import LANGUAGE_SESSION_KEY from django.utils.translation import LANGUAGE_SESSION_KEY
from .models import UUIDUser
from .settings import AUTH_TEMPLATES from .settings import AUTH_TEMPLATES
@ -892,3 +894,38 @@ class ChangelistTests(AuthViewsTestCase):
self.assertEqual(row.user_id, self.admin.pk) self.assertEqual(row.user_id, self.admin.pk)
self.assertEqual(row.object_id, str(u.pk)) self.assertEqual(row.object_id, str(u.pk))
self.assertEqual(row.change_message, 'Changed password.') self.assertEqual(row.change_message, 'Changed password.')
def test_password_change_bad_url(self):
response = self.client.get(reverse('auth_test_admin:auth_user_password_change', args=('foobar',)))
self.assertEqual(response.status_code, 404)
@override_settings(
AUTH_USER_MODEL='auth.UUIDUser',
ROOT_URLCONF='auth_tests.urls_custom_user_admin',
)
class UUIDUserTests(TestCase):
def test_admin_password_change(self):
u = UUIDUser.objects.create_superuser(username='uuid', email='foo@bar.com', password='test')
self.assertTrue(self.client.login(username='uuid', password='test'))
user_change_url = reverse('custom_user_admin:auth_uuiduser_change', args=(u.pk,))
response = self.client.get(user_change_url)
self.assertEqual(response.status_code, 200)
password_change_url = reverse('custom_user_admin:auth_user_password_change', args=(u.pk,))
response = self.client.get(password_change_url)
self.assertEqual(response.status_code, 200)
# A LogEntry is created with pk=1 which breaks a FK constraint on MySQL
with connection.constraint_checks_disabled():
response = self.client.post(password_change_url, {
'password1': 'password1',
'password2': 'password1',
})
self.assertRedirects(response, user_change_url)
row = LogEntry.objects.latest('id')
self.assertEqual(row.user_id, 1) # harcoded in CustomUserAdmin.log_change()
self.assertEqual(row.object_id, str(u.pk))
self.assertEqual(row.change_message, 'Changed password.')

View File

@ -0,0 +1,21 @@
from django.conf.urls import include, url
from django.contrib import admin
from django.contrib.auth import get_user_model
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.urls import urlpatterns
site = admin.AdminSite(name='custom_user_admin')
class CustomUserAdmin(UserAdmin):
def log_change(self, request, object, message):
# LogEntry.user column doesn't get altered to expect a UUID, so set an
# integer manually to avoid causing an error.
request.user.pk = 1
super(CustomUserAdmin, self).log_change(request, object, message)
site.register(get_user_model(), CustomUserAdmin)
urlpatterns += [
url(r'^admin/', include(site.urls)),
]