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 displayName: Run full test suite
inputs: inputs:
script: | script: |
pipenv run coverage run ./manage.py test --failfast passbook pipenv run coverage run ./manage.py test passbook
mkdir output-unittest mkdir output-unittest
mv unittest.xml output-unittest/unittest.xml mv unittest.xml output-unittest/unittest.xml
mv .coverage output-unittest/coverage mv .coverage output-unittest/coverage
@ -182,7 +182,7 @@ stages:
displayName: Run full test suite displayName: Run full test suite
inputs: inputs:
script: | script: |
pipenv run coverage run ./manage.py test --failfast e2e pipenv run coverage run ./manage.py test e2e
mkdir output-e2e mkdir output-e2e
mv unittest.xml output-e2e/unittest.xml mv unittest.xml output-e2e/unittest.xml
mv .coverage output-e2e/coverage mv .coverage output-e2e/coverage

View File

@ -11,7 +11,7 @@ class HaveIBeenPwendPolicySerializer(ModelSerializer):
class Meta: class Meta:
model = HaveIBeenPwendPolicy model = HaveIBeenPwendPolicy
fields = GENERAL_SERIALIZER_FIELDS + ["allowed_count"] fields = GENERAL_SERIALIZER_FIELDS + ["password_field", "allowed_count"]
class HaveIBeenPwendPolicyViewSet(ModelViewSet): class HaveIBeenPwendPolicyViewSet(ModelViewSet):

View File

@ -14,9 +14,9 @@ class HaveIBeenPwnedPolicyForm(forms.ModelForm):
class Meta: class Meta:
model = HaveIBeenPwendPolicy model = HaveIBeenPwendPolicy
fields = GENERAL_FIELDS + ["allowed_count"] fields = GENERAL_FIELDS + ["password_field", "allowed_count"]
widgets = { widgets = {
"name": forms.TextInput(), "name": forms.TextInput(),
"order": forms.NumberInput(), "password_field": forms.TextInput(),
"policies": FilteredSelectMultiple(_("policies"), False), "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 requests import get
from structlog import get_logger from structlog import get_logger
from passbook.core.models import User
from passbook.policies.models import Policy, PolicyResult from passbook.policies.models import Policy, PolicyResult
from passbook.policies.types import PolicyRequest
LOGGER = get_logger() LOGGER = get_logger()
@ -16,20 +16,31 @@ class HaveIBeenPwendPolicy(Policy):
"""Check if password is on HaveIBeenPwned's list by uploading the first """Check if password is on HaveIBeenPwned's list by uploading the first
5 characters of the SHA1 Hash.""" 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) allowed_count = models.IntegerField(default=0)
form = "passbook.policies.hibp.forms.HaveIBeenPwnedPolicyForm" 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 """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 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.""" if Password is not in result otherwise the count of how many times it was used."""
# Only check if password is being set if self.password_field not in request.context:
if not hasattr(user, "__password__"): LOGGER.warning(
return PolicyResult(True) "Password field not set in Policy Request",
password = getattr(user, "__password__") field=self.password_field,
fields=request.context.keys(),
)
password = request.context[self.password_field]
pw_hash = sha1(password.encode("utf-8")).hexdigest() # nosec 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 result = get(url).text
final_count = 0 final_count = 0
for line in result.split("\r\n"): 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 title: Name
type: string type: string
x-nullable: true 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: allowed_count:
title: Allowed count title: Allowed count
type: integer type: integer