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.
This commit is contained in:
@@ -17,6 +17,7 @@ from mock import patch
|
||||
|
||||
from courseware.tests.helpers import LoginEnrollmentTestCase
|
||||
from student.models import PasswordHistory
|
||||
from util.password_policy_validators import create_validator_config
|
||||
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ADVANCED_SECURITY': True})
|
||||
@@ -161,148 +162,9 @@ class TestPasswordHistory(LoginEnrollmentTestCase):
|
||||
resp.content
|
||||
)
|
||||
|
||||
@patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DIFFERENT_STUDENT_PASSWORDS_BEFORE_REUSE': 1})
|
||||
def test_student_password_reset_reuse(self):
|
||||
"""
|
||||
Goes through the password reset flows to make sure the various password reuse policies are enforced
|
||||
"""
|
||||
student_email, _ = self._setup_user()
|
||||
user = User.objects.get(email=student_email)
|
||||
|
||||
err_msg = 'You are re\\\\u002Dusing a password that you have used recently. You must have 1 distinct password'
|
||||
success_msg = 'Your Password Reset is Complete'
|
||||
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
# try to do a password reset with the same password as before
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'foo',
|
||||
'new_password2': 'foo'
|
||||
}, follow=True)
|
||||
|
||||
self.assertPasswordResetError(resp, err_msg)
|
||||
|
||||
# now retry with a different password
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'bar',
|
||||
'new_password2': 'bar'
|
||||
}, follow=True)
|
||||
|
||||
self.assertIn(success_msg, resp.content)
|
||||
|
||||
@patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_DIFFERENT_STAFF_PASSWORDS_BEFORE_REUSE': 2})
|
||||
def test_staff_password_reset_reuse(self):
|
||||
"""
|
||||
Goes through the password reset flows to make sure the various password reuse policies are enforced
|
||||
"""
|
||||
staff_email, _ = self._setup_user(is_staff=True)
|
||||
user = User.objects.get(email=staff_email)
|
||||
|
||||
err_msg = 'You are re\\\\u002Dusing a password that you have used recently. You must have 2 distinct passwords'
|
||||
success_msg = 'Your Password Reset is Complete'
|
||||
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
# try to do a password reset with the same password as before
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'foo',
|
||||
'new_password2': 'foo',
|
||||
}, follow=True)
|
||||
|
||||
self.assertPasswordResetError(resp, err_msg)
|
||||
|
||||
# now use different one
|
||||
user = User.objects.get(email=staff_email)
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'bar',
|
||||
'new_password2': 'bar',
|
||||
}, follow=True)
|
||||
|
||||
self.assertIn(success_msg, resp.content)
|
||||
|
||||
# now try again with the first one
|
||||
user = User.objects.get(email=staff_email)
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'foo',
|
||||
'new_password2': 'foo',
|
||||
}, follow=True)
|
||||
|
||||
self.assertPasswordResetError(resp, err_msg)
|
||||
|
||||
# now use different one
|
||||
user = User.objects.get(email=staff_email)
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'baz',
|
||||
'new_password2': 'baz',
|
||||
}, follow=True)
|
||||
|
||||
self.assertIn(success_msg, resp.content)
|
||||
|
||||
# now we should be able to reuse the first one
|
||||
user = User.objects.get(email=staff_email)
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'foo',
|
||||
'new_password2': 'foo',
|
||||
}, follow=True)
|
||||
|
||||
self.assertIn(success_msg, resp.content)
|
||||
|
||||
@patch.dict("django.conf.settings.ADVANCED_SECURITY_CONFIG", {'MIN_TIME_IN_DAYS_BETWEEN_ALLOWED_RESETS': 1})
|
||||
def test_password_reset_frequency_limit(self):
|
||||
"""
|
||||
Asserts the frequency limit on how often we can change passwords
|
||||
"""
|
||||
staff_email, _ = self._setup_user(is_staff=True)
|
||||
|
||||
success_msg = 'Your Password Reset is Complete'
|
||||
|
||||
# try to reset password, it should fail
|
||||
user = User.objects.get(email=staff_email)
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
# try to do a password reset with the same password as before
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'foo',
|
||||
'new_password2': 'foo',
|
||||
}, follow=True)
|
||||
|
||||
self.assertNotIn(
|
||||
success_msg,
|
||||
resp.content
|
||||
)
|
||||
|
||||
# pretend we're in the future
|
||||
staff_reset_time = timezone.now() + timedelta(days=1)
|
||||
with freeze_time(staff_reset_time):
|
||||
user = User.objects.get(email=staff_email)
|
||||
token = default_token_generator.make_token(user)
|
||||
uidb36 = int_to_base36(user.id)
|
||||
|
||||
# try to do a password reset with the same password as before
|
||||
resp = self.client.post('/password_reset_confirm/{0}-{1}/'.format(uidb36, token), {
|
||||
'new_password1': 'foo',
|
||||
'new_password2': 'foo',
|
||||
}, follow=True)
|
||||
|
||||
self.assertIn(success_msg, resp.content)
|
||||
|
||||
@patch.dict("django.conf.settings.FEATURES", {'ENFORCE_PASSWORD_POLICY': True})
|
||||
@override_settings(PASSWORD_MIN_LENGTH=6)
|
||||
@override_settings(AUTH_PASSWORD_VALIDATORS=[
|
||||
create_validator_config('util.password_policy_validators.MinimumLengthValidator', {'min_length': 6})
|
||||
])
|
||||
def test_password_policy_on_password_reset(self):
|
||||
"""
|
||||
This makes sure the proper asserts on password policy also works on password reset
|
||||
@@ -342,7 +204,7 @@ class TestPasswordHistory(LoginEnrollmentTestCase):
|
||||
|
||||
@ddt.data(
|
||||
('foo', 'foobar', 'Error in resetting your password. Please try again.'),
|
||||
('', '', 'Enter a password with at least'),
|
||||
('', '', 'This password is too short. It must contain at least'),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_password_reset_form_invalid(self, password1, password2, err_msg):
|
||||
|
||||
@@ -652,7 +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 #####
|
||||
AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", [])
|
||||
AUTH_PASSWORD_VALIDATORS = ENV_TOKENS.get("AUTH_PASSWORD_VALIDATORS", AUTH_PASSWORD_VALIDATORS)
|
||||
|
||||
### INACTIVITY SETTINGS ####
|
||||
SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIMEOUT_IN_SECONDS")
|
||||
|
||||
@@ -173,7 +173,6 @@ YOUTUBE['TEXT_API']['url'] = "{0}:{1}/test_transcripts_youtube/".format(YOUTUBE_
|
||||
############################# SECURITY SETTINGS ################################
|
||||
# Default to advanced security in common.py, so tests can reset here to use
|
||||
# a simpler security model
|
||||
FEATURES['ENFORCE_PASSWORD_POLICY'] = False
|
||||
FEATURES['ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'] = False
|
||||
FEATURES['SQUELCH_PII_IN_LOGS'] = False
|
||||
FEATURES['PREVENT_CONCURRENT_LOGINS'] = False
|
||||
@@ -183,9 +182,6 @@ FEATURES['ENABLE_MOBILE_REST_API'] = True # Show video bumper in LMS
|
||||
FEATURES['ENABLE_VIDEO_BUMPER'] = True # Show video bumper in LMS
|
||||
FEATURES['SHOW_BUMPER_PERIODICITY'] = 1
|
||||
|
||||
PASSWORD_MIN_LENGTH = None
|
||||
PASSWORD_COMPLEXITY = {}
|
||||
|
||||
# Enable courseware search for tests
|
||||
FEATURES['ENABLE_COURSEWARE_SEARCH'] = True
|
||||
|
||||
|
||||
@@ -202,9 +202,6 @@ FEATURES = {
|
||||
# Maximum number of rows to include in the csv file for downloading problem responses.
|
||||
'MAX_PROBLEM_RESPONSES_COUNT': 5000,
|
||||
|
||||
# whether to use password policy enforcement or not
|
||||
'ENFORCE_PASSWORD_POLICY': True,
|
||||
|
||||
'ENABLED_PAYMENT_REPORTS': [
|
||||
"refund_report",
|
||||
"itemized_purchase_report",
|
||||
@@ -2594,11 +2591,23 @@ FINANCIAL_REPORTS = {
|
||||
POLICY_CHANGE_TASK_RATE_LIMIT = '300/h'
|
||||
|
||||
#### PASSWORD POLICY SETTINGS #####
|
||||
PASSWORD_MIN_LENGTH = 8
|
||||
PASSWORD_MAX_LENGTH = None
|
||||
PASSWORD_COMPLEXITY = {"UPPER": 1, "LOWER": 1, "DIGITS": 1}
|
||||
PASSWORD_DICTIONARY_EDIT_DISTANCE_THRESHOLD = None
|
||||
PASSWORD_DICTIONARY = []
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME": "util.password_policy_validators.MinimumLengthValidator",
|
||||
"OPTIONS": {
|
||||
"min_length": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"NAME": "util.password_policy_validators.MaximumLengthValidator",
|
||||
"OPTIONS": {
|
||||
"max_length": 75
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
############################ ORA 2 ############################################
|
||||
|
||||
|
||||
@@ -137,7 +137,6 @@ FEATURES['ENABLE_MOBILE_REST_API'] = True
|
||||
FEATURES['ENABLE_VIDEO_ABSTRACTION_LAYER_API'] = True
|
||||
|
||||
########################## SECURITY #######################
|
||||
FEATURES['ENFORCE_PASSWORD_POLICY'] = False
|
||||
FEATURES['ENABLE_MAX_FAILED_LOGIN_ATTEMPTS'] = False
|
||||
FEATURES['SQUELCH_PII_IN_LOGS'] = False
|
||||
FEATURES['PREVENT_CONCURRENT_LOGINS'] = False
|
||||
|
||||
@@ -648,11 +648,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", AUTH_PASSWORD_VALIDATORS)
|
||||
|
||||
### INACTIVITY SETTINGS ####
|
||||
SESSION_INACTIVITY_TIMEOUT_IN_SECONDS = AUTH_TOKENS.get("SESSION_INACTIVITY_TIMEOUT_IN_SECONDS")
|
||||
|
||||
@@ -223,8 +223,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 = {}
|
||||
|
||||
######### Third-party auth ##########
|
||||
FEATURES['ENABLE_THIRD_PARTY_AUTH'] = True
|
||||
|
||||
@@ -195,7 +195,9 @@
|
||||
|
||||
// Hide each input tip
|
||||
$(this).children().each(function() {
|
||||
if (inputTipSelectors.indexOf($(this).attr('class')) >= 0) {
|
||||
// This is a 1 instead of 0 so the error message for a field is not
|
||||
// hidden on blur and only the help tip is hidden.
|
||||
if (inputTipSelectors.indexOf($(this).attr('class')) >= 1) {
|
||||
$(this).addClass('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user