summaryrefslogtreecommitdiff
path: root/django/contrib/auth/password_validation.py
diff options
context:
space:
mode:
authorErik Romijn <eromijn@solidlinks.nl>2015-03-08 15:07:57 +0100
committerErik Romijn <eromijn@solidlinks.nl>2015-06-07 19:31:20 +0200
commit1daae25bdcd735151de394a5578c22257e3e5dc7 (patch)
tree5d03536fe9cf69bf0fcf1a1997db713ce52a880b /django/contrib/auth/password_validation.py
parentf4416b1a8b92e492707a6261b7a1132f8550457f (diff)
Fixed #16860 -- Added password validation to django.contrib.auth.
Diffstat (limited to 'django/contrib/auth/password_validation.py')
-rw-r--r--django/contrib/auth/password_validation.py174
1 files changed, 174 insertions, 0 deletions
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 <ul>.
+ """
+ help_texts = password_validators_help_texts(password_validators)
+ help_items = [format_html('<li>{}</li>', help_text) for help_text in help_texts]
+ return '<ul>%s</ul>' % ''.join(help_items)
+
+
+class MinimumLengthValidator(object):
+ """
+ Validate whether the password is of a minimum length.
+ """
+ def __init__(self, min_length=8):
+ self.min_length = min_length
+
+ 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})
+
+ def get_help_text(self):
+ return _("Your password must contain at least %(min_length)d characters.") % {'min_length': self.min_length}
+
+
+class UserAttributeSimilarityValidator(object):
+ """
+ Validate whether the password is sufficiently different from the user's
+ attributes.
+
+ If no specific attributes are provided, look at a sensible list of
+ defaults. Attributes that don't exist are ignored. Comparison is made to
+ not only the full attribute value, but also its components, so that, for
+ example, a password is validated against either part of an email address,
+ as well as the full address.
+ """
+ DEFAULT_USER_ATTRIBUTES = ('username', 'first_name', 'last_name', 'email')
+
+ def __init__(self, user_attributes=DEFAULT_USER_ATTRIBUTES, max_similarity=0.7):
+ self.user_attributes = user_attributes
+ self.max_similarity = max_similarity
+
+ def validate(self, password, user=None):
+ if not user:
+ return
+
+ for attribute_name in self.user_attributes:
+ value = getattr(user, attribute_name, None)
+ if not value or not isinstance(value, string_types):
+ continue
+ value_parts = re.split('\W+', value) + [value]
+ 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))
+
+ def get_help_text(self):
+ return _("Your password can't be too similar to your other personal information.")
+
+
+class CommonPasswordValidator(object):
+ """
+ Validate whether the password is a common password.
+
+ The password is rejected if it occurs in a provided list, which may be gzipped.
+ 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'
+
+ def __init__(self, password_list_path=DEFAULT_PASSWORD_LIST_PATH):
+ try:
+ common_passwords_lines = gzip.open(password_list_path).read().decode('utf-8').splitlines()
+ except IOError:
+ common_passwords_lines = open(password_list_path).readlines()
+ self.passwords = {p.strip() for p in common_passwords_lines}
+
+ def validate(self, password, user=None):
+ if password.lower().strip() in self.passwords:
+ raise ValidationError(_("This password is too common."))
+
+ def get_help_text(self):
+ return _("Your password can't be a commonly used password.")
+
+
+class NumericPasswordValidator(object):
+ """
+ Validate whether the password is alphanumeric.
+ """
+ def validate(self, password, user=None):
+ if password.isdigit():
+ raise ValidationError(_("This password is entirely numeric."))
+
+ def get_help_text(self):
+ return _("Your password can't be entirely numeric.")