Prepare glob-scoped callers and prove offsite ledger recovery
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a11fd9-caf6-7a63-b578-9daaaa134ff2
This commit is contained in:
parent
6fb206b359
commit
93b6ba2aae
21 changed files with 1080 additions and 19 deletions
|
|
@ -412,3 +412,152 @@ def test_invalid_policy_refuses_startup_before_database_mutation(tmp_path, monke
|
|||
with pytest.raises(ValueError, match="caller_configuration_invalid"):
|
||||
main()
|
||||
assert not (tmp_path / "not-created.db").exists()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("outcome", ["accepted", "unknown", "permanent"])
|
||||
def test_original_caller_adds_binding_without_replaying_legacy_mail(tmp_path, outcome):
|
||||
import hashlib
|
||||
|
||||
store = SQLiteDeliveryStore(str(tmp_path / "ledger.db"))
|
||||
provider = Provider()
|
||||
old = event("user-engine")
|
||||
old.pop("client_id")
|
||||
fingerprint = hashlib.sha256(
|
||||
json.dumps(old, sort_keys=True, separators=(",", ":")).encode()
|
||||
).hexdigest()
|
||||
store.reserve_send(old["id"], fingerprint)
|
||||
if outcome == "accepted":
|
||||
store.record(old["id"], "synthetic-legacy-reference")
|
||||
elif outcome == "permanent":
|
||||
store.send_failed(
|
||||
old["id"], True, ProviderError("permanent_rejection", retryable=False)
|
||||
)
|
||||
admitted = policy("user-engine", adopt_legacy_invitations=True)
|
||||
scoped = ScopedTransactionalApplication(
|
||||
store, provider, "https://issuer.example.test", [admitted]
|
||||
)
|
||||
bound = dict(old, client_id="client:a")
|
||||
status, body, _ = invoke(scoped, bound, caller="user-engine")
|
||||
if outcome == "accepted":
|
||||
assert status == 200 and body["status"] == "duplicate"
|
||||
else:
|
||||
assert status == 422
|
||||
assert body["error"] == (
|
||||
"delivery_outcome_unknown"
|
||||
if outcome == "unknown"
|
||||
else "permanent_rejection"
|
||||
)
|
||||
assert provider.calls == []
|
||||
assert store.db.execute("SELECT fingerprint FROM send_attempts").fetchall() == [
|
||||
(fingerprint,)
|
||||
]
|
||||
status, body, _ = invoke(
|
||||
scoped,
|
||||
dict(bound, data=dict(bound["data"], primary_email="changed@example.test")),
|
||||
caller="user-engine",
|
||||
)
|
||||
assert status == 400 and body["error"] == "idempotency_payload_mismatch"
|
||||
status, body, _ = invoke(
|
||||
scoped, dict(bound, tenant="tenant:denied"), caller="user-engine"
|
||||
)
|
||||
assert status == 400 and body["error"] == "caller_scope_denied"
|
||||
status, body, _ = invoke(
|
||||
scoped,
|
||||
{"event_id": old["id"]},
|
||||
caller="user-engine",
|
||||
path="/v1/delivery-status",
|
||||
)
|
||||
assert status == 200
|
||||
assert (
|
||||
body["state"]
|
||||
== {
|
||||
"accepted": "provider_accepted",
|
||||
"unknown": "unknown",
|
||||
"permanent": "failed",
|
||||
}[outcome]
|
||||
)
|
||||
assert provider.calls == []
|
||||
|
||||
|
||||
def test_invitation_adoption_requires_original_caller_and_boolean():
|
||||
with pytest.raises(ValueError, match="legacy_admission_requires_original_caller"):
|
||||
policy(adopt_legacy_invitations=True)
|
||||
with pytest.raises(ValueError, match="invalid_legacy_admission"):
|
||||
policy("user-engine", adopt_legacy_invitations="true")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tenant,client,allowed",
|
||||
[
|
||||
("tenant:family-one", "client:app", True),
|
||||
("tenant:family-two", "client:app", True),
|
||||
("tenant:platform", "client:app", False),
|
||||
("tenant:family-one", "client:other", False),
|
||||
("tenant:Family-one", "client:app", False),
|
||||
("tenant:family-one/foreign", "client:app", False),
|
||||
],
|
||||
)
|
||||
def test_binding_globs_match_whole_case_sensitive_valid_identifiers(
|
||||
tmp_path, tenant, client, allowed
|
||||
):
|
||||
admitted = policy(binding_patterns=frozenset({("tenant:family-*", "client:app")}))
|
||||
app, _, provider = application(tmp_path, [admitted])
|
||||
status, body, _ = invoke(app, event(tenant=tenant, client_id=client))
|
||||
assert status == (202 if allowed else 400)
|
||||
assert len(provider.calls) == int(allowed)
|
||||
if not allowed:
|
||||
assert body["error"] == "caller_scope_denied"
|
||||
|
||||
|
||||
def test_glob_binding_applies_to_verification_and_consumption(tmp_path):
|
||||
admitted = policy(
|
||||
bindings=frozenset(),
|
||||
binding_patterns=frozenset({("tenant:family-*", "client:?")}),
|
||||
)
|
||||
app, _, provider = application(tmp_path, [admitted])
|
||||
status, body, _ = invoke(
|
||||
app,
|
||||
verification(tenant="tenant:family-one"),
|
||||
path="/v1/registration-verifications",
|
||||
)
|
||||
assert status == 202
|
||||
handle = provider.calls[0][2].split("handle=", 1)[1].splitlines()[0]
|
||||
status, _, _ = invoke(
|
||||
app, {"handle": handle}, path="/v1/registration-verifications/consume"
|
||||
)
|
||||
assert status == 200
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pattern", ["(tenant:.*)", "tenant:[ab]", "tenant:/other", "*" * 129]
|
||||
)
|
||||
def test_invalid_or_unbounded_binding_patterns_refuse_startup(pattern):
|
||||
with pytest.raises(ValueError, match="invalid_caller_binding_pattern"):
|
||||
policy(binding_patterns=frozenset({(pattern, "client:a")}))
|
||||
|
||||
|
||||
def test_new_adopted_invitation_keeps_bound_fingerprint_across_restart(tmp_path):
|
||||
admitted = policy("user-engine", adopt_legacy_invitations=True)
|
||||
app, store, _ = application(tmp_path, [admitted])
|
||||
assert invoke(app, event("user-engine"), caller="user-engine")[0] == 202
|
||||
store.db.close()
|
||||
recovered, _, provider = application(tmp_path, [admitted])
|
||||
assert (
|
||||
invoke(recovered, event("user-engine"), caller="user-engine")[1]["status"]
|
||||
== "duplicate"
|
||||
)
|
||||
assert (
|
||||
invoke(
|
||||
recovered,
|
||||
event(
|
||||
"user-engine",
|
||||
data={
|
||||
"primary_email": "changed@example.test",
|
||||
"invitation_id": "invite-1",
|
||||
},
|
||||
),
|
||||
caller="user-engine",
|
||||
)[0]
|
||||
== 400
|
||||
)
|
||||
assert provider.calls == []
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue