policies/hibp: update for flows, add unittests

This commit is contained in:
Jens Langhammer 2020-07-10 20:57:15 +02:00
parent 5bcf2aef8c
commit d74366f413
7 changed files with 78 additions and 12 deletions

View File

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

View File

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

View File

@ -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),
}

View File

@ -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.",
),
),
]

View File

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

View File

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

View File

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