"""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. """ import base64 from pathlib import Path from urllib.parse import urlencode, quote_plus 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() credentials = base64.b64encode( (quote_plus(CLIENT_ID) + ":" + quote_plus(secret)).encode("ascii") ).decode("ascii") status, result = self.transport.request("POST", self.issuer + "/token", headers={"Content-Type": "application/x-www-form-urlencoded", "Authorization": "Basic " + credentials}, body=urlencode({"grant_type": "client_credentials", "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)