Add rotating create-only requester provider and owner review
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1046b-5629-7f52-badd-f718eadb6042
This commit is contained in:
parent
b65363527e
commit
eb9614c267
4 changed files with 283 additions and 1 deletions
91
informed_decision/requester.py
Normal file
91
informed_decision/requester.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
"""Create-only requester exchange from rotating projected custody.
|
||||
|
||||
No access token or credential cache, environment delivery, refresh grant or
|
||||
browser identity. Public issuer endpoints and claims are pinned locally.
|
||||
"""
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlencode
|
||||
import time
|
||||
|
||||
import jwt
|
||||
|
||||
from .http_transport import JSONTransport, https_origin
|
||||
|
||||
CLIENT_ID = "informed-decision-sitting-requester"
|
||||
|
||||
|
||||
class RequesterError(Exception):
|
||||
"""Fixed refusal; never retains an upstream body, credential or token."""
|
||||
|
||||
|
||||
class RequesterTokenProvider:
|
||||
def __init__(self, issuer, secret_file, *, transport=None, clock=time.time):
|
||||
self.issuer = https_origin(issuer)
|
||||
self.secret_file = Path(secret_file)
|
||||
if not self.secret_file.is_absolute():
|
||||
raise ValueError("absolute requester credential path required")
|
||||
self.transport = transport or JSONTransport()
|
||||
self.clock = clock
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
# Reopen Kubernetes's projected symlink every exchange. Never copy
|
||||
# custody to the evidence volume, strip it, or hold a cached token.
|
||||
with self.secret_file.open("rb") as handle:
|
||||
raw = handle.read(4097)
|
||||
secret = raw.decode("ascii")
|
||||
if not secret or len(raw) > 4096 or any(c.isspace() for c in secret):
|
||||
raise ValueError()
|
||||
status, result = self.transport.request("POST", self.issuer + "/token",
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
body=urlencode({"grant_type": "client_credentials", "client_id": CLIENT_ID,
|
||||
"client_secret": secret, "scope": "approval:create"}).encode())
|
||||
if (status != 200 or not isinstance(result.get("token_type"), str)
|
||||
or result["token_type"].lower() != "bearer"
|
||||
or "refresh_token" in result):
|
||||
raise ValueError()
|
||||
token = result.get("access_token")
|
||||
if not isinstance(token, str) or not token or len(token) > 16384:
|
||||
raise ValueError()
|
||||
header = jwt.get_unverified_header(token)
|
||||
if header.get("alg") != "RS256" or not isinstance(header.get("kid"), str) or not header["kid"]:
|
||||
raise ValueError()
|
||||
status, jwks = self.transport.request("GET", self.issuer + "/jwks")
|
||||
if status != 200 or not isinstance(jwks.get("keys"), list):
|
||||
raise ValueError()
|
||||
keys = [key for key in jwks["keys"] if key.get("kid") == header["kid"]]
|
||||
if (len(keys) != 1 or keys[0].get("kty") != "RSA"
|
||||
or keys[0].get("use", "sig") != "sig" or keys[0].get("alg", "RS256") != "RS256"):
|
||||
raise ValueError()
|
||||
key = jwt.PyJWK.from_dict(keys[0], algorithm="RS256").key
|
||||
claims = jwt.decode(token, key, algorithms=["RS256"], issuer=self.issuer,
|
||||
audience="approval-engine", options={"require": ["iss", "aud", "sub", "iat", "exp"],
|
||||
"strict_aud": True})
|
||||
now = self.clock()
|
||||
if (claims.get("sub") != "informed-decision"
|
||||
or claims.get("tenant") != "tenant:platform"
|
||||
or claims.get("principal_type") != "service"
|
||||
or claims.get("roles") != [CLIENT_ID]
|
||||
or claims.get("scope") != "approval:create"
|
||||
or type(claims["iat"]) is not int or type(claims["exp"]) is not int
|
||||
or claims["iat"] > now or claims["exp"] <= now
|
||||
or not 0 < claims["exp"] - claims["iat"] <= 900):
|
||||
raise ValueError()
|
||||
return token
|
||||
except Exception:
|
||||
raise RequesterError("requester exchange refused") from None
|
||||
|
||||
|
||||
def compose_intake(store, token_review_call, *, issuer, secret_file,
|
||||
approval_origin, submitters_path, groups_path, transport=None):
|
||||
"""Owner composition seam; caller owns CA-verified TokenReview and listener.
|
||||
|
||||
This factory grants no submitter and starts no socket. The separate private
|
||||
listener and deployment remain reviewed owner configuration.
|
||||
"""
|
||||
from .intake_approval import IntakeApprovalClient
|
||||
from .intake_service import IntakeService, TokenReview
|
||||
provider = RequesterTokenProvider(issuer, secret_file, transport=transport)
|
||||
creator = IntakeApprovalClient(approval_origin, provider, allow_internal_http=True)
|
||||
return IntakeService(store, TokenReview(token_review_call), creator,
|
||||
submitters_path=submitters_path, groups_path=groups_path)
|
||||
Loading…
Add table
Add a link
Reference in a new issue