From 92f6a713d3804a07925eeb9e1df265439e4148b5 Mon Sep 17 00:00:00 2001 From: antoliny0919 Date: Sun, 1 Dec 2024 10:33:37 +0900 Subject: [PATCH] Fixed #35693 -- Made password validators callable. --- django/contrib/auth/password_validation.py | 12 ++++++ docs/releases/5.2.txt | 4 ++ tests/auth_tests/test_validators.py | 50 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/django/contrib/auth/password_validation.py b/django/contrib/auth/password_validation.py index d24e69e0ce..5b660492e0 100644 --- a/django/contrib/auth/password_validation.py +++ b/django/contrib/auth/password_validation.py @@ -104,6 +104,9 @@ class MinimumLengthValidator: def __init__(self, min_length=8): self.min_length = min_length + def __call__(self, *args, **kwargs): + return self.validate(*args, kwargs) + def validate(self, password, user=None): if len(password) < self.min_length: raise ValidationError(self.get_error_message(), code="password_too_short") @@ -175,6 +178,9 @@ class UserAttributeSimilarityValidator: raise ValueError("max_similarity must be at least 0.1") self.max_similarity = max_similarity + def __call__(self, *args, **kwargs): + return self.validate(*args, **kwargs) + def validate(self, password, user=None): if not user: return @@ -241,6 +247,9 @@ class CommonPasswordValidator: with open(password_list_path) as f: self.passwords = {x.strip() for x in f} + def __call__(self, *args, **kwargs): + return self.validate(*args, **kwargs) + def validate(self, password, user=None): if password.lower().strip() in self.passwords: raise ValidationError( @@ -260,6 +269,9 @@ class NumericPasswordValidator: Validate that the password is not entirely numeric. """ + def __call__(self, *args, **kwargs): + return self.validate(*args, **kwargs) + def validate(self, password, user=None): if password.isdigit(): raise ValidationError( diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index 7af0b955f6..799fb4db85 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -91,6 +91,10 @@ Minor features now have a new method ``get_error_message()``, which can be overridden in subclasses to customize the error messages. +* The :ref:`password validator classes ` + now have a new method ``__call__()``, calling the password validator class + object will perform ``validate()`` method. + :mod:`django.contrib.contenttypes` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/auth_tests/test_validators.py b/tests/auth_tests/test_validators.py index d7e4968951..14d999ccf4 100644 --- a/tests/auth_tests/test_validators.py +++ b/tests/auth_tests/test_validators.py @@ -158,6 +158,19 @@ class MinimumLengthValidatorTest(SimpleTestCase): with self.assertRaisesMessage(ValidationError, expected_error % 3) as cm: CustomMinimumLengthValidator(min_length=3).validate("12") + def test_callable_validate(self): + expected_error = ( + "This password is too short. It must contain at least %d characters." + ) + + validator = MinimumLengthValidator() + self.assertIsNone(validator("12345678")) + + with self.assertRaises(ValidationError) as cm: + validator("1234567") + self.assertEqual(cm.exception.messages, [expected_error % 8]) + self.assertEqual(cm.exception.error_list[0].code, "password_too_short") + class UserAttributeSimilarityValidatorTest(TestCase): def test_validate(self): @@ -263,6 +276,24 @@ class UserAttributeSimilarityValidatorTest(TestCase): with self.assertRaisesMessage(ValidationError, expected_error): CustomUserAttributeSimilarityValidator().validate("testclient", user=user) + def test_callable_validate(self): + user = User.objects.create_user( + username="testclient", + password="password", + email="testclient@example.com", + first_name="Test", + last_name="Client", + ) + expected_error = "The password is too similar to the %s." + + validator = UserAttributeSimilarityValidator() + self.assertIsNone(validator("testclient")) + + with self.assertRaises(ValidationError) as cm: + validator("testclient", user=user) + self.assertEqual(cm.exception.messages, [expected_error % "username"]) + self.assertEqual(cm.exception.error_list[0].code, "password_too_similar") + class CommonPasswordValidatorTest(SimpleTestCase): def test_validate(self): @@ -307,6 +338,15 @@ class CommonPasswordValidatorTest(SimpleTestCase): with self.assertRaisesMessage(ValidationError, expected_error): CustomCommonPasswordValidator().validate("godzilla") + def test_callable_validate(self): + expected_error = "This password is too common." + validator = CommonPasswordValidator() + self.assertIsNone(validator("a-safe-password")) + + with self.assertRaises(ValidationError) as cm: + validator("godzilla") + self.assertEqual(cm.exception.messages, [expected_error]) + class NumericPasswordValidatorTest(SimpleTestCase): def test_validate(self): @@ -334,6 +374,16 @@ class NumericPasswordValidatorTest(SimpleTestCase): with self.assertRaisesMessage(ValidationError, expected_error): CustomNumericPasswordValidator().validate("42424242") + def test_callable_validate(self): + expected_error = "This password is entirely numeric." + validator = NumericPasswordValidator() + self.assertIsNone(validator("a-safe-password")) + + with self.assertRaises(ValidationError) as cm: + validator("42424242") + self.assertEqual(cm.exception.messages, [expected_error]) + self.assertEqual(cm.exception.error_list[0].code, "password_entirely_numeric") + class UsernameValidatorsTests(SimpleTestCase): def test_unicode_validator(self):