feat: on_behalf_of — Bascule asserts operator identity in AC requests

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) <noreply@anthropic.com>
This commit is contained in:
Tyler J King 2026-04-03 09:06:45 -04:00
parent 295fe55bf2
commit 590c267f03
2 changed files with 10 additions and 3 deletions

View file

@ -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

View file

@ -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()