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.
479 lines
17 KiB
Python
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
|