mirror of
https://github.com/django/django.git
synced 2025-03-06 15:32:33 +00:00
[5.2.x] Fixed #36140 -- Allowed BaseUserCreationForm to define non required password fields.
Regression in e626716c28b6286f8cf0f8174077f3d2244f3eb3. Thanks buffgecko12 for the report and Sarah Boyce for the review. Backport of d15454a6e84a595ffc8dc1b926282f484f782a8f from main.
This commit is contained in:
parent
e939cffa50
commit
affad13d0c
@ -109,14 +109,14 @@ class SetPasswordMixin:
|
||||
def create_password_fields(label1=_("Password"), label2=_("Password confirmation")):
|
||||
password1 = forms.CharField(
|
||||
label=label1,
|
||||
required=False,
|
||||
required=True,
|
||||
strip=False,
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
||||
help_text=password_validation.password_validators_help_text_html(),
|
||||
)
|
||||
password2 = forms.CharField(
|
||||
label=label2,
|
||||
required=False,
|
||||
required=True,
|
||||
widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}),
|
||||
strip=False,
|
||||
help_text=_("Enter the same password as before, for verification."),
|
||||
@ -132,20 +132,6 @@ class SetPasswordMixin:
|
||||
password1 = self.cleaned_data.get(password1_field_name)
|
||||
password2 = self.cleaned_data.get(password2_field_name)
|
||||
|
||||
if not password1 and password1_field_name not in self.errors:
|
||||
error = ValidationError(
|
||||
self.fields[password1_field_name].error_messages["required"],
|
||||
code="required",
|
||||
)
|
||||
self.add_error(password1_field_name, error)
|
||||
|
||||
if not password2 and password2_field_name not in self.errors:
|
||||
error = ValidationError(
|
||||
self.fields[password2_field_name].error_messages["required"],
|
||||
code="required",
|
||||
)
|
||||
self.add_error(password2_field_name, error)
|
||||
|
||||
if password1 and password2 and password1 != password2:
|
||||
error = ValidationError(
|
||||
self.error_messages["password_mismatch"],
|
||||
@ -193,19 +179,39 @@ class SetUnusablePasswordMixin:
|
||||
help_text=help_text,
|
||||
)
|
||||
|
||||
@sensitive_variables("password1", "password2")
|
||||
def validate_passwords(
|
||||
self,
|
||||
*args,
|
||||
password1_field_name="password1",
|
||||
password2_field_name="password2",
|
||||
usable_password_field_name="usable_password",
|
||||
**kwargs,
|
||||
):
|
||||
usable_password = (
|
||||
self.cleaned_data.pop(usable_password_field_name, None) != "false"
|
||||
)
|
||||
self.cleaned_data["set_usable_password"] = usable_password
|
||||
|
||||
if usable_password:
|
||||
super().validate_passwords(*args, **kwargs)
|
||||
if not usable_password:
|
||||
return
|
||||
|
||||
password1 = self.cleaned_data.get(password1_field_name)
|
||||
password2 = self.cleaned_data.get(password2_field_name)
|
||||
|
||||
if not password1 and password1_field_name not in self.errors:
|
||||
error = ValidationError(
|
||||
self.fields[password1_field_name].error_messages["required"],
|
||||
code="required",
|
||||
)
|
||||
self.add_error(password1_field_name, error)
|
||||
|
||||
if not password2 and password2_field_name not in self.errors:
|
||||
error = ValidationError(
|
||||
self.fields[password2_field_name].error_messages["required"],
|
||||
code="required",
|
||||
)
|
||||
self.add_error(password2_field_name, error)
|
||||
|
||||
super().validate_passwords(password1_field_name, password2_field_name)
|
||||
|
||||
def validate_password_for_user(self, user, **kwargs):
|
||||
if self.cleaned_data["set_usable_password"]:
|
||||
@ -575,6 +581,8 @@ class AdminPasswordChangeForm(SetUnusablePasswordMixin, SetPasswordMixin, forms.
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["password1"].widget.attrs["autofocus"] = True
|
||||
if self.user.has_usable_password():
|
||||
self.fields["password1"].required = False
|
||||
self.fields["password2"].required = False
|
||||
self.fields["usable_password"] = (
|
||||
SetUnusablePasswordMixin.create_usable_password_field(
|
||||
self.usable_password_help_text
|
||||
@ -601,3 +609,8 @@ class AdminPasswordChangeForm(SetUnusablePasswordMixin, SetPasswordMixin, forms.
|
||||
class AdminUserCreationForm(SetUnusablePasswordMixin, UserCreationForm):
|
||||
|
||||
usable_password = SetUnusablePasswordMixin.create_usable_password_field()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["password1"].required = False
|
||||
self.fields["password2"].required = False
|
||||
|
@ -12,3 +12,7 @@ Bugfixes
|
||||
* Fixed a regression in Django 5.1.5 that caused ``validate_ipv6_address()``
|
||||
and ``validate_ipv46_address()`` to crash when handling non-string values
|
||||
(:ticket:`36098`).
|
||||
|
||||
* Fixed a regression in Django 5.1 where password fields, despite being set to
|
||||
``required=False``, were still treated as required in forms derived from
|
||||
:class:`~django.contrib.auth.forms.BaseUserCreationForm` (:ticket:`36140`).
|
||||
|
@ -1,8 +1,10 @@
|
||||
import datetime
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse
|
||||
from unittest import mock
|
||||
|
||||
from django import forms
|
||||
from django.contrib.auth.forms import (
|
||||
AdminPasswordChangeForm,
|
||||
AdminUserCreationForm,
|
||||
@ -13,6 +15,7 @@ from django.contrib.auth.forms import (
|
||||
ReadOnlyPasswordHashField,
|
||||
ReadOnlyPasswordHashWidget,
|
||||
SetPasswordForm,
|
||||
SetPasswordMixin,
|
||||
UserChangeForm,
|
||||
UserCreationForm,
|
||||
UsernameField,
|
||||
@ -24,13 +27,14 @@ from django.contrib.sites.models import Site
|
||||
from django.core import mail
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.forms import forms
|
||||
from django.forms.fields import CharField, Field, IntegerField
|
||||
from django.test import SimpleTestCase, TestCase, override_settings
|
||||
from django.test import RequestFactory, SimpleTestCase, TestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import translation
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.translation import gettext as _
|
||||
from django.views.debug import technical_500_response
|
||||
from django.views.decorators.debug import sensitive_variables
|
||||
|
||||
from .models.custom_user import (
|
||||
CustomUser,
|
||||
@ -412,6 +416,19 @@ class CustomUserCreationFormTest(TestDataMixin, TestCase):
|
||||
user = form.save(commit=True)
|
||||
self.assertSequenceEqual(user.orgs.all(), [organization])
|
||||
|
||||
def test_custom_form_with_non_required_password(self):
|
||||
class CustomUserCreationForm(BaseUserCreationForm):
|
||||
password1 = forms.CharField(required=False)
|
||||
password2 = forms.CharField(required=False)
|
||||
another_field = forms.CharField(required=True)
|
||||
|
||||
data = {
|
||||
"username": "testclientnew",
|
||||
"another_field": "Content",
|
||||
}
|
||||
form = CustomUserCreationForm(data)
|
||||
self.assertIs(form.is_valid(), True, form.errors)
|
||||
|
||||
|
||||
class UserCreationFormTest(BaseUserCreationFormTest):
|
||||
|
||||
@ -1671,3 +1688,50 @@ class AdminUserCreationFormTest(BaseUserCreationFormTest):
|
||||
u = form.save()
|
||||
self.assertEqual(u.username, data["username"])
|
||||
self.assertFalse(u.has_usable_password())
|
||||
|
||||
|
||||
class SensitiveVariablesTest(TestDataMixin, TestCase):
|
||||
@sensitive_variables("data")
|
||||
def test_passwords_marked_as_sensitive_in_admin_forms(self):
|
||||
data = {
|
||||
"password1": "passwordsensitive",
|
||||
"password2": "sensitivepassword",
|
||||
"usable_password": "true",
|
||||
}
|
||||
forms = [
|
||||
AdminUserCreationForm({**data, "username": "newusername"}),
|
||||
AdminPasswordChangeForm(self.u1, data),
|
||||
]
|
||||
|
||||
password1_fragment = """
|
||||
<td>password1</td>
|
||||
<td class="code"><pre>'********************'</pre></td>
|
||||
"""
|
||||
password2_fragment = """
|
||||
<td>password2</td>
|
||||
<td class="code"><pre>'********************'</pre></td>
|
||||
"""
|
||||
error = ValueError("Forced error")
|
||||
for form in forms:
|
||||
with self.subTest(form=form):
|
||||
with mock.patch.object(
|
||||
SetPasswordMixin, "validate_passwords", side_effect=error
|
||||
):
|
||||
try:
|
||||
form.is_valid()
|
||||
except ValueError:
|
||||
exc_info = sys.exc_info()
|
||||
else:
|
||||
self.fail("Form validation should have failed.")
|
||||
|
||||
response = technical_500_response(RequestFactory().get("/"), *exc_info)
|
||||
|
||||
self.assertNotContains(response, "sensitivepassword", status_code=500)
|
||||
self.assertNotContains(response, "passwordsensitive", status_code=500)
|
||||
self.assertContains(response, str(error), status_code=500)
|
||||
self.assertContains(
|
||||
response, password1_fragment, html=True, status_code=500
|
||||
)
|
||||
self.assertContains(
|
||||
response, password2_fragment, html=True, status_code=500
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user