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.
This commit is contained in:
Dillon Dumesnil
2018-09-13 15:28:21 -04:00
parent 40918f36d8
commit 4fa27f98dc
11 changed files with 441 additions and 332 deletions

View File

@@ -447,11 +447,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_AL
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS", 15 * 60)
#### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
PASSWORD_COMPLEXITY = ENV_TOKENS.get("PASSWORD_COMPLEXITY", {})
PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = ENV_TOKENS.get("PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD")
PASSWORD_DICTIONARY = ENV_TOKENS.get("PASSWORD_DICTIONARY", [])
AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", [])
### INACTIVITY SETTINGS ####
SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIMEOUT_IN_SECONDS")

View File

@@ -27,7 +27,7 @@ from openedx.core.djangoapps.user_api import accounts as accounts_settings
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference
from student.message_types import PasswordReset
from student.models import CourseEnrollmentAllowed, email_exists_or_retired
from util.password_policy_validators import password_max_length, password_min_length, validate_password
from util.password_policy_validators import edX_validate_password
def send_password_reset_email_for_user(user, request):
@@ -193,7 +193,6 @@ class AccountCreationForm(forms.Form):
"""
_EMAIL_INVALID_MSG = _("A properly formatted e-mail is required")
_PASSWORD_INVALID_MSG = _("A valid password is required")
_NAME_TOO_SHORT_MSG = _("Your legal name must be a minimum of two characters long")
# TODO: Resolve repetition
@@ -209,15 +208,9 @@ class AccountCreationForm(forms.Form):
"max_length": _("Email cannot be more than %(limit_value)s characters long"),
}
)
password = forms.CharField(
min_length=password_min_length(),
max_length=password_max_length(),
error_messages={
"required": _PASSWORD_INVALID_MSG,
"min_length": _PASSWORD_INVALID_MSG,
"max_length": _PASSWORD_INVALID_MSG,
}
)
password = forms.CharField()
name = forms.CharField(
min_length=accounts_settings.NAME_MIN_LENGTH,
error_messages={
@@ -288,7 +281,12 @@ class AccountCreationForm(forms.Form):
"""Enforce password policies (if applicable)"""
password = self.cleaned_data["password"]
if self.enforce_password_policy:
validate_password(password, username=self.cleaned_data.get('username'))
# Creating a temporary user object to test password against username
# This user should NOT be saved
username = self.cleaned_data.get('username')
email = self.cleaned_data.get('email')
temp_user = User(username=username, email=email) if username else None
edX_validate_password(password, temp_user)
return password
def clean_email(self):

View File

@@ -94,7 +94,7 @@ from student.text_me_the_app import TextMeTheAppFragmentView
from util.bad_request_rate_limiter import BadRequestRateLimiter
from util.db import outer_atomic
from util.json_request import JsonResponse
from util.password_policy_validators import SecurityPolicyError, validate_password
from util.password_policy_validators import edX_validate_password
log = logging.getLogger("edx.student")
@@ -830,7 +830,7 @@ def password_reset_confirm_wrapper(request, uidb36=None, token=None):
password = request.POST['new_password1']
try:
validate_password(password, user=user)
edX_validate_password(password, user=user)
except ValidationError as err:
# We have a password reset attempt which violates some security
# policy, or any other validation. Use the existing Django template to communicate that

View File

@@ -1,322 +1,450 @@
"""
This file exposes a number of password complexity validators which can be optionally added to
This file exposes a number of password validators which can be optionally added to
account creation
This file was inspired by the django-passwords project at https://github.com/dstufft/django-passwords
authored by dstufft (https://github.com/dstufft)
"""
from __future__ import division
from __future__ import unicode_literals
import logging
import string
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_lazy as _
from django.utils.translation import ungettext_lazy as ungettext
from Levenshtein import distance
from django.utils.translation import ugettext as _, ungettext
from six import text_type
from student.models import PasswordHistory
log = logging.getLogger(__name__)
# In description order
_allowed_password_complexity = [
'ALPHABETIC',
'UPPER',
'LOWER',
'NUMERIC',
'DIGITS',
'PUNCTUATION',
'NON ASCII',
'WORDS',
]
class SecurityPolicyError(ValidationError):
pass
def password_min_length():
def password_validators_instruction_texts():
"""
Returns minimum required length of a password.
Can be overridden by site configuration of PASSWORD_MIN_LENGTH.
Return a string of instruction texts of all configured validators.
Expects at least the MinimumLengthValidator to be defined.
"""
min_length = getattr(settings, 'PASSWORD_MIN_LENGTH', None)
if min_length is None:
return 2 # Note: This default is simply historical
return min_length
def password_max_length():
"""
Returns maximum allowed length of a password. If zero, no maximum.
Can be overridden by site configuration of PASSWORD_MAX_LENGTH.
"""
# Note: The default value here is simply historical
max_length = getattr(settings, 'PASSWORD_MAX_LENGTH', None)
if max_length is None:
return 75 # Note: This default is simply historical
return max_length
def password_complexity():
"""
:return: A dict of complexity requirements from settings
"""
complexity = {}
if settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
complexity = getattr(settings, 'PASSWORD_COMPLEXITY', {})
valid_complexity = {x: y for x, y in complexity.iteritems() if x in _allowed_password_complexity}
if not password_complexity.logged:
invalid = frozenset(complexity.keys()) - frozenset(valid_complexity.keys())
for key in invalid:
log.warning('Unrecognized %s value in PASSWORD_COMPLEXITY setting.', key)
password_complexity.logged = True
return valid_complexity
# Declare static variable for the function above, which helps avoid issuing multiple log warnings.
# We don't instead keep a cached version of the complexity rules, because that might trip up unit tests.
password_complexity.logged = False
def _password_complexity_descriptions(which=None):
"""
which: A list of which complexities to describe, None if you want the configured ones
:return: A list of complexity descriptions
"""
descs = []
complexity = password_complexity()
if which is None:
which = complexity.keys()
for key in _allowed_password_complexity: # we iterate over allowed keys so that we get the order right
value = complexity.get(key, 0) if key in which else 0
if not value:
continue
if key == 'ALPHABETIC':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} letter', '{num} letters', value).format(num=value))
elif key == 'UPPER':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} uppercase letter', '{num} uppercase letters', value).format(num=value))
elif key == 'LOWER':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} lowercase letter', '{num} lowercase letters', value).format(num=value))
elif key == 'DIGITS':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} digit', '{num} digits', value).format(num=value))
elif key == 'NUMERIC':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} number', '{num} numbers', value).format(num=value))
elif key == 'PUNCTUATION':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} punctuation mark', '{num} punctuation marks', value).format(num=value))
elif key == 'NON ASCII': # note that our definition of non-ascii is non-letter, non-digit, non-punctuation
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} symbol', '{num} symbols', value).format(num=value))
elif key == 'WORDS':
# Translators: This appears in a list of password requirements
descs.append(ungettext('{num} word', '{num} words', value).format(num=value))
else:
raise Exception('Unexpected complexity value {}'.format(key))
return descs
def password_instructions():
"""
:return: A string suitable for display to the user to tell them what password to enter
"""
min_length = password_min_length()
reqs = _password_complexity_descriptions()
if not reqs:
return ungettext('Your password must contain at least {num} character.',
'Your password must contain at least {num} characters.',
min_length).format(num=min_length)
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 ungettext('Your password must contain at least {num} character, including {requirements}.',
'Your password must contain at least {num} characters, including {requirements}.',
min_length).format(num=min_length, requirements=' & '.join(reqs))
return _('Your password must contain {length_instruction}.'.format(length_instruction=length_instruction))
def validate_password(password, user=None, username=None, password_reset=True):
def password_validators_restrictions():
"""
Checks user-provided password against our current site policy.
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
Raises a ValidationError or SecurityPolicyError depending on the type of error.
Arguments:
password: The user-provided password as a string
user: A User model object, if available. Required to check against security policy.
username: The user-provided username, if available. Taken from 'user' if not provided.
password_reset: Whether to run validators that only make sense in a password reset
context (like PasswordHistory).
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:
password = text_type(password, encoding='utf8') # some checks rely on unicode semantics (e.g. length)
# some checks rely on unicode semantics (e.g. length)
password = text_type(password, encoding='utf8')
except UnicodeDecodeError:
raise ValidationError(_('Invalid password.')) # no reason to get into weeds
# no reason to get into weeds
raise ValidationError([_('Invalid password.')])
username = username or (user and user.username)
if user and password_reset:
_validate_password_security(password, user)
_validate_password_dictionary(password)
_validate_password_against_username(password, username)
# Some messages are composable, so we'll add them together here
errors = [_validate_password_length(password)]
errors += _validate_password_complexity(password)
errors = filter(None, errors)
if errors:
msg = _('Enter a password with at least {requirements}.').format(requirements=' & '.join(errors))
raise ValidationError(msg)
validate_password(password, user)
def _validate_password_security(password, user):
def _validate_condition(password, fn, min_count):
"""
Check password reuse and similar operational security policy considerations.
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
"""
# Check reuse
if not PasswordHistory.is_allowable_password_reuse(user, password):
if user.is_staff:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE']
else:
num_distinct = settings.ADVANCED_SECURITY_CONFIG['MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE']
raise SecurityPolicyError(ungettext(
"You are re-using a password that you have used recently. "
"You must have {num} distinct password before reusing a previous password.",
"You are re-using a password that you have used recently. "
"You must have {num} distinct passwords before reusing a previous password.",
num_distinct
).format(num=num_distinct))
# Check reset frequency
if PasswordHistory.is_password_reset_too_soon(user):
num_days = settings.ADVANCED_SECURITY_CONFIG['MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS']
raise SecurityPolicyError(ungettext(
"You are resetting passwords too frequently. Due to security policies, "
"{num} day must elapse between password resets.",
"You are resetting passwords too frequently. Due to security policies, "
"{num} days must elapse between password resets.",
num_days
).format(num=num_days))
valid_count = len([c for c in password if fn(c)])
return valid_count >= min_count
def _validate_password_length(value):
"""
Validator that enforces minimum length of a password
"""
min_length = password_min_length()
max_length = password_max_length()
if min_length and len(value) < min_length:
# This is an error that can be composed with other requirements, so just return a fragment
# Translators: This appears in a list of password requirements
class MinimumLengthValidator(DjangoMinimumLengthValidator):
def get_instruction_text(self):
return ungettext(
"{num} character",
"{num} characters",
min_length
).format(num=min_length)
elif max_length and len(value) > max_length:
raise ValidationError(ungettext(
"Enter a password with at most {num} character.",
"Enter a password with at most {num} characters.",
max_length
).format(num=max_length))
'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
def _validate_password_complexity(value):
class MaximumLengthValidator(object):
"""
Validator that enforces minimum complexity
Validate whether the password is shorter than a maximum length.
Parameters:
max_length (int): the maximum number of characters to require in the password.
"""
complexities = password_complexity()
if not complexities:
return []
def __init__(self, max_length=75):
self.max_length = max_length
# Sets are here intentionally
uppercase, lowercase, digits, non_ascii, punctuation = set(), set(), set(), set(), set()
alphabetic, numeric = [], []
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},
)
for character in value:
if character.isupper():
uppercase.add(character)
elif character.islower():
lowercase.add(character)
elif character.isdigit():
digits.add(character)
elif character in string.punctuation:
punctuation.add(character)
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:
non_ascii.add(character)
return ''
if character.isalpha():
alphabetic.append(character)
if 'N' in unicodedata.category(character): # Check to see if the unicode category contains a 'N'umber
numeric.append(character)
words = set(value.split())
errors = []
if len(uppercase) < complexities.get("UPPER", 0):
errors.append('UPPER')
if len(lowercase) < complexities.get("LOWER", 0):
errors.append('LOWER')
if len(digits) < complexities.get("DIGITS", 0):
errors.append('DIGITS')
if len(punctuation) < complexities.get("PUNCTUATION", 0):
errors.append('PUNCTUATION')
if len(non_ascii) < complexities.get("NON ASCII", 0):
errors.append('NON ASCII')
if len(words) < complexities.get("WORDS", 0):
errors.append('WORDS')
if len(numeric) < complexities.get("NUMERIC", 0):
errors.append('NUMERIC')
if len(alphabetic) < complexities.get("ALPHABETIC", 0):
errors.append('ALPHABETIC')
if errors:
return _password_complexity_descriptions(errors)
else:
return []
def get_restriction(self):
"""
Returns a key, value pair for the restrictions related to the Validator
"""
return 'min_alphabetic', self.min_alphabetic
def _validate_password_against_username(password, username):
if not username:
return
if password == username:
# Translators: This message is shown to users who enter a password matching
# the username they enter(ed).
raise ValidationError(_(u"Password cannot be the same as the username."))
def _validate_password_dictionary(value):
class NumericValidator(object):
"""
Insures that the password is not too similar to a defined set of dictionary words
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.
"""
if not settings.FEATURES.get('ENFORCE_PASSWORD_POLICY', False):
return
def __init__(self, min_numeric=0):
self.min_numeric = min_numeric
password_max_edit_distance = getattr(settings, "PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD", None)
password_dictionary = getattr(settings, "PASSWORD_DICTIONARY", None)
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},
)
if password_max_edit_distance and password_dictionary:
for word in password_dictionary:
edit_distance = distance(value, text_type(word))
if edit_distance <= password_max_edit_distance:
raise ValidationError(_("Password is too similar to a dictionary word."),
code="dictionary_word")
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

