Skip to content

fix(server): repair Orbit binding ID auth and preserve legacy upstreams [SAO-17527] - #273

Open
josjeon wants to merge 4 commits into
mainfrom
fix/sao-17527-binding-id-auth-context
Open

josjeon wants to merge 4 commits into
mainfrom
fix/sao-17527-binding-id-auth-context

Conversation

@josjeon

@josjeon josjeon commented Sep 28, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

SAO-17527 reports 10/10 failures for DELETE /api/v1/control-bindings/{binding_id} in demo-v2-poc. The ID route sent control_bindings.write without target context. Orbit rejected that request with 400, which Agent Control surfaced as 502. GET and PATCH use the same by-ID authorization pattern by source inspection; their failure was not reproduced live for this ticket.

Change

  • For target-aware authorizers, resolve the caller namespace, load the binding within that namespace, then authorize control_bindings.read or control_bindings.write with its stored target. The handler reads or mutates the row in the authorized namespace again.
  • At startup, Agent Control derives Orbit's identity URL from the known management authorization URL and passes it to the generic HTTP provider. Local JWT authorizers also use stored-target authorization.
  • Custom HTTP upstreams with neither an explicit nor a derivable identity URL, and other legacy authorizers, retain their previous single namespace-wide authorization call. AGENT_CONTROL_AUTH_UPSTREAM_IDENTITY_URL is optional; compatible custom upstreams can set it to opt into stored-target authorization. Those deployments do not need a new setting to keep their prior behavior.
  • On the target-aware path, invalid credentials retain 401; missing or cross-namespace IDs and target denials return the binding 404. Legacy provider error behavior is unchanged. Request and response schemas are unchanged; generated TypeScript route comments reflect the provider-specific behavior.

Validation

  • On the previous PR commit (a63d331), the full local server suite passed (936 tests); focused auth and control-binding suites passed (193 tests), including GET/PATCH/DELETE compatibility checks for a custom upstream that rejects target context.
  • On dcf5901, local Ruff lint and server mypy passed. DB-free configuration and mock HTTP checks covered Orbit URL derivation, custom upstream fallback, explicit URL precedence, and identity/target authorization requests.
  • On reviewer follow-up 2edb64d, a GET/PATCH/DELETE regression test covers an identity/target-authorization namespace mismatch and checks that the binding remains unchanged. Local make prepush passed after installing declared optional test dependencies. The DB-backed endpoint test could not run locally because PostgreSQL and Docker were unavailable.
  • GitHub Python CI, UI CI, TypeScript SDK CI, server build, title validation, and Codecov patch checks passed on 2edb64d.
  • The earlier PR commit was exercised on an isolated GCP DevStack for successful ID operations, missing IDs, and invalid credentials. This commit has not been manually exercised on the shared demo stack.

Rollout and remaining risk

  • Before deploying Agent Control, verify that the target Orbit environment exposes POST /internal/auth/resolve_tenant_context and that its management authorization URL matches the known path. Target-aware ID calls add one identity request before the stored-target authorization request.
  • Monitor by-ID binding responses and upstream authorization errors. Roll back by reverting this PR and redeploying the prior Agent Control version.
  • A same-namespace caller may still infer that an inaccessible ID exists from latency or a transient upstream error. Normal target denials return 404. Targetless list authorization is outside this change.

@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Resolve caller identity before the namespace-scoped binding lookup, then authorize against the stored target. Preserve 401 responses and mask missing or target-denied bindings as 404.

Add provider and endpoint regression coverage and refresh generated TypeScript route descriptions.
@josjeon
josjeon force-pushed the fix/sao-17527-binding-id-auth-context branch from 4c7725d to 39138dd Compare September 30, 2026 16:08
@josjeon josjeon changed the title fix(server): authorize binding ID routes with stored target context [SAO-17527] fix(server): repair Orbit binding ID auth and preserve legacy upstreams [SAO-17527] Sep 30, 2026
@josjeon
josjeon enabled auto-merge (squash) September 30, 2026 19:32

context = {"target_type": target_type, "target_id": target_id}
try:
return await authorizer.authorize(request, operation, context)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The principal returned by authorize() should be same as what we received earlier at identity = await authorizer.resolve_identity(request, operation) if both work properly. For any reason if there is namespace_key mistmatch from both then incorrect namespace_key would execute for the binding. Good to have,

  principal = await authorizer.authorize(request, operation, context)
  if principal.namespace_key != identity.namespace_key:
      raise ...  # fail closed
  return principal

@josjeon josjeon Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. I added a namespace equality check after target authorization and before the handler receives the principal in 2edb64d. If the identity and target grants disagree, the route fails closed with the existing 404 CONTROL_BINDING_NOT_FOUND response, keeping inaccessible and missing IDs indistinguishable. I also added a parameterized GET/PATCH/DELETE regression test that verifies the binding remains unchanged. Local pre-push lint/typecheck and GitHub Python CI passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants