Browse Source

added have i been pawend api call

funding-tag
Holger Frey 5 years ago
parent
commit
8c9ee1668c
  1. 3
      ordr3/models.py
  2. 59
      ordr3/services.py
  3. 74
      tests/test_services.py

3
ordr3/models.py

@ -1,5 +1,8 @@
import enum import enum
from datetime import datetime from datetime import datetime
from collections import namedtuple
FlashMessage = namedtuple("FlashMessage", ["message", "description"])
@enum.unique @enum.unique

59
ordr3/services.py

@ -1,10 +1,11 @@
import hashlib
from datetime import datetime from datetime import datetime
from collections import namedtuple from collections import namedtuple
from urllib.parse import urlparse from urllib.parse import urlparse
from sqlalchemy.orm.exc import NoResultFound import requests
from . import models from . import models, security
CONSUMABLE_STATI = { CONSUMABLE_STATI = {
models.OrderStatus.ORDERED, models.OrderStatus.ORDERED,
@ -43,7 +44,7 @@ def create_log_entry(order, status, user):
def verify_credentials(repo, pass_ctx, username, password): def verify_credentials(repo, pass_ctx, username, password):
try: try:
user = repo.get_user_by_username(username) user = repo.get_user_by_username(username)
except (NoResultFound, StopIteration): except StopIteration:
# user was not found # user was not found
return None return None
valid, new_hash = pass_ctx.verify_and_update(password, user.password) valid, new_hash = pass_ctx.verify_and_update(password, user.password)
@ -87,3 +88,55 @@ def check_vendor_name(repo, to_check):
return CheckVendorResult(canonical_name, False) return CheckVendorResult(canonical_name, False)
else: else:
return CheckVendorResult(vendor, True) return CheckVendorResult(vendor, True)
def set_new_password(user, password):
crypt_context = security.get_passlib_context()
user.password = crypt_context.hash(password)
messages = []
pwd_length = len(password)
if pwd_length < 16:
messages.append(
models.FlashMessage(
f"Your password is quite short.",
(
f"Your Password has only {pwd_length} characters. "
'Have a look at <a href="https://www.xkcd.com/936/">'
"this comic strip</a> why longer passwords are better."
),
)
)
sha1_hash = hashlib.sha1(password.encode()).hexdigest()
if have_i_been_pwned(sha1_hash):
messages.append(
models.FlashMessage(
"This password appears in a breach or has been compromised.",
(
"Please consider changing your password. "
'<a href="/breached">Read this page</a> on why you '
"see this message."
),
)
)
return messages
def have_i_been_pwned(password_hash):
head, tails = password_hash[:5], password_hash[5:].lower()
url = f"https://api.pwnedpasswords.com/range/{head}"
headers = {"Add-Padding": "true"}
try:
response = requests.get(url, headers=headers, timeout=2)
response.raise_for_status()
except requests.RequestException:
return False
lower_case_lines = (line.lower() for line in response.text.splitlines())
for line in lower_case_lines:
if line.startswith(tails):
return True
return False

74
tests/test_services.py

@ -216,3 +216,77 @@ def test_check_vendor_name(input, name, found):
assert result.name == name assert result.name == name
assert result.found == found assert result.found == found
def test_have_i_been_pwned_ok():
from ordr3.services import have_i_been_pwned
assert not have_i_been_pwned("21BD2x")
def test_have_i_been_pwned_not_ok():
from ordr3.services import have_i_been_pwned
assert have_i_been_pwned("21BD2008F2FF3F9F3AE0A2072D19CD17E971B33A")
def test_have_i_been_pwned_request_exception():
from ordr3.services import have_i_been_pwned
assert not have_i_been_pwned("xxxxx")
def test_set_new_password_ok(monkeypatch):
from ordr3 import services
from ordr3.models import User
from ordr3.security import get_passlib_context
user = User(*list("ABCDEFG"))
monkeypatch.setattr(services, "have_i_been_pwned", lambda x: False)
result = services.set_new_password(user, "1234567890123456")
assert get_passlib_context().verify("1234567890123456", user.password)
assert result == []
def test_set_new_password_to_short(monkeypatch):
from ordr3 import services
from ordr3.models import User
from ordr3.security import get_passlib_context
user = User(*list("ABCDEFG"))
monkeypatch.setattr(services, "have_i_been_pwned", lambda x: False)
result = services.set_new_password(user, "1")
assert get_passlib_context().verify("1", user.password)
assert len(result) == 1
assert result[0].message.startswith("Your password is quite short")
def test_set_new_password_breached(monkeypatch):
from ordr3 import services
from ordr3.models import User
from ordr3.security import get_passlib_context
user = User(*list("ABCDEFG"))
monkeypatch.setattr(services, "have_i_been_pwned", lambda x: True)
result = services.set_new_password(user, "1234567890123456")
assert get_passlib_context().verify("1234567890123456", user.password)
assert len(result) == 1
assert result[0].message.startswith("This password appears in a breach")
def test_set_new_password_to_short_and_breached(monkeypatch):
from ordr3 import services
from ordr3.models import User
from ordr3.security import get_passlib_context
user = User(*list("ABCDEFG"))
monkeypatch.setattr(services, "have_i_been_pwned", lambda x: True)
result = services.set_new_password(user, "1")
assert get_passlib_context().verify("1", user.password)
assert len(result) == 2
assert result[0].message.startswith("Your password is quite short")
assert result[1].message.startswith("This password appears in a breach")

Loading…
Cancel
Save