summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Schilling <mail@paulschilling.de>2022-09-24 15:26:14 +0100
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2022-12-29 09:42:22 +0100
commit298d02a77a69321af8c0023df3250663e9d1362d (patch)
tree087ab161aeb1765d38d31f39ae004fc3d76f20c0
parent1833eb3f3e3bda5052637f1a51a27fa1b11b6871 (diff)
Fixed #25617 -- Added case-insensitive unique username validation in UserCreationForm.
Co-Authored-By: Neven Mundar <nmundar@gmail.com>
-rw-r--r--django/contrib/auth/forms.py17
-rw-r--r--docs/releases/4.2.txt7
-rw-r--r--docs/topics/auth/default.txt15
-rw-r--r--tests/auth_tests/test_forms.py68
4 files changed, 79 insertions, 28 deletions
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py
index 63420af94e..0376d17709 100644
--- a/django/contrib/auth/forms.py
+++ b/django/contrib/auth/forms.py
@@ -81,7 +81,7 @@ class UsernameField(forms.CharField):
}
-class UserCreationForm(forms.ModelForm):
+class BaseUserCreationForm(forms.ModelForm):
"""
A form that creates a user, with no privileges, from the given username and
password.
@@ -146,6 +146,21 @@ class UserCreationForm(forms.ModelForm):
return user
+class UserCreationForm(BaseUserCreationForm):
+ error_messages = {
+ **BaseUserCreationForm.error_messages,
+ "unique": _("A user with that username already exists."),
+ }
+
+ def clean_username(self):
+ """Reject usernames that differ only in case."""
+ username = self.cleaned_data.get("username")
+ if username and User.objects.filter(username__iexact=username).exists():
+ raise forms.ValidationError(self.error_messages["unique"], code="unique")
+ else:
+ return username
+
+
class UserChangeForm(forms.ModelForm):
password = ReadOnlyPasswordHashField(
label=_("Password"),
diff --git a/docs/releases/4.2.txt b/docs/releases/4.2.txt
index b5a7e61f6f..826545f444 100644
--- a/docs/releases/4.2.txt
+++ b/docs/releases/4.2.txt
@@ -127,6 +127,9 @@ Minor features
* :class:`~django.contrib.auth.forms.UserCreationForm` now saves many-to-many
form fields for a custom user model.
+* The new :class:`~django.contrib.auth.forms.BaseUserCreationForm` is now the
+ recommended base class for customizing the user creation form.
+
:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -484,6 +487,10 @@ Miscellaneous
* The minimum supported version of ``asgiref`` is increased from 3.5.2 to
3.6.0.
+* :class:`~django.contrib.auth.forms.UserCreationForm` now rejects usernames
+ that differ only in case. If you need the previous behavior, use
+ :class:`~django.contrib.auth.forms.BaseUserCreationForm` instead.
+
.. _deprecated-features-4.2:
Features deprecated in 4.2
diff --git a/docs/topics/auth/default.txt b/docs/topics/auth/default.txt
index 549430aaad..038f2b8eaf 100644
--- a/docs/topics/auth/default.txt
+++ b/docs/topics/auth/default.txt
@@ -1654,9 +1654,12 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
A form used in the admin interface to change a user's information and
permissions.
-.. class:: UserCreationForm
+.. class:: BaseUserCreationForm
+
+ .. versionadded:: 4.2
- A :class:`~django.forms.ModelForm` for creating a new user.
+ A :class:`~django.forms.ModelForm` for creating a new user. This is the
+ recommended base class if you need to customize the user creation form.
It has three fields: ``username`` (from the user model), ``password1``,
and ``password2``. It verifies that ``password1`` and ``password2`` match,
@@ -1665,11 +1668,19 @@ provides several built-in forms located in :mod:`django.contrib.auth.forms`:
sets the user's password using
:meth:`~django.contrib.auth.models.User.set_password()`.
+.. class:: UserCreationForm
+
+ Inherits from :class:`BaseUserCreationForm`. To help prevent confusion with
+ similar usernames, the form doesn't allow usernames that differ only in
+ case.
+
.. versionchanged:: 4.2
In older versions, :class:`UserCreationForm` didn't save many-to-many
form fields for a custom user model.
+ In older versions, usernames that differ only in case are allowed.
+
.. currentmodule:: django.contrib.auth
Authentication data in templates
diff --git a/tests/auth_tests/test_forms.py b/tests/auth_tests/test_forms.py
index 1aee923bb9..c3ce1f570f 100644
--- a/tests/auth_tests/test_forms.py
+++ b/tests/auth_tests/test_forms.py
@@ -6,6 +6,7 @@ from unittest import mock
from django.contrib.auth.forms import (
AdminPasswordChangeForm,
AuthenticationForm,
+ BaseUserCreationForm,
PasswordChangeForm,
PasswordResetForm,
ReadOnlyPasswordHashField,
@@ -54,14 +55,14 @@ class TestDataMixin:
cls.u6 = User.objects.create(username="unknown_password", password="foo$bar")
-class UserCreationFormTest(TestDataMixin, TestCase):
+class BaseUserCreationFormTest(TestDataMixin, TestCase):
def test_user_already_exists(self):
data = {
"username": "testclient",
"password1": "test123",
"password2": "test123",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(
form["username"].errors,
@@ -74,7 +75,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "test123",
"password2": "test123",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertFalse(form.is_valid())
validator = next(
v
@@ -90,7 +91,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "test123",
"password2": "test",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(
form["password2"].errors, [str(form.error_messages["password_mismatch"])]
@@ -99,7 +100,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
def test_both_passwords(self):
# One (or both) passwords weren't given
data = {"username": "jsmith"}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
required_error = [str(Field.default_error_messages["required"])]
self.assertFalse(form.is_valid())
self.assertEqual(form["password1"].errors, required_error)
@@ -119,7 +120,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "test123",
"password2": "test123",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertTrue(form.is_valid())
form.save(commit=False)
self.assertEqual(password_changed.call_count, 0)
@@ -133,7 +134,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "test123",
"password2": "test123",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertTrue(form.is_valid())
u = form.save()
self.assertEqual(u.username, "宝")
@@ -147,7 +148,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "pwd2",
"password2": "pwd2",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertTrue(form.is_valid())
user = form.save()
self.assertNotEqual(user.username, ohm_username)
@@ -168,7 +169,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "pwd2",
"password2": "pwd2",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(
form.errors["username"], ["A user with that username already exists."]
@@ -198,7 +199,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": "testclient",
"password2": "testclient",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertFalse(form.is_valid())
self.assertEqual(len(form["password2"].errors), 2)
self.assertIn(
@@ -210,8 +211,8 @@ class UserCreationFormTest(TestDataMixin, TestCase):
)
def test_custom_form(self):
- class CustomUserCreationForm(UserCreationForm):
- class Meta(UserCreationForm.Meta):
+ class CustomUserCreationForm(BaseUserCreationForm):
+ class Meta(BaseUserCreationForm.Meta):
model = ExtensionUser
fields = UserCreationForm.Meta.fields + ("date_of_birth",)
@@ -225,8 +226,8 @@ class UserCreationFormTest(TestDataMixin, TestCase):
self.assertTrue(form.is_valid())
def test_custom_form_with_different_username_field(self):
- class CustomUserCreationForm(UserCreationForm):
- class Meta(UserCreationForm.Meta):
+ class CustomUserCreationForm(BaseUserCreationForm):
+ class Meta(BaseUserCreationForm.Meta):
model = CustomUser
fields = ("email", "date_of_birth")
@@ -240,8 +241,8 @@ class UserCreationFormTest(TestDataMixin, TestCase):
self.assertTrue(form.is_valid())
def test_custom_form_hidden_username_field(self):
- class CustomUserCreationForm(UserCreationForm):
- class Meta(UserCreationForm.Meta):
+ class CustomUserCreationForm(BaseUserCreationForm):
+ class Meta(BaseUserCreationForm.Meta):
model = CustomUserWithoutIsActiveField
fields = ("email",) # without USERNAME_FIELD
@@ -254,8 +255,8 @@ class UserCreationFormTest(TestDataMixin, TestCase):
self.assertTrue(form.is_valid())
def test_custom_form_saves_many_to_many_field(self):
- class CustomUserCreationForm(UserCreationForm):
- class Meta(UserCreationForm.Meta):
+ class CustomUserCreationForm(BaseUserCreationForm):
+ class Meta(BaseUserCreationForm.Meta):
model = CustomUserWithM2M
fields = UserCreationForm.Meta.fields + ("orgs",)
@@ -278,7 +279,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
"password1": " testpassword ",
"password2": " testpassword ",
}
- form = UserCreationForm(data)
+ form = BaseUserCreationForm(data)
self.assertTrue(form.is_valid())
self.assertEqual(form.cleaned_data["password1"], data["password1"])
self.assertEqual(form.cleaned_data["password2"], data["password2"])
@@ -294,7 +295,7 @@ class UserCreationFormTest(TestDataMixin, TestCase):
]
)
def test_password_help_text(self):
- form = UserCreationForm()
+ form = BaseUserCreationForm()
self.assertEqual(
form.fields["password1"].help_text,
"<ul><li>"
@@ -313,10 +314,12 @@ class UserCreationFormTest(TestDataMixin, TestCase):
]
)
def test_user_create_form_validates_password_with_all_data(self):
- """UserCreationForm password validation uses all of the form's data."""
+ """
+ BaseUserCreationForm password validation uses all of the form's data.
+ """
- class CustomUserCreationForm(UserCreationForm):
- class Meta(UserCreationForm.Meta):
+ class CustomUserCreationForm(BaseUserCreationForm):
+ class Meta(BaseUserCreationForm.Meta):
model = User
fields = ("username", "email", "first_name", "last_name")
@@ -336,13 +339,13 @@ class UserCreationFormTest(TestDataMixin, TestCase):
)
def test_username_field_autocapitalize_none(self):
- form = UserCreationForm()
+ form = BaseUserCreationForm()
self.assertEqual(
form.fields["username"].widget.attrs.get("autocapitalize"), "none"
)
def test_html_autocomplete_attributes(self):
- form = UserCreationForm()
+ form = BaseUserCreationForm()
tests = (
("username", "username"),
("password1", "new-password"),
@@ -355,6 +358,21 @@ class UserCreationFormTest(TestDataMixin, TestCase):
)
+class UserCreationFormTest(TestDataMixin, TestCase):
+ def test_case_insensitive_username(self):
+ data = {
+ "username": "TeStClIeNt",
+ "password1": "test123",
+ "password2": "test123",
+ }
+ form = UserCreationForm(data)
+ self.assertFalse(form.is_valid())
+ self.assertEqual(
+ form["username"].errors,
+ ["A user with that username already exists."],
+ )
+
+
# To verify that the login form rejects inactive users, use an authentication
# backend that allows them.
@override_settings(