From 590c267f032414ab5a4342cb4c3e530c218d52974e382100f35d3b5236a2fe96 Mon Sep 17 00:00:00 2001 From: Tyler J King Date: Fri, 3 Apr 2026 09:06:45 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20on=5Fbehalf=5Fof=20=E2=80=94=20Bascule?= =?UTF-8?q?=20asserts=20operator=20identity=20in=20AC=20requests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a trusted caller (Bascule SA) sets on_behalf_of in the authorize request, the AC principal is the specified operator DID instead of the SA's JWT identity. Display name extracted from last DID segment. Co-Authored-By: Claude Opus 4.6 (1M context) --- gsap_broker/models/gsap.py | 3 +++ gsap_broker/routers/authorize.py | 10 +++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/gsap_broker/models/gsap.py b/gsap_broker/models/gsap.py index 82a61e9..598eb48 100644 --- a/gsap_broker/models/gsap.py +++ b/gsap_broker/models/gsap.py @@ -76,6 +76,9 @@ class AuthorizeRequest(BaseModel): accord_template: str driver_id: str session_mode: bool = False # When true, AC stays active across multiple CRs + # When set by a trusted caller (e.g. Bascule), overrides the driver-derived principal. + # The SA authenticates the request; this field says who the AC is FOR. + on_behalf_of: Optional[str] = None class AuthorizeResponse(BaseModel): status: str diff --git a/gsap_broker/routers/authorize.py b/gsap_broker/routers/authorize.py index 1b1fb7b..544af22 100644 --- a/gsap_broker/routers/authorize.py +++ b/gsap_broker/routers/authorize.py @@ -67,23 +67,27 @@ async def authorize(body: AuthorizeRequest, http_request: Request, db: AsyncSess expires = now + timedelta(minutes=settings.ac_ttl_minutes) ctx_id = uuid.uuid4() + # on_behalf_of: trusted caller (Bascule SA) asserts who the AC is for + principal_did = request.on_behalf_of or auth_result.principal_did + display_name = request.on_behalf_of.rsplit("/", 1)[-1] if request.on_behalf_of else auth_result.display_name + ac = AuthorizationContext( context_id=ctx_id, issued_at=now, expires_at=expires, - principal=Principal(did=auth_result.principal_did, display_name=auth_result.display_name, driver_id=request.driver_id), + principal=Principal(did=principal_did, display_name=display_name, driver_id=request.driver_id), accord=Accord(template=request.accord_template), operation=Operation(playbook=request.playbook, corpus_entry_cid=request.corpus_entry_cid, parameters_cid=request.parameters_cid), identity_proof=IdentityProof(token_jti=auth_result.token_jti, elevation_active=auth_result.elevation_active, mfa_satisfied=auth_result.mfa_satisfied), broker={"did": settings.broker_did, "name": settings.broker_name}) ac_db = AuthorizationContextDB( - context_id=ctx_id, principal_did=auth_result.principal_did, driver_id=request.driver_id, + context_id=ctx_id, principal_did=principal_did, driver_id=request.driver_id, playbook=request.playbook, corpus_entry_cid=request.corpus_entry_cid, parameters_cid=request.parameters_cid, accord_template=request.accord_template, token_jti=auth_result.token_jti, elevation_active=auth_result.elevation_active, mfa_satisfied=auth_result.mfa_satisfied, status="authorized", issued_at=now, expires_at=expires, session_mode=request.session_mode) db.add(ac_db) - cid = await chronicle.emit("GSAP_AC_ISSUED", {"event_code": "0x2704", "context_id": str(ctx_id), "principal_did": auth_result.principal_did, "playbook": request.playbook}) + cid = await chronicle.emit("GSAP_AC_ISSUED", {"event_code": "0x2704", "context_id": str(ctx_id), "principal_did": principal_did, "playbook": request.playbook}) ac_db.chronicle_event_cid = cid await db.commit()