Files
edx-platform/common/djangoapps/util/password_policy_validators.py
Dillon Dumesnil 4fa27f98dc Implementing django password validators for edX. This involves removing
the old validate password method and configuration values in favor of
AUTH_PASSWORD_VALIDATORS, a list of validators to use to check a
password. These include some that come straight from Django and some
that were written according to Django's specifications. This work also
included maintaining the current messaging as instruction text and
passing along restrictions for the password field.
2018-10-10 10:58:21 -04:00

451 lines
16 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,
MinimumLengthValidator as DjangoMinimumLengthValidator,
)
from django.core.exceptions import ValidationError
from django.utils.translation import ugettext as _, ungettext
from six import text_type
from student.models import PasswordHistory
log = logging.getLogger(__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 edX_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.')])
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(
'Your password must contain at least %(min_alphabetic)d letter.',
'Your 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(
'Your password must contain at least %(min_numeric)d number.',
'Your 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(
'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
),
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(
'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
),
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 characters
as defined by unicode categories.
Parameters:
min_punctuation (int): the minimum number of punctuation characters 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(
'Your password must contain at least %(min_punctuation)d punctuation character.',
'Your password must contain at least %(min_punctuation)d punctuation characters.',
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 character.",
"Your password must contain at least %(min_punctuation)d punctuation characters.",
self.min_punctuation
) % {'min_punctuation': self.min_punctuation}
def get_instruction_text(self):
if self.min_punctuation > 0:
return ungettext(
'%(num)d punctuation character',
'%(num)d punctuation characters',
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(
'Your password must contain at least %(min_symbol)d symbol.',
'Your 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