diff --git a/common/djangoapps/util/password_policy_validators.py b/common/djangoapps/util/password_policy_validators.py index 5c9ef10b06..0363bfbcc0 100644 --- a/common/djangoapps/util/password_policy_validators.py +++ b/common/djangoapps/util/password_policy_validators.py @@ -160,6 +160,12 @@ def validate_password(password, user=None, username=None): 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. """ + if not isinstance(password, text_type): + try: + password = text_type(password, encoding='utf8') # some checks rely on unicode semantics (e.g. length) + except UnicodeDecodeError: + raise ValidationError(_('Invalid password.')) # no reason to get into weeds + username = username or (user and user.username) if user: @@ -308,7 +314,7 @@ def _validate_password_dictionary(value): if password_max_edit_distance and password_dictionary: for word in password_dictionary: - edit_distance = distance(text_type(value), text_type(word)) + 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") diff --git a/common/djangoapps/util/tests/test_password_policy_validators.py b/common/djangoapps/util/tests/test_password_policy_validators.py index aa547d6f25..97b636a358 100644 --- a/common/djangoapps/util/tests/test_password_policy_validators.py +++ b/common/djangoapps/util/tests/test_password_policy_validators.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """Tests for util.password_policy_validators module.""" import mock @@ -8,7 +9,9 @@ from django.conf import settings from django.core.exceptions import ValidationError from django.test.utils import override_settings -from util.password_policy_validators import password_instructions, validate_password, _validate_password_dictionary +from util.password_policy_validators import ( + password_instructions, password_min_length, validate_password, _validate_password_dictionary +) @ddt @@ -22,18 +25,38 @@ class PasswordPolicyValidatorsTestCase(unittest.TestCase): """ Tests dictionary checks """ # Direct match with self.assertRaises(ValidationError): - _validate_password_dictionary('testme') + _validate_password_dictionary(u'testme') # Off by one with self.assertRaises(ValidationError): - _validate_password_dictionary('estme') + _validate_password_dictionary(u'estme') # Off by two with self.assertRaises(ValidationError): - _validate_password_dictionary('bestmet') + _validate_password_dictionary(u'bestmet') # Off by three (should pass) - _validate_password_dictionary('bestem') + _validate_password_dictionary(u'bestem') + + def test_unicode_password(self): + """ Tests that validate_password enforces unicode """ + byte_str = b'𤭮' + unicode_str = u'𤭮' + + # Sanity checks and demonstration of why this test is useful + self.assertEqual(len(byte_str), 4) + self.assertEqual(len(unicode_str), 1) + self.assertEqual(password_min_length(), 2) + + # Test length check + with self.assertRaises(ValidationError): + validate_password(byte_str) + validate_password(byte_str + byte_str) + + # Test badly encoded password + with self.assertRaises(ValidationError) as cm: + validate_password(b'\xff\xff') + self.assertEquals('Invalid password.', cm.exception.message) @data( (u'', 'at least 2 characters & 2 letters & 1 number.'),