View File

@@ -652,11 +652,7 @@ MAX_FAILED_LOGIN_ATTEMPTS_ALLOWED = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_AL
MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS = ENV_TOKENS.get("MAX_FAILED_LOGIN_ATTEMPTS_LOCKOUT_PERIOD_SECS", 15 * 60)
#### PASSWORD POLICY SETTINGS #####
PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
PASSWORD_COMPLEXITY = ENV_TOKENS.get("PASSWORD_COMPLEXITY", {})
PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = ENV_TOKENS.get("PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD")
PASSWORD_DICTIONARY = ENV_TOKENS.get("PASSWORD_DICTIONARY", [])
AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", [])
### INACTIVITY SETTINGS ####
SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIMEOUT_IN_SECONDS")

View File

@@ -142,9 +142,6 @@ FEATURES['ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'] = False
FEATURES['SQUELCH_PII_IN_LOGS'] = False
FEATURES['PREVENT_CONCURRENT_LOGINS'] = False
FEATURES['ADVANCED_SECURITY'] = False
PASSWORD_MIN_LENGTH = None
PASSWORD_COMPLEXITY = {}
########################### Milestones #################################
FEATURES['MILESTONES_APP'] = True

View File

@@ -8,7 +8,7 @@ from django.conf import settings
from django.utils.translation import ugettext as _
from util.date_utils import DEFAULT_SHORT_DATE_FORMAT, strftime_localized
from util.password_policy_validators import validate_password
from util.password_policy_validators import edX_validate_password
class NonCompliantPasswordException(Exception):
@@ -104,7 +104,7 @@ def _check_user_compliance(user, password):
Returns a boolean indicating whether or not the user is compliant with password policy rules.
"""
try:
validate_password(password, user=user, password_reset=False)
edX_validate_password(password, user=user)
return True
except Exception: # pylint: disable=broad-except
# If anything goes wrong, we should assume the password is not compliant but we don't necessarily

View File

@@ -18,7 +18,7 @@ from student.models import User, UserProfile, Registration, email_exists_or_reti
from student import forms as student_forms
from student import views as student_views
from util.model_utils import emit_setting_changed_event
from util.password_policy_validators import validate_password
from util.password_policy_validators import edX_validate_password
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api import errors, accounts, forms, helpers
@@ -327,7 +327,7 @@ def create_account(username, password, email):
# Validate the username, password, and email
# This will raise an exception if any of these are not in a valid format.
_validate_username(username)
_validate_password(password, username)
_validate_password(password, username, email)
_validate_email(email)
# Create the user account, setting them to "inactive" until they activate their account.
@@ -494,17 +494,17 @@ def get_confirm_email_validation_error(confirm_email, email):
return _validate(_validate_confirm_email, errors.AccountEmailInvalid, confirm_email, email)
def get_password_validation_error(password, username=None):
def get_password_validation_error(password, username=None, email=None):
"""Get the built-in validation error message for when
the password is invalid in some way.
:param password: The proposed password (unicode).
:param username: The username associated with the user's account (unicode).
:param default: The message to default to in case of no error.
:param email: The email associated with the user's account (unicode).
:return: Validation error message.
"""
return _validate(_validate_password, errors.AccountPasswordInvalid, password, username)
return _validate(_validate_password, errors.AccountPasswordInvalid, password, username, email)
def get_country_validation_error(country):
@@ -643,15 +643,17 @@ def _validate_confirm_email(confirm_email, email):
raise errors.AccountEmailInvalid(accounts.REQUIRED_FIELD_CONFIRM_EMAIL_MSG)
def _validate_password(password, username=None):
def _validate_password(password, username=None, email=None):
"""Validate the format of the user's password.
Passwords cannot be the same as the username of the account,
so we take `username` as an argument.
so we create a temp_user using the username and email to test the password against.
This user is never saved.
Arguments:
password (unicode): The proposed password.
username (unicode): The username associated with the user's account.
email (unicode): The email associated with the user's account.
Returns:
None
@@ -662,12 +664,12 @@ def _validate_password(password, username=None):
"""
try:
_validate_type(password, basestring, accounts.PASSWORD_BAD_TYPE_MSG)
validate_password(password, username=username)
temp_user = User(username=username, email=email) if username else None
edX_validate_password(password, user=temp_user)
except errors.AccountDataBadType as invalid_password_err:
raise errors.AccountPasswordInvalid(text_type(invalid_password_err))
except ValidationError as validation_err:
raise errors.AccountPasswordInvalid(validation_err.message)
raise errors.AccountPasswordInvalid(' '.join(validation_err.messages))
def _validate_country(country):

View File

@@ -18,7 +18,7 @@ from openedx.features.enterprise_support.api import enterprise_customer_for_requ
from student.forms import get_registration_extension_form
from student.models import UserProfile
from util.password_policy_validators import (
password_complexity, password_instructions, password_max_length, password_min_length
password_validators_instruction_texts, password_validators_restrictions
)
@@ -118,9 +118,10 @@ def get_login_session_form(request):
"password",
label=password_label,
field_type="password",
restrictions={
"max_length": password_max_length(),
}
# The following restriction contains the assumption that the max password length will never exceed 5000
# characters. The point of this restriction on the login page is to prevent any sort of attacks
# involving sending massive passwords.
restrictions={'max_length': 5000}
)
form_desc.add_field(
@@ -419,22 +420,12 @@ class RegistrationFormFactory(object):
# meant to hold the user's password.
password_label = _(u"Password")
restrictions = {
"min_length": password_min_length(),
"max_length": password_max_length(),
}
complexities = password_complexity()
for key, value in complexities.iteritems():
api_key = key.lower().replace(' ', '_')
restrictions[api_key] = value
form_desc.add_field(
"password",
label=password_label,
field_type="password",
instructions=password_instructions(),
restrictions=restrictions,
instructions=password_validators_instruction_texts(),
restrictions=password_validators_restrictions(),
required=required
)

View File

@@ -125,8 +125,8 @@ class FormDescription(object):
ALLOWED_RESTRICTIONS = {
"text": ["min_length", "max_length"],
"password": ["min_length", "max_length", "upper", "lower", "digits", "punctuation", "non_ascii", "words",
"numeric", "alphabetic"],
"password": ["min_length", "max_length", "min_upper", "min_lower",
"min_punctuation", "min_symbol", "min_numeric", "min_alphabetic"],
"email": ["min_length", "max_length", "readonly"],
}

View File

@@ -144,14 +144,15 @@ class RegistrationValidationView(APIView):
return invalid_email_error or email_exists_error
def confirm_email_handler(self, request):
email = request.data.get('email', None)
email = request.data.get('email')
confirm_email = request.data.get('confirm_email')
return get_confirm_email_validation_error(confirm_email, email)
def password_handler(self, request):
username = request.data.get('username', None)
username = request.data.get('username')
email = request.data.get('email')
password = request.data.get('password')
return get_password_validation_error(password, username)
return get_password_validation_error(password, username, email)
def country_handler(self, request):
country = request.data.get('country')