From b289bb90b5e9bc684858a4c2b3801fed114ad985 Mon Sep 17 00:00:00 2001 From: Jeff LaJoie Date: Fri, 9 Mar 2018 09:42:50 -0500 Subject: [PATCH] LEARNER-4423: Adds in PCI compliance checks for alphabetic and numeric characters --- .../student/tests/test_password_policy.py | 38 +++++++++++++++++++ .../util/password_policy_validators.py | 12 ++++++ 2 files changed, 50 insertions(+) diff --git a/common/djangoapps/student/tests/test_password_policy.py b/common/djangoapps/student/tests/test_password_policy.py index 148e73792a..a8f16337c2 100644 --- a/common/djangoapps/student/tests/test_password_policy.py +++ b/common/djangoapps/student/tests/test_password_policy.py @@ -160,6 +160,44 @@ class TestPasswordPolicy(TestCase): obj = json.loads(response.content) self.assertTrue(obj['success']) + @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'NUMERIC': 3}) + def test_not_enough_numeric_characters(self): + self.url_params['password'] = u'thishouldfail½2' + response = self.client.post(self.url, self.url_params) + self.assertEqual(response.status_code, 400) + obj = json.loads(response.content) + self.assertEqual( + obj['value'], + "Password: Must be more complex (must contain 3 or more numbers)", + ) + + @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'NUMERIC': 3}) + def test_enough_numeric_characters(self): + self.url_params['password'] = u'thisShouldPass½33' # This unicode 1/2 should count as a numeric value here + response = self.client.post(self.url, self.url_params) + self.assertEqual(response.status_code, 200) + obj = json.loads(response.content) + self.assertTrue(obj['success']) + + @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'ALPHABETIC': 3}) + def test_not_enough_alphabetic_characters(self): + self.url_params['password'] = '123456ab' + response = self.client.post(self.url, self.url_params) + self.assertEqual(response.status_code, 400) + obj = json.loads(response.content) + self.assertEqual( + obj['value'], + "Password: Must be more complex (must contain 3 or more letters)", + ) + + @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", {'ALPHABETIC': 3}) + def test_enough_alphabetic_characters(self): + self.url_params['password'] = u'𝒯𝓗Ï𝓼𝒫å𝓼𝓼𝔼𝓼' + response = self.client.post(self.url, self.url_params) + self.assertEqual(response.status_code, 200) + obj = json.loads(response.content) + self.assertTrue(obj['success']) + @patch.dict("django.conf.settings.PASSWORD_COMPLEXITY", { 'PUNCTUATION': 3, 'WORDS': 3, diff --git a/common/djangoapps/util/password_policy_validators.py b/common/djangoapps/util/password_policy_validators.py index eaa552722c..ac787bb5db 100644 --- a/common/djangoapps/util/password_policy_validators.py +++ b/common/djangoapps/util/password_policy_validators.py @@ -8,6 +8,7 @@ authored by dstufft (https://github.com/dstufft) from __future__ import division import string +import unicodedata from nltk.metrics.distance import edit_distance from django.conf import settings @@ -63,7 +64,9 @@ def validate_password_complexity(value): if complexities is None: return + # Sets are here intentionally uppercase, lowercase, digits, non_ascii, punctuation = set(), set(), set(), set(), set() + alphabetic, numeric = [], [] for character in value: if character.isupper(): @@ -77,6 +80,11 @@ def validate_password_complexity(value): else: non_ascii.add(character) + 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 = [] @@ -92,6 +100,10 @@ def validate_password_complexity(value): errors.append(_("must contain {0} or more non ascii characters").format(complexities["NON ASCII"])) if len(words) < complexities.get("WORDS", 0): errors.append(_("must contain {0} or more unique words").format(complexities["WORDS"])) + if len(numeric) < complexities.get("NUMERIC", 0): + errors.append(_("must contain {0} or more numbers").format(complexities["NUMERIC"])) + if len(alphabetic) < complexities.get("ALPHABETIC", 0): + errors.append(_("must contain {0} or more letters").format(complexities["ALPHABETIC"])) if errors: raise ValidationError(message.format(u', '.join(errors)), code=code)