diff --git a/passbook/policies/password/api.py b/passbook/policies/password/api.py index 6124d4bf1..c95ef2fff 100644 --- a/passbook/policies/password/api.py +++ b/passbook/policies/password/api.py @@ -12,6 +12,7 @@ class PasswordPolicySerializer(ModelSerializer): class Meta: model = PasswordPolicy fields = GENERAL_SERIALIZER_FIELDS + [ + "password_field", "amount_uppercase", "amount_lowercase", "amount_symbols", diff --git a/passbook/policies/password/forms.py b/passbook/policies/password/forms.py index a60fd522f..3b9e7695e 100644 --- a/passbook/policies/password/forms.py +++ b/passbook/policies/password/forms.py @@ -14,6 +14,7 @@ class PasswordPolicyForm(forms.ModelForm): model = PasswordPolicy fields = GENERAL_FIELDS + [ + "password_field", "amount_uppercase", "amount_lowercase", "amount_symbols", @@ -23,6 +24,7 @@ class PasswordPolicyForm(forms.ModelForm): ] widgets = { "name": forms.TextInput(), + "password_field": forms.TextInput(), "symbol_charset": forms.TextInput(), "error_message": forms.TextInput(), } diff --git a/passbook/policies/password/migrations/0002_passwordpolicy_password_field.py b/passbook/policies/password/migrations/0002_passwordpolicy_password_field.py new file mode 100644 index 000000000..84d5fe063 --- /dev/null +++ b/passbook/policies/password/migrations/0002_passwordpolicy_password_field.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-10 18:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_policies_password", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="passwordpolicy", + name="password_field", + field=models.TextField( + default="password", + help_text="Field key to check, field keys defined in Prompt stages are available.", + ), + ), + ] diff --git a/passbook/policies/password/models.py b/passbook/policies/password/models.py index 4f14da98d..f0250a2f4 100644 --- a/passbook/policies/password/models.py +++ b/passbook/policies/password/models.py @@ -14,6 +14,13 @@ LOGGER = get_logger() class PasswordPolicy(Policy): """Policy to make sure passwords have certain properties""" + password_field = models.TextField( + default="password", + help_text=_( + "Field key to check, field keys defined in Prompt stages are available." + ), + ) + amount_uppercase = models.IntegerField(default=0) amount_lowercase = models.IntegerField(default=0) amount_symbols = models.IntegerField(default=0) @@ -24,19 +31,29 @@ class PasswordPolicy(Policy): form = "passbook.policies.password.forms.PasswordPolicyForm" def passes(self, request: PolicyRequest) -> PolicyResult: - # Only check if password is being set - if not hasattr(request.user, "__password__"): - return PolicyResult(True) - password = getattr(request.user, "__password__") + if self.password_field not in request.context: + LOGGER.warning( + "Password field not set in Policy Request", + field=self.password_field, + fields=request.context.keys(), + ) + password = request.context[self.password_field] - filter_regex = r"" + filter_regex = [] if self.amount_lowercase > 0: - filter_regex += r"[a-z]{%d,}" % self.amount_lowercase + filter_regex.append(r"[a-z]{%d,}" % self.amount_lowercase) if self.amount_uppercase > 0: - filter_regex += r"[A-Z]{%d,}" % self.amount_uppercase + filter_regex.append(r"[A-Z]{%d,}" % self.amount_uppercase) if self.amount_symbols > 0: - filter_regex += r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols) - result = bool(re.compile(filter_regex).match(password)) + filter_regex.append( + r"[%s]{%d,}" % (self.symbol_charset, self.amount_symbols) + ) + full_regex = "|".join(filter_regex) + LOGGER.debug("Built regex", regexp=full_regex) + result = bool(re.compile(full_regex).match(password)) + + result = result and len(password) >= self.length_min + if not result: return PolicyResult(result, self.error_message) return PolicyResult(result) diff --git a/passbook/policies/password/tests.py b/passbook/policies/password/tests.py new file mode 100644 index 000000000..51875ff6d --- /dev/null +++ b/passbook/policies/password/tests.py @@ -0,0 +1,42 @@ +"""Password Policy tests""" +from django.test import TestCase +from guardian.shortcuts import get_anonymous_user + +from passbook.policies.password.models import PasswordPolicy +from passbook.policies.types import PolicyRequest, PolicyResult + + +class TestPasswordPolicy(TestCase): + """Test Password Policy""" + + def test_false(self): + """Failing password case""" + policy = PasswordPolicy.objects.create( + name="test_false", + amount_uppercase=1, + amount_lowercase=2, + amount_symbols=3, + length_min=24, + error_message="test message", + ) + request = PolicyRequest(get_anonymous_user()) + request.context["password"] = "test" + result: PolicyResult = policy.passes(request) + self.assertFalse(result.passing) + self.assertEqual(result.messages, ("test message",)) + + def test_true(self): + """Positive password case""" + policy = PasswordPolicy.objects.create( + name="test_true", + amount_uppercase=1, + amount_lowercase=2, + amount_symbols=3, + length_min=3, + error_message="test message", + ) + request = PolicyRequest(get_anonymous_user()) + request.context["password"] = "Test()!" + result: PolicyResult = policy.passes(request) + self.assertTrue(result.passing) + self.assertEqual(result.messages, tuple()) diff --git a/swagger.yaml b/swagger.yaml index 3cfe4a277..b4bd00851 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5838,6 +5838,11 @@ definitions: title: Name type: string x-nullable: true + password_field: + title: Password field + description: Field key to check, field keys defined in Prompt stages are available. + type: string + minLength: 1 amount_uppercase: title: Amount uppercase type: integer