mirror of
https://github.com/django/django.git
synced 2025-10-31 09:41:08 +00:00
Fixed #34429 -- Allowed setting unusable passwords for users in the auth forms.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
This commit is contained in:
@@ -221,6 +221,16 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
|
||||
form["password2"].errors,
|
||||
)
|
||||
|
||||
# passwords are not validated if `usable_password` is unset
|
||||
data = {
|
||||
"username": "othertestclient",
|
||||
"password1": "othertestclient",
|
||||
"password2": "othertestclient",
|
||||
"usable_password": "false",
|
||||
}
|
||||
form = BaseUserCreationForm(data)
|
||||
self.assertIs(form.is_valid(), True, form.errors)
|
||||
|
||||
def test_custom_form(self):
|
||||
class CustomUserCreationForm(BaseUserCreationForm):
|
||||
class Meta(BaseUserCreationForm.Meta):
|
||||
@@ -349,6 +359,19 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
|
||||
["The password is too similar to the first name."],
|
||||
)
|
||||
|
||||
# passwords are not validated if `usable_password` is unset
|
||||
form = CustomUserCreationForm(
|
||||
{
|
||||
"username": "testuser",
|
||||
"password1": "testpassword",
|
||||
"password2": "testpassword",
|
||||
"first_name": "testpassword",
|
||||
"last_name": "lastname",
|
||||
"usable_password": "false",
|
||||
}
|
||||
)
|
||||
self.assertIs(form.is_valid(), True, form.errors)
|
||||
|
||||
def test_username_field_autocapitalize_none(self):
|
||||
form = BaseUserCreationForm()
|
||||
self.assertEqual(
|
||||
@@ -368,6 +391,17 @@ class BaseUserCreationFormTest(TestDataMixin, TestCase):
|
||||
form.fields[field_name].widget.attrs["autocomplete"], autocomplete
|
||||
)
|
||||
|
||||
def test_unusable_password(self):
|
||||
data = {
|
||||
"username": "new-user-which-does-not-exist",
|
||||
"usable_password": "false",
|
||||
}
|
||||
form = BaseUserCreationForm(data)
|
||||
self.assertIs(form.is_valid(), True, form.errors)
|
||||
u = form.save()
|
||||
self.assertEqual(u.username, data["username"])
|
||||
self.assertFalse(u.has_usable_password())
|
||||
|
||||
|
||||
class UserCreationFormTest(TestDataMixin, TestCase):
|
||||
def test_case_insensitive_username(self):
|
||||
@@ -744,6 +778,23 @@ class SetPasswordFormTest(TestDataMixin, TestCase):
|
||||
form["new_password2"].errors,
|
||||
)
|
||||
|
||||
# SetPasswordForm does not consider usable_password for form validation
|
||||
data = {
|
||||
"new_password1": "testclient",
|
||||
"new_password2": "testclient",
|
||||
"usable_password": "false",
|
||||
}
|
||||
form = SetPasswordForm(user, data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form["new_password2"].errors), 2)
|
||||
self.assertIn(
|
||||
"The password is too similar to the username.", form["new_password2"].errors
|
||||
)
|
||||
self.assertIn(
|
||||
"This password is too short. It must contain at least 12 characters.",
|
||||
form["new_password2"].errors,
|
||||
)
|
||||
|
||||
def test_no_password(self):
|
||||
user = User.objects.get(username="testclient")
|
||||
data = {"new_password1": "new-password"}
|
||||
@@ -973,23 +1024,33 @@ class UserChangeFormTest(TestDataMixin, TestCase):
|
||||
|
||||
@override_settings(ROOT_URLCONF="auth_tests.urls_admin")
|
||||
def test_link_to_password_reset_in_helptext_via_to_field(self):
|
||||
user = User.objects.get(username="testclient")
|
||||
form = UserChangeForm(data={}, instance=user)
|
||||
password_help_text = form.fields["password"].help_text
|
||||
matches = re.search('<a href="(.*?)">', password_help_text)
|
||||
cases = [
|
||||
(
|
||||
"testclient",
|
||||
'you can change or unset the password using <a href="(.*?)">',
|
||||
),
|
||||
(
|
||||
"unusable_password",
|
||||
"Enable password-based authentication for this user by setting "
|
||||
'a password using <a href="(.*?)">this form</a>.',
|
||||
),
|
||||
]
|
||||
for username, expected_help_text in cases:
|
||||
with self.subTest(username=username):
|
||||
user = User.objects.get(username=username)
|
||||
form = UserChangeForm(data={}, instance=user)
|
||||
password_help_text = form.fields["password"].help_text
|
||||
matches = re.search(expected_help_text, password_help_text)
|
||||
|
||||
# URL to UserChangeForm in admin via to_field (instead of pk).
|
||||
admin_user_change_url = reverse(
|
||||
f"admin:{user._meta.app_label}_{user._meta.model_name}_change",
|
||||
args=(user.username,),
|
||||
)
|
||||
joined_url = urllib.parse.urljoin(admin_user_change_url, matches.group(1))
|
||||
url_prefix = f"admin:{user._meta.app_label}_{user._meta.model_name}"
|
||||
# URL to UserChangeForm in admin via to_field (instead of pk).
|
||||
user_change_url = reverse(f"{url_prefix}_change", args=(user.username,))
|
||||
joined_url = urllib.parse.urljoin(user_change_url, matches.group(1))
|
||||
|
||||
pw_change_url = reverse(
|
||||
f"admin:{user._meta.app_label}_{user._meta.model_name}_password_change",
|
||||
args=(user.pk,),
|
||||
)
|
||||
self.assertEqual(joined_url, pw_change_url)
|
||||
pw_change_url = reverse(
|
||||
f"{url_prefix}_password_change", args=(user.pk,)
|
||||
)
|
||||
self.assertEqual(joined_url, pw_change_url)
|
||||
|
||||
def test_custom_form(self):
|
||||
class CustomUserChangeForm(UserChangeForm):
|
||||
@@ -1363,6 +1424,15 @@ class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
|
||||
form["password2"].errors,
|
||||
)
|
||||
|
||||
# passwords are not validated if `usable_password` is unset
|
||||
data = {
|
||||
"password1": "testclient",
|
||||
"password2": "testclient",
|
||||
"usable_password": "false",
|
||||
}
|
||||
form = AdminPasswordChangeForm(user, data)
|
||||
self.assertIs(form.is_valid(), True, form.errors)
|
||||
|
||||
def test_password_whitespace_not_stripped(self):
|
||||
user = User.objects.get(username="testclient")
|
||||
data = {
|
||||
@@ -1417,3 +1487,29 @@ class AdminPasswordChangeFormTest(TestDataMixin, TestCase):
|
||||
self.assertEqual(
|
||||
form.fields[field_name].widget.attrs["autocomplete"], autocomplete
|
||||
)
|
||||
|
||||
def test_enable_password_authentication(self):
|
||||
user = User.objects.get(username="unusable_password")
|
||||
form = AdminPasswordChangeForm(
|
||||
user,
|
||||
{"password1": "complexpassword", "password2": "complexpassword"},
|
||||
)
|
||||
self.assertNotIn("usable_password", form.fields)
|
||||
self.assertIs(form.is_valid(), True)
|
||||
user = form.save(commit=True)
|
||||
self.assertIs(user.has_usable_password(), True)
|
||||
|
||||
def test_disable_password_authentication(self):
|
||||
user = User.objects.get(username="testclient")
|
||||
form = AdminPasswordChangeForm(
|
||||
user,
|
||||
{"usable_password": "false", "password1": "", "password2": "test"},
|
||||
)
|
||||
self.assertIn("usable_password", form.fields)
|
||||
self.assertIn(
|
||||
"If disabled, the current password for this user will be lost.",
|
||||
form.fields["usable_password"].help_text,
|
||||
)
|
||||
self.assertIs(form.is_valid(), True) # Valid despite password empty/mismatch.
|
||||
user = form.save(commit=True)
|
||||
self.assertIs(user.has_usable_password(), False)
|
||||
|
||||
@@ -23,6 +23,8 @@ from django.contrib.auth.views import (
|
||||
redirect_to_login,
|
||||
)
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.messages import Message
|
||||
from django.contrib.messages.test import MessagesTestMixin
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.contrib.sites.requests import RequestSite
|
||||
from django.core import mail
|
||||
@@ -1365,7 +1367,7 @@ def get_perm(Model, perm):
|
||||
ROOT_URLCONF="auth_tests.urls_admin",
|
||||
PASSWORD_HASHERS=["django.contrib.auth.hashers.MD5PasswordHasher"],
|
||||
)
|
||||
class ChangelistTests(AuthViewsTestCase):
|
||||
class ChangelistTests(MessagesTestMixin, AuthViewsTestCase):
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
@@ -1429,7 +1431,7 @@ class ChangelistTests(AuthViewsTestCase):
|
||||
row = LogEntry.objects.latest("id")
|
||||
self.assertEqual(row.get_change_message(), "No fields changed.")
|
||||
|
||||
def test_user_change_password(self):
|
||||
def test_user_with_usable_password_change_password(self):
|
||||
user_change_url = reverse(
|
||||
"auth_test_admin:auth_user_change", args=(self.admin.pk,)
|
||||
)
|
||||
@@ -1440,11 +1442,118 @@ class ChangelistTests(AuthViewsTestCase):
|
||||
response = self.client.get(user_change_url)
|
||||
# Test the link inside password field help_text.
|
||||
rel_link = re.search(
|
||||
r'you can change the password using <a href="([^"]*)">this form</a>',
|
||||
r'change or unset the password using <a href="([^"]*)">this form</a>',
|
||||
response.content.decode(),
|
||||
)[1]
|
||||
self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)
|
||||
|
||||
response = self.client.get(password_change_url)
|
||||
# Test the form title with original (usable) password
|
||||
self.assertContains(
|
||||
response, f"<h1>Change password: {self.admin.username}</h1>"
|
||||
)
|
||||
# Breadcrumb.
|
||||
self.assertContains(
|
||||
response, f"{self.admin.username}</a>\n› Change password"
|
||||
)
|
||||
# Submit buttons
|
||||
self.assertContains(response, '<input type="submit" name="set-password"')
|
||||
self.assertContains(response, '<input type="submit" name="unset-password"')
|
||||
|
||||
# Password change.
|
||||
response = self.client.post(
|
||||
password_change_url,
|
||||
{
|
||||
"password1": "password1",
|
||||
"password2": "password1",
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, user_change_url)
|
||||
self.assertMessages(
|
||||
response, [Message(level=25, message="Password changed successfully.")]
|
||||
)
|
||||
row = LogEntry.objects.latest("id")
|
||||
self.assertEqual(row.get_change_message(), "Changed password.")
|
||||
self.logout()
|
||||
self.login(password="password1")
|
||||
|
||||
# Disable password-based authentication without proper submit button.
|
||||
response = self.client.post(
|
||||
password_change_url,
|
||||
{
|
||||
"password1": "password1",
|
||||
"password2": "password1",
|
||||
"usable_password": "false",
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, password_change_url)
|
||||
self.assertMessages(
|
||||
response,
|
||||
[
|
||||
Message(
|
||||
level=40,
|
||||
message="Conflicting form data submitted. Please try again.",
|
||||
)
|
||||
],
|
||||
)
|
||||
# No password change yet.
|
||||
self.login(password="password1")
|
||||
|
||||
# Disable password-based authentication with proper submit button.
|
||||
response = self.client.post(
|
||||
password_change_url,
|
||||
{
|
||||
"password1": "password1",
|
||||
"password2": "password1",
|
||||
"usable_password": "false",
|
||||
"unset-password": 1,
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, user_change_url)
|
||||
self.assertMessages(
|
||||
response,
|
||||
[Message(level=25, message="Password-based authentication was disabled.")],
|
||||
)
|
||||
row = LogEntry.objects.latest("id")
|
||||
self.assertEqual(row.get_change_message(), "Changed password.")
|
||||
self.logout()
|
||||
# Password-based authentication was disabled.
|
||||
with self.assertRaises(AssertionError):
|
||||
self.login(password="password1")
|
||||
self.admin.refresh_from_db()
|
||||
self.assertIs(self.admin.has_usable_password(), False)
|
||||
|
||||
def test_user_with_unusable_password_change_password(self):
|
||||
# Test for title with unusable password with a test user
|
||||
test_user = User.objects.get(email="staffmember@example.com")
|
||||
test_user.set_unusable_password()
|
||||
test_user.save()
|
||||
user_change_url = reverse(
|
||||
"auth_test_admin:auth_user_change", args=(test_user.pk,)
|
||||
)
|
||||
password_change_url = reverse(
|
||||
"auth_test_admin:auth_user_password_change", args=(test_user.pk,)
|
||||
)
|
||||
|
||||
response = self.client.get(user_change_url)
|
||||
# Test the link inside password field help_text.
|
||||
rel_link = re.search(
|
||||
r'by setting a password using <a href="([^"]*)">this form</a>',
|
||||
response.content.decode(),
|
||||
)[1]
|
||||
self.assertEqual(urljoin(user_change_url, rel_link), password_change_url)
|
||||
|
||||
response = self.client.get(password_change_url)
|
||||
# Test the form title with original (usable) password
|
||||
self.assertContains(response, f"<h1>Set password: {test_user.username}</h1>")
|
||||
# Breadcrumb.
|
||||
self.assertContains(
|
||||
response, f"{test_user.username}</a>\n› Set password"
|
||||
)
|
||||
# Submit buttons
|
||||
self.assertContains(response, '<input type="submit" name="set-password"')
|
||||
self.assertNotContains(response, '<input type="submit" name="unset-password"')
|
||||
|
||||
response = self.client.post(
|
||||
password_change_url,
|
||||
{
|
||||
@@ -1453,10 +1562,11 @@ class ChangelistTests(AuthViewsTestCase):
|
||||
},
|
||||
)
|
||||
self.assertRedirects(response, user_change_url)
|
||||
self.assertMessages(
|
||||
response, [Message(level=25, message="Password changed successfully.")]
|
||||
)
|
||||
row = LogEntry.objects.latest("id")
|
||||
self.assertEqual(row.get_change_message(), "Changed password.")
|
||||
self.logout()
|
||||
self.login(password="password1")
|
||||
|
||||
def test_user_change_different_user_password(self):
|
||||
u = User.objects.get(email="staffmember@example.com")
|
||||
|
||||
Reference in New Issue
Block a user