Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a092fe-13b1-7f12-ac74-7d258af4d79c
84 lines
4.1 KiB
Python
84 lines
4.1 KiB
Python
import pathlib
|
|
import sys
|
|
import unittest
|
|
|
|
sys.path.insert(0, str(pathlib.Path(__file__).parents[1]))
|
|
from password_setup import PasswordSetupGrants
|
|
|
|
|
|
class PasswordSetupGrantTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.now = 100.0
|
|
self.calls = []
|
|
self.grants = PasswordSetupGrants(
|
|
public_url="https://kc.example/setup/password",
|
|
setter=lambda subject, password: self.calls.append((subject, password)),
|
|
ttl_seconds=60,
|
|
clock=lambda: self.now,
|
|
)
|
|
|
|
def token(self, url):
|
|
return url.partition("token=")[2]
|
|
|
|
def test_grant_is_single_use(self):
|
|
token = self.token(self.grants.issue("binky-admin"))
|
|
self.assertTrue(self.grants.valid(token))
|
|
self.grants.consume(token, "a-secure-password")
|
|
self.assertEqual([("binky-admin", "a-secure-password")], self.calls)
|
|
self.assertFalse(self.grants.valid(token))
|
|
with self.assertRaisesRegex(ValueError, "invalid or expired"):
|
|
self.grants.consume(token, "a-secure-password")
|
|
|
|
def test_new_grant_revokes_previous_subject_grant(self):
|
|
first = self.token(self.grants.issue("binky-admin"))
|
|
second = self.token(self.grants.issue("binky-admin"))
|
|
self.assertFalse(self.grants.valid(first))
|
|
self.assertTrue(self.grants.valid(second))
|
|
|
|
def test_expired_grant_fails_closed(self):
|
|
token = self.token(self.grants.issue("binky-admin"))
|
|
self.now = 161.0
|
|
self.assertFalse(self.grants.valid(token))
|
|
with self.assertRaisesRegex(ValueError, "invalid or expired"):
|
|
self.grants.consume(token, "a-secure-password")
|
|
|
|
def test_password_policy_precedes_consumption(self):
|
|
token = self.token(self.grants.issue("binky-admin"))
|
|
with self.assertRaisesRegex(ValueError, "12 characters"):
|
|
self.grants.consume(token, "too-short")
|
|
self.assertTrue(self.grants.valid(token))
|
|
|
|
|
|
class CompanyReturnTests(unittest.TestCase):
|
|
def test_return_is_bound_to_grant_and_cannot_be_changed_by_browser(self):
|
|
mapping = {"tenant:trial:demo-company": "https://vergabe.example/demo-company/"}
|
|
grants = PasswordSetupGrants(public_url="https://kc.example/setup/password",
|
|
setter=lambda *args: None, tenant_returns=mapping)
|
|
url = grants.issue("recipient", "tenant:trial:demo-company")
|
|
token = url.partition("token=")[2]
|
|
mapping["tenant:trial:demo-company"] = "https://attacker.example/"
|
|
self.assertNotIn("recipient", url)
|
|
self.assertNotIn("return", url)
|
|
self.assertEqual("https://vergabe.example/demo-company/", grants.consume(token, "test-password-long"))
|
|
with self.assertRaises(ValueError):
|
|
grants.consume(token, "test-password-long")
|
|
|
|
def test_unknown_tenant_has_no_return_and_bad_targets_fail(self):
|
|
for target in ("http://example.test/", "https://example.test/?next=evil",
|
|
"https://example.test/#fragment", "https://user:password@example.test/"):
|
|
with self.assertRaises(ValueError):
|
|
PasswordSetupGrants(public_url="https://kc.example/setup/password",
|
|
setter=lambda *args: None, tenant_returns={"tenant:trial:demo": target})
|
|
grants = PasswordSetupGrants(public_url="https://kc.example/setup/password", setter=lambda *args: None)
|
|
token = grants.issue("recipient", "tenant:unknown").partition("token=")[2]
|
|
self.assertEqual("", grants.consume(token, "test-password-long"))
|
|
|
|
def test_expiry_and_failure_never_release_a_return(self):
|
|
clock = [0]
|
|
grants = PasswordSetupGrants(public_url="https://kc.example/setup/password",
|
|
setter=lambda *args: None, clock=lambda: clock[0], ttl_seconds=1,
|
|
tenant_returns={"tenant:trial:demo": "https://app.example/demo/"})
|
|
token = grants.issue("recipient", "tenant:trial:demo").partition("token=")[2]
|
|
clock[0] = 2
|
|
with self.assertRaises(ValueError):
|
|
grants.consume(token, "test-password-long")
|