SECURITY: enforce passport status — revocation now takes effect live
All checks were successful
check / gate (push) Successful in 21s
All checks were successful
check / gate (push) Successful in 21s
A revoked passport returns HTTP 200, status=revoked, band=unproven, allowed_actions=[] (verified live 2026-08-13). resolve_passport keyed refusal only on HTTP 4xx and band=="untrusted", so it returned band 'unproven' and the agent was seated. Revocation was NOT enforced on the live auth path at all — and now that agent auth actually works, a revoked agent could authenticate and act. Extracts decide_trust(body) -> (band, actions) | raise. Only status=="active" is allowed; revoked/suspended/frozen/unknown all refuse, fail-closed on the field that carries the most consequential fact about an identity. The agent call site turns that into a clean 403 passport_revoked. This is the REAL revocation gate — the token cannot be un-issued, but its standing is re-checked on every request, so revocation takes effect on the next call with no webhook required. The Eternitas webhook remains useful for invalidating locally-issued credentials/grants (G6.3, not built yet), but it was never the primary gate and its being unwired is no longer a live exposure. Behavioral tests: revoked body refused, active accepted, unknown/missing status fails closed. Co-Authored-By: Claude (Fable 5) <noreply@anthropic.com>
This commit is contained in:
@@ -168,3 +168,32 @@ def test_every_throttled_action_has_a_configured_base():
|
||||
s = Settings()
|
||||
for action, field in ACTION_BASE.items():
|
||||
assert getattr(s, field) > 0, f"{action} has no positive base rate"
|
||||
|
||||
|
||||
# ---- revocation enforced on the live trust path (not just the webhook) ----
|
||||
def test_revoked_passport_is_refused_by_trust_decision():
|
||||
"""A revoked passport returns HTTP 200, status=revoked, band=unproven,
|
||||
allowed=[] (verified live 2026-08-13). The decision must refuse it — the
|
||||
old code returned band 'unproven' and seated the agent."""
|
||||
from api.app.auth import PassportNotInGoodStanding, decide_trust
|
||||
|
||||
revoked = {"status": "revoked", "band": "unproven", "allowed_actions": []}
|
||||
with pytest.raises(PassportNotInGoodStanding):
|
||||
decide_trust(revoked, "ET26-NJQT-QMR0")
|
||||
|
||||
|
||||
def test_active_passport_is_accepted_by_trust_decision():
|
||||
from api.app.auth import decide_trust
|
||||
|
||||
active = {"status": "active", "band": "gold", "allowed_actions": ["read", "send"]}
|
||||
band, actions = decide_trust(active, "ET26-1EF9-VJAN")
|
||||
assert band == "gold" and actions == ("read", "send")
|
||||
|
||||
|
||||
def test_unknown_or_missing_status_fails_closed():
|
||||
from api.app.auth import PassportNotInGoodStanding, decide_trust
|
||||
|
||||
for body in ({"band": "gold"}, {"status": "suspended"}, {"status": "frozen"},
|
||||
{"status": ""}, {}):
|
||||
with pytest.raises(PassportNotInGoodStanding):
|
||||
decide_trust(body, "ET26-X")
|
||||
|
||||
Reference in New Issue
Block a user