Files
edx-platform/common/djangoapps/util/password_policy_validators.py
Dillon Dumesnil deb758c808 Added in tests for the new password validation. Fixed old tests that
relied on the old configuration values and old way of validating
passwords. Also improved registration page by always showing error
messages rather than hiding them on leaving the field.
2018-10-10 11:06:30 -04:00

479 lines
17 KiB
Python

"""
This file exposes a number of password validators which can be optionally added to
account creation
"""
from __future__ import unicode_literals
import logging
import unicodedata
from django.conf import settings
from django.contrib.auth.password_validation import (
get_default_password_validators,
validate_password as django_validate_password,
MinimumLengthValidator as DjangoMinimumLengthValidator,
)
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _, ungettext
from six import text_type
log = logging.getLogger(__name__)
# The following constant contains the assumption that the max password length will never exceed 5000
# characters. The point of this restriction is to restrict the login page password field to prevent
# any sort of attacks involving sending massive passwords.
DEFAULT_MAX_PASSWORD_LENGTH = 5000
def create_validator_config(name, options={}):
"""
This function is meant to be used for testing purposes to create validators
easily. It returns a validator config of the form:
{
"NAME": "util.password_policy_validators.SymbolValidator",
"OPTIONS": {"min_symbol": 1}
}
Parameters:
name (str): the path name to the validator class to instantiate
options (dict): The dictionary of options to pass in to the validator.
These are used to initialize the validator with parameters.
If undefined, the default parameters will be used.
Returns:
Dictionary containing the NAME and OPTIONS for the validator. These will
be used to instantiate an instance of the validator using Django.
"""
if options:
return {'NAME': name, 'OPTIONS': options}
return {'NAME': name}
def password_validators_instruction_texts():
"""
Return a string of instruction texts of all configured validators.
Expects at least the MinimumLengthValidator to be defined.
"""
complexity_instructions = []
# For clarity in the printed instructions, the minimum length instruction
# is separated from the complexity instructions.
length_instruction = ''
password_validators = get_default_password_validators()
for validator in password_validators:
if hasattr(validator, 'get_instruction_text'):
text = validator.get_instruction_text()
if isinstance(validator, MinimumLengthValidator):
length_instruction = text
else:
complexity_instructions.append(text)
if complexity_instructions:
return _('Your password must contain {length_instruction}, including {complexity_instructions}.').format(
length_instruction=length_instruction,
complexity_instructions=' & '.join(complexity_instructions)
)
else:
return _('Your password must contain {length_instruction}.'.format(length_instruction=length_instruction))
def password_validators_restrictions():
"""
Return a dictionary of complexity restrictions to be used by mobile users on
the registration form
"""
password_validators = get_default_password_validators()
complexity_restrictions = dict(validator.get_restriction()
for validator in password_validators
if hasattr(validator, 'get_restriction')
)
return complexity_restrictions
def validate_password(password, user=None):
"""
EdX's custom password validator for passwords. This function performs the
following functions:
1) Converts the password to unicode if it is not already
2) Calls Django's validate_password method. This calls the validate function
in all validators specified in AUTH_PASSWORD_VALIDATORS configuration.
Parameters:
password (str or unicode): the user's password to be validated
user (django.contrib.auth.models.User): The user object to use for validating
the given password against the username and/or email.
Returns:
None
Raises:
ValidationError if unable to convert password to utf8 or if any of the
password validators fail.
"""
if not isinstance(password, text_type):
try:
# some checks rely on unicode semantics (e.g. length)
password = text_type(password, encoding='utf8')
except UnicodeDecodeError:
# no reason to get into weeds
raise ValidationError([_('Invalid password.')])
django_validate_password(password, user)
def _validate_condition(password, fn, min_count):
"""
Validates the password using the given function. This is performed by
iterating through each character in the password and counting up the number
of characters that satisfy the function.
Parameters:
password (str): the password
fn: the function to be tested against the string.
min_count (int): the minimum number of characters that must satisfy the function
Return:
True if valid_count >= min_count, else False
"""
valid_count = len([c for c in password if fn(c)])
return valid_count >= min_count
class MinimumLengthValidator(DjangoMinimumLengthValidator):
def get_instruction_text(self):
return ungettext(
'at least %(min_length)d character',
'at least %(min_length)d characters',
self.min_length
) % {'min_length': self.min_length}
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_length', self.min_length
class MaximumLengthValidator(object):
"""
Validate whether the password is shorter than a maximum length.
Parameters:
max_length (int): the maximum number of characters to require in the password.
"""
def __init__(self, max_length=75):
self.max_length = max_length
def validate(self, password, user=None):
if len(password) > self.max_length:
raise ValidationError(
ungettext(
'This password is too long. It must contain no more than %(max_length)d character.',
'This password is too long. It must contain no more than %(max_length)d characters.',
self.max_length
),
code='password_too_long',
params={'max_length': self.max_length},
)
def get_help_text(self):
return ungettext(
'Your password must contain no more than %(max_length)d character.',
'Your password must contain no more than %(max_length)d characters.',
self.max_length
) % {'max_length': self.max_length}
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'max_length', self.max_length
class AlphabeticValidator(object):
"""
Validate whether the password contains at least min_alphabetic letters.
Parameters:
min_alphabetic (int): the minimum number of alphabetic characters to require
in the password. Must be >= 0.
"""
def __init__(self, min_alphabetic=0):
self.min_alphabetic = min_alphabetic
def validate(self, password, user=None):
if _validate_condition(password, lambda c: c.isalpha(), self.min_alphabetic):
return
raise ValidationError(
ungettext(
'This password must contain at least %(min_alphabetic)d letter.',
'This password must contain at least %(min_alphabetic)d letters.',
self.min_alphabetic
),
code='too_few_alphabetic_char',
params={'min_alphabetic': self.min_alphabetic},
)
def get_help_text(self):
return ungettext(
'Your password must contain at least %(min_alphabetic)d letter.',
'Your password must contain at least %(min_alphabetic)d letters.',
self.min_alphabetic
) % {'min_alphabetic': self.min_alphabetic}
def get_instruction_text(self):
if self.min_alphabetic > 0:
return ungettext(
'%(num)d letter',
'%(num)d letters',
self.min_alphabetic
) % {'num': self.min_alphabetic}
else:
return ''
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_alphabetic', self.min_alphabetic
class NumericValidator(object):
"""
Validate whether the password contains at least min_numeric numbers.
Parameters:
min_numeric (int): the minimum number of numeric characters to require
in the password. Must be >= 0.
"""
def __init__(self, min_numeric=0):
self.min_numeric = min_numeric
def validate(self, password, user=None):
if _validate_condition(password, lambda c: c.isnumeric(), self.min_numeric):
return
raise ValidationError(
ungettext(
'This password must contain at least %(min_numeric)d number.',
'This password must contain at least %(min_numeric)d numbers.',
self.min_numeric
),
code='too_few_numeric_char',
params={'min_numeric': self.min_numeric},
)
def get_help_text(self):
return ungettext(
"Your password must contain at least %(min_numeric)d number.",
"Your password must contain at least %(min_numeric)d numbers.",
self.min_numeric
) % {'min_numeric': self.min_numeric}
def get_instruction_text(self):
if self.min_numeric > 0:
return ungettext(
'%(num)d number',
'%(num)d numbers',
self.min_numeric
) % {'num': self.min_numeric}
else:
return ''
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_numeric', self.min_numeric
class UppercaseValidator(object):
"""
Validate whether the password contains at least min_upper uppercase letters.
Parameters:
min_upper (int): the minimum number of uppercase characters to require
in the password. Must be >= 0.
"""
def __init__(self, min_upper=0):
self.min_upper = min_upper
def validate(self, password, user=None):
if _validate_condition(password, lambda c: c.isupper(), self.min_upper):
return
raise ValidationError(
ungettext(
'This password must contain at least %(min_upper)d uppercase letter.',
'This password must contain at least %(min_upper)d uppercase letters.',
self.min_upper
),
code='too_few_uppercase_char',
params={'min_upper': self.min_upper},
)
def get_help_text(self):
return ungettext(
"Your password must contain at least %(min_upper)d uppercase letter.",
"Your password must contain at least %(min_upper)d uppercase letters.",
self.min_upper
) % {'min_upper': self.min_upper}
def get_instruction_text(self):
if self.min_upper > 0:
return ungettext(
'%(num)d uppercase letter',
'%(num)d uppercase letters',
self.min_upper
) % {'num': self.min_upper}
else:
return ''
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_upper', self.min_upper
class LowercaseValidator(object):
"""
Validate whether the password contains at least min_lower lowercase letters.
Parameters:
min_lower (int): the minimum number of lowercase characters to require
in the password. Must be >= 0.
"""
def __init__(self, min_lower=0):
self.min_lower = min_lower
def validate(self, password, user=None):
if _validate_condition(password, lambda c: c.islower(), self.min_lower):
return
raise ValidationError(
ungettext(
'This password must contain at least %(min_lower)d lowercase letter.',
'This password must contain at least %(min_lower)d lowercase letters.',
self.min_lower
),
code='too_few_lowercase_char',
params={'min_lower': self.min_lower},
)
def get_help_text(self):
return ungettext(
"Your password must contain at least %(min_lower)d lowercase letter.",
"Your password must contain at least %(min_lower)d lowercase letters.",
self.min_lower
) % {'min_lower': self.min_lower}
def get_instruction_text(self):
if self.min_lower > 0:
return ungettext(
'%(num)d lowercase letter',
'%(num)d lowercase letters',
self.min_lower
) % {'num': self.min_lower}
else:
return ''
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_lower', self.min_lower
class PunctuationValidator(object):
"""
Validate whether the password contains at least min_punctuation punctuation marks
as defined by unicode categories.
Parameters:
min_punctuation (int): the minimum number of punctuation marks to require
in the password. Must be >= 0.
"""
def __init__(self, min_punctuation=0):
self.min_punctuation = min_punctuation
def validate(self, password, user=None):
if _validate_condition(password, lambda c: 'P' in unicodedata.category(c), self.min_punctuation):
return
raise ValidationError(
ungettext(
'This password must contain at least %(min_punctuation)d punctuation mark.',
'This password must contain at least %(min_punctuation)d punctuation marks.',
self.min_punctuation
),
code='too_few_punctuation_characters',
params={'min_punctuation': self.min_punctuation},
)
def get_help_text(self):
return ungettext(
"Your password must contain at least %(min_punctuation)d punctuation mark.",
"Your password must contain at least %(min_punctuation)d punctuation marks.",
self.min_punctuation
) % {'min_punctuation': self.min_punctuation}
def get_instruction_text(self):
if self.min_punctuation > 0:
return ungettext(
'%(num)d punctuation mark',
'%(num)d punctuation marks',
self.min_punctuation
) % {'num': self.min_punctuation}
else:
return ''
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_punctuation', self.min_punctuation
class SymbolValidator(object):
"""
Validate whether the password contains at least min_symbol symbols as defined by unicode categories.
Parameters:
min_symbol (int): the minimum number of symbols to require
in the password. Must be >= 0.
"""
def __init__(self, min_symbol=0):
self.min_symbol = min_symbol
def validate(self, password, user=None):
if _validate_condition(password, lambda c: 'S' in unicodedata.category(c), self.min_symbol):
return
raise ValidationError(
ungettext(
'This password must contain at least %(min_symbol)d symbol.',
'This password must contain at least %(min_symbol)d symbols.',
self.min_symbol
),
code='too_few_symbols',
params={'min_symbol': self.min_symbol},
)
def get_help_text(self):
return ungettext(
"Your password must contain at least %(min_symbol)d symbol.",
"Your password must contain at least %(min_symbol)d symbols.",
self.min_symbol
) % {'min_symbol': self.min_symbol}
def get_instruction_text(self):
if self.min_symbol > 0:
return ungettext(
'%(num)d symbol',
'%(num)d symbols',
self.min_symbol
) % {'num': self.min_symbol}
else:
return ''
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_symbol', self.min_symbol