diff options
| author | Fabian Braun <fsbraun@gmx.de> | 2024-01-23 16:45:18 +0100 |
|---|---|---|
| committer | Natalia <124304+nessita@users.noreply.github.com> | 2024-02-20 12:13:32 -0300 |
| commit | e626716c28b6286f8cf0f8174077f3d2244f3eb3 (patch) | |
| tree | 7414e177d60175ed20fb0c4e7da5586ae154014b /tests/auth_tests | |
| parent | 8a757244f9e50c4d076e559e4b74b9d83ab089b6 (diff) | |
Fixed #34429 -- Allowed setting unusable passwords for users in the auth forms.
Co-authored-by: Natalia <124304+nessita@users.noreply.github.com>
Diffstat (limited to 'tests/auth_tests')
| -rw-r--r-- | tests/auth_tests/test_forms.py | 126 | ||||
| -rw-r--r-- | tests/auth_tests/test_views.py | 116 |
2 files changed, 224 insertions, 18 deletions
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py index 7e78006429..373a981955 100644 --- a/tests/auth_tests/test_forms.py +++ b/tests/auth_tests/test_forms.py @@ -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) diff --git a/tests/auth_tests/test_views.py b/tests/auth_tests/test_views.py index f4cf6ed2f4..d6bf6fbf52 100644 --- a/tests/auth_tests/test_views.py +++ b/tests/auth_tests/test_views.py @@ -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,25 @@ 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, { @@ -1453,11 +1469,105 @@ 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") + # 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, + { + "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.") + def test_user_change_different_user_password(self): u = User.objects.get(email="staffmember@example.com") response = self.client.post( |
