diff options
| author | Erik Romijn <eromijn@solidlinks.nl> | 2015-03-08 15:07:57 +0100 |
|---|---|---|
| committer | Erik Romijn <eromijn@solidlinks.nl> | 2015-06-07 19:31:20 +0200 |
| commit | 1daae25bdcd735151de394a5578c22257e3e5dc7 (patch) | |
| tree | 5d03536fe9cf69bf0fcf1a1997db713ce52a880b /django/contrib/auth/password_validation.py | |
| parent | f4416b1a8b92e492707a6261b7a1132f8550457f (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.py | 174 |
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.") |
