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:
Dillon Dumesnil
2018-10-02 14:21:07 -04:00
parent 4fa27f98dc
commit deb758c808
30 changed files with 482 additions and 469 deletions

View File

@@ -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):

View File

@@ -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")

View File

@@ -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

View File

@@ -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 ############################################

View File

@@ -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

View File

@@ -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")

View File

@@ -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

View File

@@ -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');
}
});