Close the second round of review findings: GrantedCode, no minting from a name
This commit is contained in:
parent
d402d7e498
commit
8ead217556
@ -92,25 +92,50 @@ def issue_agent_session(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def issue_view_session(
|
class GrantedCode:
|
||||||
store: Store,
|
"""Proof that a one-time view code was redeemed.
|
||||||
agent: str,
|
|
||||||
scopes: Optional[List[str]] = None,
|
|
||||||
ttl: int = 3600,
|
|
||||||
granted_by: Optional[str] = None,
|
|
||||||
) -> dict:
|
|
||||||
"""Mint a human_via_agent session.
|
|
||||||
|
|
||||||
Plumbing primitive, not an entry point: it trusts whoever calls it. Code
|
Constructed only by viewcodes.redeem_code(), and only from a code that
|
||||||
reached from outside the process must authenticate first. issue_code()
|
actually matched. Minting a human session now requires one of these, so the
|
||||||
enforces that by requiring an agent session, and redeem_code() may present a
|
session minting path cannot be reached by passing an agent name.
|
||||||
one-time code as its credential.
|
|
||||||
|
|
||||||
`granted_by` is the agent that approved the code. It is recorded so the
|
This is a guardrail against wiring mistakes, not a security boundary: any
|
||||||
audit trail can answer "who let this human in" and not merely "which
|
code running in this process can construct it. The boundary that matters is
|
||||||
agent's things did they see".
|
that no network-reachable route reaches this function without either an
|
||||||
|
agent session (issue_code) or a valid code (redeem_code).
|
||||||
"""
|
"""
|
||||||
granted = list(scopes if scopes is not None else HUMAN_VIEW_SCOPES)
|
|
||||||
|
__slots__ = ("agent", "scopes", "ttl", "_sigil_internal")
|
||||||
|
|
||||||
|
def __init__(self, agent: str, scopes: List[str], ttl: int, _internal=None):
|
||||||
|
if _internal is not _ISSUER:
|
||||||
|
raise errors.SigilError(
|
||||||
|
"GrantedCode cannot be constructed directly; it comes from"
|
||||||
|
" viewcodes.redeem_code()"
|
||||||
|
)
|
||||||
|
self.agent = agent
|
||||||
|
self.scopes = list(scopes)
|
||||||
|
self.ttl = ttl
|
||||||
|
self._sigil_internal = _internal
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"<GrantedCode agent={self.agent!r}>"
|
||||||
|
|
||||||
|
|
||||||
|
_ISSUER = object()
|
||||||
|
|
||||||
|
|
||||||
|
def issue_view_session(store: Store, grant: "GrantedCode") -> dict:
|
||||||
|
"""Mint a human_via_agent session from a redeemed code.
|
||||||
|
|
||||||
|
Takes a GrantedCode rather than an agent name, so the only way to reach a
|
||||||
|
human session is to have redeemed a code. `granted_by` is the agent that
|
||||||
|
approved it, recorded so the audit trail can answer "who let this human in"
|
||||||
|
and not merely "which agent's things did they see".
|
||||||
|
"""
|
||||||
|
agent = grant.agent
|
||||||
|
granted = list(grant.scopes)
|
||||||
|
ttl = grant.ttl
|
||||||
extra = [s for s in granted if s not in HUMAN_VIEW_SCOPES]
|
extra = [s for s in granted if s not in HUMAN_VIEW_SCOPES]
|
||||||
if extra:
|
if extra:
|
||||||
# Defence in depth: even if a caller tries to widen a human session,
|
# Defence in depth: even if a caller tries to widen a human session,
|
||||||
@ -125,7 +150,7 @@ def issue_view_session(
|
|||||||
agent,
|
agent,
|
||||||
granted,
|
granted,
|
||||||
expires,
|
expires,
|
||||||
granted_by=granted_by or agent,
|
granted_by=agent,
|
||||||
)
|
)
|
||||||
store.record(
|
store.record(
|
||||||
"session.issued",
|
"session.issued",
|
||||||
@ -135,7 +160,7 @@ def issue_view_session(
|
|||||||
"kind": HUMAN_VIA_AGENT,
|
"kind": HUMAN_VIA_AGENT,
|
||||||
"scopes": granted,
|
"scopes": granted,
|
||||||
"expires_at": expires,
|
"expires_at": expires,
|
||||||
"granted_by": granted_by or agent,
|
"granted_by": agent,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@ -143,7 +168,7 @@ def issue_view_session(
|
|||||||
"kind": HUMAN_VIA_AGENT,
|
"kind": HUMAN_VIA_AGENT,
|
||||||
"subject": f"{agent}'s human",
|
"subject": f"{agent}'s human",
|
||||||
"on_behalf_of": agent,
|
"on_behalf_of": agent,
|
||||||
"granted_by": granted_by or agent,
|
"granted_by": agent,
|
||||||
"scopes": granted,
|
"scopes": granted,
|
||||||
"expires_at": expires,
|
"expires_at": expires,
|
||||||
"ttl": ttl,
|
"ttl": ttl,
|
||||||
@ -198,4 +223,6 @@ def assert_may_act_as_agent(session: dict) -> str:
|
|||||||
|
|
||||||
def revoke(store: Store, token: str) -> None:
|
def revoke(store: Store, token: str) -> None:
|
||||||
store.revoke_session(hash_token(token))
|
store.revoke_session(hash_token(token))
|
||||||
store.record("session.revoked", actor="system", detail={"token_prefix": token[:8]})
|
# A digest, not a prefix of the token: the log outlives the session.
|
||||||
|
store.record("session.revoked", actor="system",
|
||||||
|
detail={"token_ref": "tok:" + hash_token(token)[:12]})
|
||||||
Loading…
x
Reference in New Issue
Block a user