Commit f585d4c3 by Muhammad Shoaib Committed by Jonathan Piacenti

muhhshoaib/security-change-user-password-requirements-685:changed user password requirement

muhhshoaib/security-change-user-password-requirements-685: handled password complexity scenarios messages according to Chris Suggestions

muhhshoaib/security-change-user-password-requirements-685: changed the chracteres storage from sets to lists
parent 7da84af9
...@@ -40,42 +40,87 @@ def validate_password_complexity(value): ...@@ -40,42 +40,87 @@ def validate_password_complexity(value):
code = "complexity" code = "complexity"
complexities = getattr(settings, "PASSWORD_COMPLEXITY", None) complexities = getattr(settings, "PASSWORD_COMPLEXITY", None)
min_password_complexity_score = getattr(settings, "MINIMUM_PASSWORD_COMPLEXITY_SCORE", 0)
if complexities is None: if complexities is None:
return return
uppercase, lowercase, digits, non_ascii, punctuation = set(), set(), set(), set(), set() uppercase, lowercase, digits, non_ascii, punctuation = [], [], [], [], []
for character in value: for character in value:
if character.isupper(): if character.isupper():
uppercase.add(character) uppercase.append(character)
elif character.islower(): elif character.islower():
lowercase.add(character) lowercase.append(character)
elif character.isdigit(): elif character.isdigit():
digits.add(character) digits.append(character)
elif character in string.punctuation: elif character in string.punctuation:
punctuation.add(character) punctuation.append(character)
else: else:
non_ascii.add(character) non_ascii.append(character)
words = set(value.split()) words = value.split()
password_complexity_score = 0
errors = [] errors = []
if len(uppercase) < complexities.get("UPPER", 0): if len(uppercase) < complexities.get("UPPER", 0):
errors.append(_("must contain {0} or more uppercase characters").format(complexities["UPPER"])) errors.append((_("must contain {0} or more uppercase characters").format(complexities["UPPER"]), complexities.get("UPPER_SCORE", 1)))
elif complexities.get("UPPER", 0) > 0:
password_complexity_score += complexities.get("UPPER_SCORE", 1)
if len(lowercase) < complexities.get("LOWER", 0): if len(lowercase) < complexities.get("LOWER", 0):
errors.append(_("must contain {0} or more lowercase characters").format(complexities["LOWER"])) errors.append((_("must contain {0} or more lowercase characters").format(complexities["LOWER"]), complexities.get("LOWER_SCORE", 1)))
elif complexities.get("LOWER", 0) > 0:
password_complexity_score += complexities.get("LOWER_SCORE", 1)
if len(digits) < complexities.get("DIGITS", 0): if len(digits) < complexities.get("DIGITS", 0):
errors.append(_("must contain {0} or more digits").format(complexities["DIGITS"])) errors.append((_("must contain {0} or more digits").format(complexities["DIGITS"]), complexities.get("DIGITS_SCORE", 2)))
elif complexities.get("DIGITS", 0) > 0:
password_complexity_score += complexities.get("DIGITS_SCORE", 2)
if len(punctuation) < complexities.get("PUNCTUATION", 0): if len(punctuation) < complexities.get("PUNCTUATION", 0):
errors.append(_("must contain {0} or more punctuation characters").format(complexities["PUNCTUATION"])) errors.append((_("must contain {0} or more punctuation characters").format(complexities["PUNCTUATION"]), complexities.get("PUNCTUATION_SCORE", 2)))
elif complexities.get("PUNCTUATION", 0) > 0:
password_complexity_score += complexities.get("PUNCTUATION_SCORE", 2)
if len(non_ascii) < complexities.get("NON ASCII", 0): if len(non_ascii) < complexities.get("NON ASCII", 0):
errors.append(_("must contain {0} or more non ascii characters").format(complexities["NON ASCII"])) errors.append((_("must contain {0} or more non ascii characters").format(complexities["NON ASCII"]), complexities.get("NON_ASCII_SCORE", 2)))
elif complexities.get("NON ASCII", 0) > 0:
password_complexity_score += complexities.get("NON_ASCII_SCORE", 2)
if len(words) < complexities.get("WORDS", 0): if len(words) < complexities.get("WORDS", 0):
errors.append(_("must contain {0} or more unique words").format(complexities["WORDS"])) errors.append((_("must contain {0} or more unique words").format(complexities["WORDS"]), complexities.get("WORDS_SCORE", 2)))
elif complexities.get("WORDS", 0) > 0:
password_complexity_score += complexities.get("WORDS_SCORE", 2)
if 0 < min_password_complexity_score <= password_complexity_score:
return
if errors: if errors:
raise ValidationError(message.format(u', '.join(errors)), code=code) #show only those errors required to achieve minimum password complexity score
diff = min_password_complexity_score - password_complexity_score
if diff > 0:
filtered_error_messages = get_filtered_messages(diff, errors)
raise ValidationError(message.format(u', '.join(filtered_error_messages)), code=code)
else:
raise ValidationError(message.format(u', '.join(error[0] for error in errors)), code=code)
def get_filtered_messages(diff, errors):
"""
Get Filtered error Messages for password complexity
"""
from operator import itemgetter
weight = 0
# sort the array
sorted_errors = sorted(errors, key=itemgetter(1))
error_messages = []
index = 0
while diff > weight and len(sorted_errors) > index:
error_messages.append(sorted_errors[index][0])
weight += sorted_errors[index][1]
index += 1
return error_messages
def validate_password_dictionary(value): def validate_password_dictionary(value):
......
...@@ -145,7 +145,7 @@ class SessionApiSecurityTest(TestCase): ...@@ -145,7 +145,7 @@ class SessionApiSecurityTest(TestCase):
""" """
Try (and fail) user creation without any punctuation in password Try (and fail) user creation without any punctuation in password
""" """
response, mock_audit_log = self._do_request(self.user_url, 'test', 'test64SS', email='test@edx.org', response, mock_audit_log = self._do_request(self.user_url, 'test', 'test64Ss', email='test@edx.org', # pylint: disable=W0612
first_name='John', last_name='Doe', secure=True) first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters,' message = _('Password: Must be more complex (must contain 2 or more uppercase characters,'
' must contain 2 or more punctuation characters)') ' must contain 2 or more punctuation characters)')
...@@ -156,7 +156,7 @@ class SessionApiSecurityTest(TestCase): ...@@ -156,7 +156,7 @@ class SessionApiSecurityTest(TestCase):
""" """
Try (and fail) user creation without any numeric characters in password Try (and fail) user creation without any numeric characters in password
""" """
response, mock_audit_log = self._do_request(self.user_url, 'test', 'test.paSS!', email='test@edx.org', response, mock_audit_log = self._do_request(self.user_url, 'test', 'test.paSs!', email='test@edx.org', # pylint: disable=W0612
first_name='John', last_name='Doe', secure=True) first_name='John', last_name='Doe', secure=True)
message = _('Password: Must be more complex (must contain 2 or more uppercase characters,' message = _('Password: Must be more complex (must contain 2 or more uppercase characters,'
' must contain 2 or more digits)') ' must contain 2 or more digits)')
......
...@@ -526,9 +526,28 @@ MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {}) ...@@ -526,9 +526,28 @@ MICROSITE_CONFIGURATION = ENV_TOKENS.get('MICROSITE_CONFIGURATION', {})
MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', '')) MICROSITE_ROOT_DIR = path(ENV_TOKENS.get('MICROSITE_ROOT_DIR', ''))
#### PASSWORD POLICY SETTINGS ##### #### PASSWORD POLICY SETTINGS #####
# Minimum number of characters a password should have
# e.g PASSWORD_MIN_LENGTH = 8
PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH") PASSWORD_MIN_LENGTH = ENV_TOKENS.get("PASSWORD_MIN_LENGTH")
# Maximum number of characters a password should have
# e.g PASSWORD_MAX_LENGTH = None
PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH") PASSWORD_MAX_LENGTH = ENV_TOKENS.get("PASSWORD_MAX_LENGTH")
# Password complexity should be a dict of complexity levels and their corresponding score
# e.g.
# { 'UPPER': 2, 'LOWER': 2, 'PUNCTUATION': 2, 'DIGITS': 2,
# 'UPPER_SCORE': 1, 'LOWER_SCORE': 1, 'PUNCTUATION_SCORE': 2, 'DIGITS_SCORE': 2 }
# if score is not specified for any of complexity level its default value will be used
# UPPER_SCORE has a default value of 1
# LOWER_SCORE has a default value of 1
# PUNCTUATION_SCORE has a default value of 2
# DIGITS_SCORE has a default value of 2
# NON_ASCII_SCORE has a default value of 2
# WORDS_SCORE has a default value of 2
# Default scores are only applicable if relevant complexity level is specified
PASSWORD_COMPLEXITY = ENV_TOKENS.get("PASSWORD_COMPLEXITY", {}) PASSWORD_COMPLEXITY = ENV_TOKENS.get("PASSWORD_COMPLEXITY", {})
# Minimum score required to fulfill password complexity criteria. defaults to zero
# which means a password should fulfill all levels specified in PASSWORD_COMPLEXITY
MINIMUM_PASSWORD_COMPLEXITY_SCORE = ENV_TOKENS.get('MINIMUM_PASSWORD_COMPLEXITY_SCORE', 0)
PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = ENV_TOKENS.get("PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD") PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = ENV_TOKENS.get("PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD")
PASSWORD_DICTIONARY = ENV_TOKENS.get("PASSWORD_DICTIONARY", []) PASSWORD_DICTIONARY = ENV_TOKENS.get("PASSWORD_DICTIONARY", [])
......
...@@ -313,3 +313,7 @@ except ImportError: ...@@ -313,3 +313,7 @@ except ImportError:
########################## ALLOWED API USER IP ######################## ########################## ALLOWED API USER IP ########################
API_ALLOWED_IP_ADDRESSES = ['127.0.0.1'] API_ALLOWED_IP_ADDRESSES = ['127.0.0.1']
# Minimum score required to fulfill password complexity criteria. defaults to zero
# which means a password should fulfill all levels specified in PASSWORD_COMPLEXITY
MINIMUM_PASSWORD_COMPLEXITY_SCORE = 0
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment