diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index d817ea8e94..e92f8f5e82 100644 --- a/django/conf/global_settings.py +++ b/django/conf/global_settings.py @@ -534,6 +534,8 @@ PASSWORD_HASHERS = [ 'django.contrib.auth.hashers.CryptPasswordHasher', ] +AUTH_PASSWORD_VALIDATORS = [] + ########### # SIGNING # ########### diff --git a/django/conf/project_template/project_name/settings.py b/django/conf/project_template/project_name/settings.py index 3eaf05cfc0..e58e229c8a 100644 --- a/django/conf/project_template/project_name/settings.py +++ b/django/conf/project_template/project_name/settings.py @@ -82,6 +82,25 @@ DATABASES = { } +# Password validation +# https://docs.djangoproject.com/en/{{ docs_version }}/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + # Internationalization # https://docs.djangoproject.com/en/{{ docs_version }}/topics/i18n/ diff --git a/django/contrib/auth/common-passwords.txt.gz b/django/contrib/auth/common-passwords.txt.gz new file mode 100644 index 0000000000..78d490df5a Binary files /dev/null and b/django/contrib/auth/common-passwords.txt.gz differ diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index daf46e4a81..0d7954a548 100644 --- a/django/contrib/auth/forms.py +++ b/django/contrib/auth/forms.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals from django import forms -from django.contrib.auth import authenticate, get_user_model +from django.contrib.auth import ( + authenticate, get_user_model, password_validation, +) from django.contrib.auth.hashers import ( UNUSABLE_PASSWORD_PREFIX, identify_hasher, ) @@ -263,7 +265,8 @@ class SetPasswordForm(forms.Form): 'password_mismatch': _("The two password fields didn't match."), } new_password1 = forms.CharField(label=_("New password"), - widget=forms.PasswordInput) + widget=forms.PasswordInput, + help_text=password_validation.password_validators_help_text_html()) new_password2 = forms.CharField(label=_("New password confirmation"), widget=forms.PasswordInput) @@ -280,10 +283,13 @@ class SetPasswordForm(forms.Form): self.error_messages['password_mismatch'], code='password_mismatch', ) + password_validation.validate_password(password2, self.user) return password2 def save(self, commit=True): - self.user.set_password(self.cleaned_data['new_password1']) + password = self.cleaned_data["new_password1"] + self.user.set_password(password) + password_validation.password_changed(password, self.user) if commit: self.user.save() return self.user @@ -327,6 +333,7 @@ class AdminPasswordChangeForm(forms.Form): password1 = forms.CharField( label=_("Password"), widget=forms.PasswordInput, + help_text=password_validation.password_validators_help_text_html(), ) password2 = forms.CharField( label=_("Password (again)"), @@ -347,13 +354,16 @@ class AdminPasswordChangeForm(forms.Form): self.error_messages['password_mismatch'], code='password_mismatch', ) + password_validation.validate_password(password2, self.user) return password2 def save(self, commit=True): """ Saves the new password. """ - self.user.set_password(self.cleaned_data["password1"]) + password = self.cleaned_data["password1"] + self.user.set_password(password) + password_validation.password_changed(password, self.user) if commit: self.user.save() return self.user diff --git a/django/contrib/auth/password_validation.py b/django/contrib/auth/password_validation.py new file mode 100644 index 0000000000..a44bf57875 --- /dev/null +++ b/django/contrib/auth/password_validation.py @@ -0,0 +1,174 @@ +from __future__ import unicode_literals + +import gzip +import os +import re +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.encoding import force_text +from django.utils.html import format_html +from django.utils.module_loading import import_string +from django.utils.six import string_types +from django.utils.translation import ugettext as _ + + +@lru_cache.lru_cache(maxsize=None) +def get_default_password_validators(): + return get_password_validators(settings.AUTH_PASSWORD_VALIDATORS) + + +def get_password_validators(validator_config): + validators = [] + for validator in validator_config: + try: + klass = import_string(validator['NAME']) + except ImportError: + msg = "The module in NAME could not be imported: %s. Check your AUTH_PASSWORD_VALIDATORS setting." + raise ImproperlyConfigured(msg % validator['NAME']) + validators.append(klass(**validator.get('OPTIONS', {}))) + + return validators + + +def validate_password(password, user=None, password_validators=None): + """ + Validate whether the password meets all validator requirements. + + If the password is valid, return ``None``. + If the password is invalid, raise ValidationError with all error messages. + """ + errors = [] + if password_validators is None: + password_validators = get_default_password_validators() + for validator in password_validators: + try: + validator.validate(password, user) + except ValidationError as error: + errors += error.messages + if errors: + raise ValidationError(errors) + + +def password_changed(password, user=None, password_validators=None): + """ + Inform all validators that have implemented a password_changed() method + that the password has been changed. + """ + if password_validators is None: + password_validators = get_default_password_validators() + for validator in password_validators: + password_changed = getattr(validator, 'password_changed', lambda *a: None) + password_changed(password, user) + + +def password_validators_help_texts(password_validators=None): + """ + Return a list of all help texts of all configured validators. + """ + help_texts = [] + if password_validators is None: + password_validators = get_default_password_validators() + for validator in password_validators: + help_texts.append(validator.get_help_text()) + return help_texts + + +def password_validators_help_text_html(password_validators=None): + """ + Return an HTML string with all help texts of all configured validators + in an