mirror of
https://github.com/openai/codex.git
synced 2026-11-13 21:30:47 +00:00
Share runtime authentication with model providers and catalogs (#57607)
## Why Model catalog refreshes need to use the same registered actor and credential owner as model requests when runtime authentication is enabled. ## What changed - Add `ProviderAuthContext` to carry credentials and optional runtime authentication into model providers and catalog clients. - Select runtime authentication independently for inference and catalog destinations. Reject unsupported OpenAI origins while preserving provider authentication for other destinations. - Include API organization and project selectors in runtime registration. Reuse registered runtimes across projects within an organization and replace them when the organization changes. - Preserve transport restrictions and account bindings, and strip sensitive headers on cross-origin redirects. - Apply the catalog timeout to authentication and fetching while allowing credential refresh and registration to finish safely. ## Testing Add coverage for runtime-authenticated catalogs, provider isolation, resource selection, gateway authentication, cross-origin redirects, and timeout recovery that preserves registration and refreshed tokens. GitOrigin-RevId: c14f1a02159031ea0af1f9b26ea890f8dca7d6ad
This commit is contained in:
@@ -82,7 +82,9 @@ fn registered(request: &Request) -> ResponseTemplate {
|
||||
let actor = if body["actor_auth"]["type"] == "oauth" {
|
||||
json!({"kind": "chatgpt", "user_id": "user-a", "account_id": "workspace-a"})
|
||||
} else {
|
||||
json!({"kind": "platform", "user_id": "user-a", "organization_id": "org-a", "project_id": "project-a"})
|
||||
json!({"kind": "platform", "user_id": "user-a",
|
||||
"organization_id": body["actor_auth"]["organization_id"].as_str().unwrap_or("org-a"),
|
||||
"project_id": body["actor_auth"]["project_id"].as_str().unwrap_or("project-a")})
|
||||
};
|
||||
ResponseTemplate::new(200).set_body_json(json!({
|
||||
"runtime_agent_instance_id": request.headers["idempotency-key"].to_str().unwrap(),
|
||||
@@ -161,6 +163,85 @@ async fn concurrent_requests_share_registration_and_replacement_preserves_in_fli
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn projects_share_one_runtime_and_organization_changes_replace_it() {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.respond_with(registered)
|
||||
.expect(3)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let initial = CodexAuth::from_api_key("key-a");
|
||||
let (manager, _, _) = setup(&server, initial.clone(), initial).await;
|
||||
let mut selection = PlatformResourceSelection {
|
||||
organization_id: Some("org-a".into()),
|
||||
project_id: Some("project-a".into()),
|
||||
};
|
||||
let first = manager.auth_with_selection(&selection).await.unwrap();
|
||||
for project in [Some("project-b"), None, Some("project-a")] {
|
||||
selection.project_id = project.map(str::to_owned);
|
||||
let next = manager.auth_with_selection(&selection).await.unwrap();
|
||||
assert!(Arc::ptr_eq(first.runtime(), next.runtime()));
|
||||
}
|
||||
selection.organization_id = Some("org-b".into());
|
||||
let other_org = manager.auth_with_selection(&selection).await.unwrap();
|
||||
assert_ne!(first.runtime().id(), other_org.runtime().id());
|
||||
selection.organization_id = Some("org-a".into());
|
||||
let replacement = manager.auth_with_selection(&selection).await.unwrap();
|
||||
assert_ne!(first.runtime().id(), replacement.runtime().id());
|
||||
assert_ne!(other_org.runtime().id(), replacement.runtime().id());
|
||||
server.verify().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn failed_registration_does_not_poison_another_project_or_reuse_its_idempotency_key() {
|
||||
for status in [403, 503] {
|
||||
let server = MockServer::start().await;
|
||||
Mock::given(method("POST"))
|
||||
.and(body_partial_json(
|
||||
json!({"actor_auth": {"project_id": "denied"}}),
|
||||
))
|
||||
.respond_with(ResponseTemplate::new(status))
|
||||
.with_priority(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
Mock::given(method("POST"))
|
||||
.respond_with(registered)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
let initial = CodexAuth::from_api_key("key-a");
|
||||
let (manager, _, _) = setup(&server, initial.clone(), initial).await;
|
||||
let mut selection = PlatformResourceSelection {
|
||||
organization_id: Some("org-a".into()),
|
||||
project_id: Some("denied".into()),
|
||||
};
|
||||
for _ in 0..2 {
|
||||
assert!(manager.auth_with_selection(&selection).await.is_err());
|
||||
}
|
||||
let failed = server.received_requests().await.unwrap();
|
||||
assert_eq!(failed.len(), if status == 403 { 1 } else { 2 });
|
||||
if status == 503 {
|
||||
assert_eq!(failed[0].body, failed[1].body);
|
||||
assert_eq!(
|
||||
failed[0].headers["idempotency-key"],
|
||||
failed[1].headers["idempotency-key"]
|
||||
);
|
||||
}
|
||||
|
||||
selection.project_id = Some("allowed".into());
|
||||
let ready = manager.auth_with_selection(&selection).await.unwrap();
|
||||
assert_ne!(
|
||||
ready.runtime().id(),
|
||||
failed[0].headers["idempotency-key"].to_str().unwrap()
|
||||
);
|
||||
selection.project_id = Some("denied".into());
|
||||
let reused = manager.auth_with_selection(&selection).await.unwrap();
|
||||
assert!(Arc::ptr_eq(ready.runtime(), reused.runtime()));
|
||||
server.verify().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn policy_recovery_preserves_pending_registration() {
|
||||
let server = MockServer::start().await;
|
||||
|
||||
Reference in New Issue
Block a user