diff --git a/azure-pipelines.yml b/azure-pipelines.yml index caec20644..62cfa8213 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -139,7 +139,7 @@ stages: displayName: Run full test suite inputs: script: | - pipenv run coverage run ./manage.py test --failfast passbook + pipenv run coverage run ./manage.py test passbook mkdir output-unittest mv unittest.xml output-unittest/unittest.xml mv .coverage output-unittest/coverage @@ -182,7 +182,7 @@ stages: displayName: Run full test suite inputs: script: | - pipenv run coverage run ./manage.py test --failfast e2e + pipenv run coverage run ./manage.py test e2e mkdir output-e2e mv unittest.xml output-e2e/unittest.xml mv .coverage output-e2e/coverage diff --git a/passbook/policies/hibp/api.py b/passbook/policies/hibp/api.py index 1c2034edf..03c5a736a 100644 --- a/passbook/policies/hibp/api.py +++ b/passbook/policies/hibp/api.py @@ -11,7 +11,7 @@ class HaveIBeenPwendPolicySerializer(ModelSerializer): class Meta: model = HaveIBeenPwendPolicy - fields = GENERAL_SERIALIZER_FIELDS + ["allowed_count"] + fields = GENERAL_SERIALIZER_FIELDS + ["password_field", "allowed_count"] class HaveIBeenPwendPolicyViewSet(ModelViewSet): diff --git a/passbook/policies/hibp/forms.py b/passbook/policies/hibp/forms.py index f9560bf14..f8f39df0e 100644 --- a/passbook/policies/hibp/forms.py +++ b/passbook/policies/hibp/forms.py @@ -14,9 +14,9 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm): class Meta: model = HaveIBeenPwendPolicy - fields = GENERAL_FIELDS + ["allowed_count"] + fields = GENERAL_FIELDS + ["password_field", "allowed_count"] widgets = { "name": forms.TextInput(), - "order": forms.NumberInput(), + "password_field": forms.TextInput(), "policies": FilteredSelectMultiple(_("policies"), False), } diff --git a/passbook/policies/hibp/migrations/0002_haveibeenpwendpolicy_password_field.py b/passbook/policies/hibp/migrations/0002_haveibeenpwendpolicy_password_field.py new file mode 100644 index 000000000..8cf6d60fc --- /dev/null +++ b/passbook/policies/hibp/migrations/0002_haveibeenpwendpolicy_password_field.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.8 on 2020-07-10 18:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_policies_hibp", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="haveibeenpwendpolicy", + 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/hibp/models.py b/passbook/policies/hibp/models.py index 613594474..90407321d 100644 --- a/passbook/policies/hibp/models.py +++ b/passbook/policies/hibp/models.py @@ -6,8 +6,8 @@ from django.utils.translation import gettext as _ from requests import get from structlog import get_logger -from passbook.core.models import User from passbook.policies.models import Policy, PolicyResult +from passbook.policies.types import PolicyRequest LOGGER = get_logger() @@ -16,20 +16,31 @@ class HaveIBeenPwendPolicy(Policy): """Check if password is on HaveIBeenPwned's list by uploading the first 5 characters of the SHA1 Hash.""" + password_field = models.TextField( + default="password", + help_text=_( + "Field key to check, field keys defined in Prompt stages are available." + ), + ) + allowed_count = models.IntegerField(default=0) form = "passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm" - def passes(self, user: User) -> PolicyResult: + def passes(self, request: PolicyRequest) -> PolicyResult: """Check if password is in HIBP DB. Hashes given Password with SHA1, uses the first 5 characters of Password in request and checks if full hash is in response. Returns 0 if Password is not in result otherwise the count of how many times it was used.""" - # Only check if password is being set - if not hasattr(user, "__password__"): - return PolicyResult(True) - password = getattr(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] + pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec - url = "https://api.pwnedpasswords.com/range/%s" % pw_hash[:5] + url = f"https://api.pwnedpasswords.com/range/{pw_hash[:5]}" result = get(url).text final_count = 0 for line in result.split("\r\n"): diff --git a/passbook/policies/hibp/tests.py b/passbook/policies/hibp/tests.py new file mode 100644 index 000000000..6c70256d1 --- /dev/null +++ b/passbook/policies/hibp/tests.py @@ -0,0 +1,29 @@ +"""HIBP Policy tests""" +from django.test import TestCase +from guardian.shortcuts import get_anonymous_user +from oauth2_provider.generators import generate_client_secret + +from passbook.policies.hibp.models import HaveIBeenPwendPolicy +from passbook.policies.types import PolicyRequest, PolicyResult + + +class TestHIBPPolicy(TestCase): + """Test HIBP Policy""" + + def test_false(self): + """Failing password case""" + policy = HaveIBeenPwendPolicy.objects.create(name="test_false",) + request = PolicyRequest(get_anonymous_user()) + request.context["password"] = "password" + result: PolicyResult = policy.passes(request) + self.assertFalse(result.passing) + self.assertTrue(result.messages[0].startswith("Password exists on ")) + + def test_true(self): + """Positive password case""" + policy = HaveIBeenPwendPolicy.objects.create(name="test_true",) + request = PolicyRequest(get_anonymous_user()) + request.context["password"] = generate_client_secret() + result: PolicyResult = policy.passes(request) + self.assertTrue(result.passing) + self.assertEqual(result.messages, tuple()) diff --git a/swagger.yaml b/swagger.yaml index b4bd00851..b224c1175 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -5819,6 +5819,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 allowed_count: title: Allowed count type: integer