Refs #16860 -- Minor edits and fixes to password validation.
This commit is contained in:
parent
a0047c6242
commit
55b3bd8468
|
@ -8,6 +8,7 @@ from difflib import SequenceMatcher
|
|||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||
from django.utils import lru_cache
|
||||
from django.utils._os import upath
|
||||
from django.utils.encoding import force_text
|
||||
from django.utils.html import format_html
|
||||
from django.utils.module_loading import import_string
|
||||
|
@ -47,7 +48,7 @@ def validate_password(password, user=None, password_validators=None):
|
|||
try:
|
||||
validator.validate(password, user)
|
||||
except ValidationError as error:
|
||||
errors += error.messages
|
||||
errors.append(error)
|
||||
if errors:
|
||||
raise ValidationError(errors)
|
||||
|
||||
|
@ -95,8 +96,11 @@ class MinimumLengthValidator(object):
|
|||
|
||||
def validate(self, password, user=None):
|
||||
if len(password) < self.min_length:
|
||||
msg = _("This password is too short. It must contain at least %(min_length)d characters.")
|
||||
raise ValidationError(msg % {'min_length': self.min_length})
|
||||
raise ValidationError(
|
||||
_("This password is too short. It must contain at least %(min_length)d characters."),
|
||||
code='password_too_short',
|
||||
params={'min_length': self.min_length},
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password must contain at least %(min_length)d characters.") % {'min_length': self.min_length}
|
||||
|
@ -131,7 +135,11 @@ class UserAttributeSimilarityValidator(object):
|
|||
for value_part in value_parts:
|
||||
if SequenceMatcher(a=password.lower(), b=value_part.lower()).quick_ratio() > self.max_similarity:
|
||||
verbose_name = force_text(user._meta.get_field(attribute_name).verbose_name)
|
||||
raise ValidationError(_("The password is too similar to the %s." % verbose_name))
|
||||
raise ValidationError(
|
||||
_("The password is too similar to the %(verbose_name)s."),
|
||||
code='password_too_similar',
|
||||
params={'verbose_name': verbose_name},
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password can't be too similar to your other personal information.")
|
||||
|
@ -145,7 +153,9 @@ class CommonPasswordValidator(object):
|
|||
The list Django ships with contains 1000 common passwords, created by Mark Burnett:
|
||||
https://xato.net/passwords/more-top-worst-passwords/
|
||||
"""
|
||||
DEFAULT_PASSWORD_LIST_PATH = os.path.dirname(os.path.realpath(__file__)) + '/common-passwords.txt.gz'
|
||||
DEFAULT_PASSWORD_LIST_PATH = os.path.join(
|
||||
os.path.dirname(os.path.realpath(upath(__file__))), 'common-passwords.txt.gz'
|
||||
)
|
||||
|
||||
def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
|
||||
try:
|
||||
|
@ -156,7 +166,10 @@ class CommonPasswordValidator(object):
|
|||
|
||||
def validate(self, password, user=None):
|
||||
if password.lower().strip() in self.passwords:
|
||||
raise ValidationError(_("This password is too common."))
|
||||
raise ValidationError(
|
||||
_("This password is too common."),
|
||||
code='password_too_common',
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password can't be a commonly used password.")
|
||||
|
@ -168,7 +181,10 @@ class NumericPasswordValidator(object):
|
|||
"""
|
||||
def validate(self, password, user=None):
|
||||
if password.isdigit():
|
||||
raise ValidationError(_("This password is entirely numeric."))
|
||||
raise ValidationError(
|
||||
_("This password is entirely numeric."),
|
||||
code='password_entirely_numeric',
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password can't be entirely numeric.")
|
||||
|
|
|
@ -2774,11 +2774,11 @@ AUTH_PASSWORD_VALIDATORS
|
|||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
Default: ``[]``
|
||||
Default: ``[]`` (Empty list)
|
||||
|
||||
Sets the validators that are used to check the strength of user's passwords.
|
||||
See :ref:`password-validation` for more details.
|
||||
By default, no validation is performed and all passwords are accepted.
|
||||
The list of validators that are used to check the strength of user's passwords.
|
||||
See :ref:`password-validation` for more details. By default, no validation is
|
||||
performed and all passwords are accepted.
|
||||
|
||||
.. _settings-messages:
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ What's new in Django 1.9
|
|||
Password validation
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Django now offers password validation, to help prevent the usage of weak
|
||||
Django now offers password validation to help prevent the usage of weak
|
||||
passwords by users. The validation is integrated in the included password
|
||||
change and reset forms and is simple to integrate in any other code.
|
||||
Validation is performed by one or more validators, configured in the new
|
||||
|
@ -36,10 +36,10 @@ Validation is performed by one or more validators, configured in the new
|
|||
|
||||
Four validators are included in Django, which can enforce a minimum length,
|
||||
compare the password to the user's attributes like their name, ensure
|
||||
passwords aren't entirely numeric or check against an included list of common
|
||||
passwords aren't entirely numeric, or check against an included list of common
|
||||
passwords. You can combine multiple validators, and some validators have
|
||||
custom configuration options. For example, you can choose to provide a custom
|
||||
list of common passwords. Each validator provides a help text to explain their
|
||||
list of common passwords. Each validator provides a help text to explain its
|
||||
requirements to the user.
|
||||
|
||||
By default, no validation is performed and all passwords are accepted, so if
|
||||
|
|
|
@ -242,6 +242,10 @@ from the ``User`` model.
|
|||
Password validation
|
||||
===================
|
||||
|
||||
.. module:: django.contrib.auth.password_validation
|
||||
|
||||
.. versionadded:: 1.9
|
||||
|
||||
Users often choose poor passwords. To help mitigate this problem, Django
|
||||
offers pluggable password validation. You can configure multiple password
|
||||
validators at the same time. A few validators are included in Django, but it's
|
||||
|
@ -254,14 +258,14 @@ Validators can also have optional settings to fine tune their behavior.
|
|||
|
||||
Validation is controlled by the :setting:`AUTH_PASSWORD_VALIDATORS` setting.
|
||||
By default, validators are used in the forms to reset or change passwords.
|
||||
The default for setting is an empty list, which means no validators are
|
||||
The default for the setting is an empty list, which means no validators are
|
||||
applied. In new projects created with the default :djadmin:`startproject`
|
||||
template, a simple set of validators is enabled.
|
||||
|
||||
.. note::
|
||||
|
||||
Password validation can prevent the use of many types of weak passwords.
|
||||
However, the fact that a password passes all the validators, doesn't
|
||||
However, the fact that a password passes all the validators doesn't
|
||||
guarantee that it is a strong password. There are many factors that can
|
||||
weaken a password that are not detectable by even the most advanced
|
||||
password validators.
|
||||
|
@ -344,7 +348,7 @@ Django includes four validators:
|
|||
`Mark Burnett <https://xato.net/passwords/more-top-worst-passwords/>`_.
|
||||
|
||||
The ``password_list_path`` can be set to the path of a custom file of
|
||||
common passwords. This file should contain one password per line, and
|
||||
common passwords. This file should contain one password per line and
|
||||
may be plain text or gzipped.
|
||||
|
||||
.. class:: NumericPasswordValidator()
|
||||
|
@ -354,8 +358,6 @@ Django includes four validators:
|
|||
Integrating validation
|
||||
-----------------------
|
||||
|
||||
.. module:: django.contrib.auth.password_validation
|
||||
|
||||
There are a few functions in ``django.contrib.auth.password_validation`` that
|
||||
you can call from your own forms or other code to integrate password
|
||||
validation. This can be useful if you use custom forms for password setting,
|
||||
|
@ -368,14 +370,14 @@ or if you have API calls that allow passwords to be set, for example.
|
|||
:exc:`~django.core.exceptions.ValidationError` with all the error messages
|
||||
from the validators.
|
||||
|
||||
The user object is optional: if it's not provided, some validators may not
|
||||
be able to perform any validation and will accept any password.
|
||||
The ``user`` object is optional: if it's not provided, some validators may
|
||||
not be able to perform any validation and will accept any password.
|
||||
|
||||
.. function:: password_changed(password, user=None, password_validators=None)
|
||||
|
||||
Informs all validators that the password has been changed. This can be used
|
||||
by some validators, e.g. a validator that prevents password reuse. This
|
||||
should be called once the password has been successfully changed.
|
||||
by validators such as one that prevents password reuse. This should be
|
||||
called once the password has been successfully changed.
|
||||
|
||||
.. function:: password_validators_help_texts(password_validators=None)
|
||||
|
||||
|
@ -440,11 +442,17 @@ Here's a basic example of a validator, with one optional setting::
|
|||
|
||||
def validate(self, password, user=None):
|
||||
if len(password) < self.min_length:
|
||||
raise ValidationError(_("This password is too short."))
|
||||
raise ValidationError(
|
||||
_("This password must contain at least %(min_length)d characters."),
|
||||
code='password_too_short',
|
||||
params={'min_length': self.min_length},
|
||||
)
|
||||
|
||||
def get_help_text(self):
|
||||
return _("Your password must contain at least %(min_length)d characters.")
|
||||
return _(
|
||||
"Your password must contain at least %(min_length)d characters."
|
||||
% {'min_length': self.min_length}
|
||||
)
|
||||
|
||||
You can also implement ``password_changed(password, user=None``), which will
|
||||
be called after a successful password change. That can be used to prevent
|
||||
|
|
|
@ -278,8 +278,11 @@ class SetPasswordFormTest(TestDataMixin, TestCase):
|
|||
form = SetPasswordForm(user, data)
|
||||
self.assertFalse(form.is_valid())
|
||||
self.assertEqual(len(form["new_password2"].errors), 2)
|
||||
self.assertTrue('The password is too similar to the username.' in form["new_password2"].errors)
|
||||
self.assertTrue('This password is too short. It must contain at least 12 characters.' in form["new_password2"].errors)
|
||||
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
|
||||
)
|
||||
|
||||
|
||||
@override_settings(USE_TZ=False, PASSWORD_HASHERS=['django.contrib.auth.hashers.SHA1PasswordHasher'])
|
||||
|
|
|
@ -12,6 +12,7 @@ from django.contrib.auth.password_validation import (
|
|||
)
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase, override_settings
|
||||
from django.utils._os import upath
|
||||
|
||||
|
||||
@override_settings(AUTH_PASSWORD_VALIDATORS=[
|
||||
|
@ -43,10 +44,12 @@ class PasswordValidationTest(TestCase):
|
|||
with self.assertRaises(ValidationError, args=['This password is too short.']) as cm:
|
||||
validate_password('django4242')
|
||||
self.assertEqual(cm.exception.messages, [msg_too_short])
|
||||
self.assertEqual(cm.exception.error_list[0].code, 'password_too_short')
|
||||
|
||||
with self.assertRaises(ValidationError) as cm:
|
||||
validate_password('password')
|
||||
self.assertEqual(cm.exception.messages, ['This password is too common.', msg_too_short])
|
||||
self.assertEqual(cm.exception.error_list[0].code, 'password_too_common')
|
||||
|
||||
self.assertIsNone(validate_password('password', password_validators=[]))
|
||||
|
||||
|
@ -56,14 +59,14 @@ class PasswordValidationTest(TestCase):
|
|||
def test_password_validators_help_texts(self):
|
||||
help_texts = password_validators_help_texts()
|
||||
self.assertEqual(len(help_texts), 2)
|
||||
self.assertTrue('12 characters' in help_texts[1])
|
||||
self.assertIn('12 characters', help_texts[1])
|
||||
|
||||
self.assertEqual(password_validators_help_texts(password_validators=[]), [])
|
||||
|
||||
def test_password_validators_help_text_html(self):
|
||||
help_text = password_validators_help_text_html()
|
||||
self.assertEqual(help_text.count('<li>'), 2)
|
||||
self.assertTrue('12 characters' in help_text)
|
||||
self.assertIn('12 characters', help_text)
|
||||
|
||||
|
||||
class MinimumLengthValidatorTest(TestCase):
|
||||
|
@ -75,6 +78,7 @@ class MinimumLengthValidatorTest(TestCase):
|
|||
with self.assertRaises(ValidationError) as cm:
|
||||
MinimumLengthValidator().validate('1234567')
|
||||
self.assertEqual(cm.exception.messages, [expected_error % 8])
|
||||
self.assertEqual(cm.exception.error_list[0].code, 'password_too_short')
|
||||
|
||||
with self.assertRaises(ValidationError) as cm:
|
||||
MinimumLengthValidator(min_length=3).validate('12')
|
||||
|
@ -100,13 +104,17 @@ class UserAttributeSimilarityValidatorTest(TestCase):
|
|||
with self.assertRaises(ValidationError) as cm:
|
||||
UserAttributeSimilarityValidator().validate('testclient', user=user),
|
||||
self.assertEqual(cm.exception.messages, [expected_error % "username"])
|
||||
self.assertEqual(cm.exception.error_list[0].code, 'password_too_similar')
|
||||
|
||||
with self.assertRaises(ValidationError) as cm:
|
||||
UserAttributeSimilarityValidator().validate('example.com', user=user),
|
||||
self.assertEqual(cm.exception.messages, [expected_error % "email address"])
|
||||
|
||||
with self.assertRaises(ValidationError) as cm:
|
||||
UserAttributeSimilarityValidator(user_attributes=['first_name'], max_similarity=0.3).validate('testclient', user=user),
|
||||
UserAttributeSimilarityValidator(
|
||||
user_attributes=['first_name'],
|
||||
max_similarity=0.3,
|
||||
).validate('testclient', user=user)
|
||||
self.assertEqual(cm.exception.messages, [expected_error % "first name"])
|
||||
|
||||
self.assertIsNone(
|
||||
|
@ -130,7 +138,7 @@ class CommonPasswordValidatorTest(TestCase):
|
|||
self.assertEqual(cm.exception.messages, [expected_error])
|
||||
|
||||
def test_validate_custom_list(self):
|
||||
path = os.path.dirname(os.path.realpath(__file__)) + '/common-passwords-custom.txt'
|
||||
path = os.path.join(os.path.dirname(os.path.realpath(upath(__file__))), 'common-passwords-custom.txt')
|
||||
validator = CommonPasswordValidator(password_list_path=path)
|
||||
expected_error = "This password is too common."
|
||||
self.assertIsNone(validator.validate('a-safe-password'))
|
||||
|
@ -138,6 +146,7 @@ class CommonPasswordValidatorTest(TestCase):
|
|||
with self.assertRaises(ValidationError) as cm:
|
||||
validator.validate('from-my-custom-list')
|
||||
self.assertEqual(cm.exception.messages, [expected_error])
|
||||
self.assertEqual(cm.exception.error_list[0].code, 'password_too_common')
|
||||
|
||||
def test_help_text(self):
|
||||
self.assertEqual(
|
||||
|
@ -154,6 +163,7 @@ class NumericPasswordValidatorTest(TestCase):
|
|||
with self.assertRaises(ValidationError) as cm:
|
||||
NumericPasswordValidator().validate('42424242')
|
||||
self.assertEqual(cm.exception.messages, [expected_error])
|
||||
self.assertEqual(cm.exception.error_list[0].code, 'password_entirely_numeric')
|
||||
|
||||
def test_help_text(self):
|
||||
self.assertEqual(
|
||||
|
|
Loading…
Reference in New Issue