Skip to content

Commit 0280d73

Browse files
winston-openaicopyberry
authored andcommitted
Broker credential aliases in child environments (#40484)
## What changed - Discover supported credentials inherited from the parent even when their canonical provider variable is filtered from the child environment. - Replace matching values and credentials embedded in longer child environment values with broker dummies, then restore only aliases that the broker virtualized. - Preserve provider and host bindings when selecting credentials for outbound requests, including GitHub Enterprise and custom OpenAI hosts. - Expose helpers for identifying provider and binding variables, plus an API for virtualizing credential-bearing text before it is persisted. ## Testing - Extend credential broker tests for filtered parent variables, exact and embedded aliases, text virtualization, user overrides, and host-bound GitHub and OpenAI credentials. GitOrigin-RevId: 55d673911900470aab4e206d0f4f5e62ed3611ae
1 parent 4f6d43c commit 0280d73

8 files changed

Lines changed: 565 additions & 41 deletions

File tree

codex-rs/network-proxy/src/credential_broker.rs

Lines changed: 292 additions & 38 deletions
Large diffs are not rendered by default.

codex-rs/network-proxy/src/credential_broker/providers.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub(super) enum CredentialHostBinding {
3636

3737
pub(super) struct CredentialSource {
3838
pub(super) env_vars: &'static [&'static str],
39+
pub(super) binding_env_vars: &'static [&'static str],
3940
pub(super) host_binding:
4041
fn(&HashMap<String, String>, Option<&str>) -> Option<CredentialHostBinding>,
4142
}
@@ -102,6 +103,21 @@ pub(super) fn credential_context_env_keys(
102103
.flat_map(|provider| provider.context_env_vars.iter().copied())
103104
}
104105

106+
pub(super) fn credential_binding_env_keys(
107+
brokered_keys: &[String],
108+
) -> impl Iterator<Item = &'static str> + '_ {
109+
credential_providers()
110+
.flat_map(CredentialProvider::sources)
111+
.filter(move |source| {
112+
source.env_vars.iter().any(|key| {
113+
brokered_keys
114+
.iter()
115+
.any(|brokered_key| super::env_key_matches(brokered_key, key))
116+
})
117+
})
118+
.flat_map(|source| source.binding_env_vars.iter().copied())
119+
}
120+
105121
pub(super) fn credential_env_keys() -> impl Iterator<Item = &'static str> {
106122
credential_providers()
107123
.flat_map(CredentialProvider::sources)

codex-rs/network-proxy/src/credential_broker/providers/github.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ pub(super) static PROVIDER: CredentialProvider = CredentialProvider {
2424
sources: &[
2525
CredentialSource {
2626
env_vars: GITHUB_CLOUD_TOKEN_ENV_VARS,
27+
binding_env_vars: &[],
2728
host_binding: github_cloud_binding,
2829
},
2930
CredentialSource {
3031
env_vars: GITHUB_ENTERPRISE_TOKEN_ENV_VARS,
32+
binding_env_vars: &[GH_HOST_ENV_VAR],
3133
host_binding: github_enterprise_binding,
3234
},
3335
],

codex-rs/network-proxy/src/credential_broker/providers/openai.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub(super) static PROVIDER: CredentialProvider = CredentialProvider {
1818
context_env_vars: &[OPENAI_BASE_URL_ENV_VAR],
1919
sources: &[CredentialSource {
2020
env_vars: OPENAI_API_KEY_ENV_VARS,
21+
binding_env_vars: &[OPENAI_BASE_URL_ENV_VAR],
2122
host_binding,
2223
}],
2324
reset_on_configuration_change: true,

codex-rs/network-proxy/src/credential_broker_tests.rs

Lines changed: 230 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,13 @@ fn virtualize_child_env_replaces_supported_credentials() {
5151
let broker = CredentialBroker::new(/*enabled*/ true);
5252
let github_token = "github_pat_11AA0bbCC_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH";
5353
let openai_api_key = "sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
54+
let authorization = format!("Bearer {github_token}");
5455
let mut env = env_map([
5556
("GH_TOKEN", github_token),
57+
("HOMEBREW_GITHUB_API_TOKEN", github_token),
58+
("AUTH_HEADER", authorization.as_str()),
5659
("OPENAI_API_KEY", openai_api_key),
57-
("GH_ENTERPRISE_TOKEN", "ghp-enterprise-real"),
60+
("GH_ENTERPRISE_TOKEN", github_token),
5861
]);
5962

6063
broker.virtualize_child_env(&mut env);
@@ -63,30 +66,74 @@ fn virtualize_child_env_replaces_supported_credentials() {
6366
let openai_dummy = env.get("OPENAI_API_KEY").expect("dummy OpenAI API key");
6467
assert_credential_shape(github_token, github_dummy, "github_pat_");
6568
assert_credential_shape(openai_api_key, openai_dummy, "sk-proj-");
69+
assert_eq!(env.get("HOMEBREW_GITHUB_API_TOKEN"), Some(github_dummy));
70+
assert_eq!(env.get("GH_ENTERPRISE_TOKEN"), Some(github_dummy));
71+
assert_eq!(
72+
env.get("AUTH_HEADER"),
73+
Some(&format!("Bearer {github_dummy}"))
74+
);
75+
let mut persisted_credentials = format!("{github_token}\n{openai_api_key}");
76+
assert!(broker.virtualize_text(&mut persisted_credentials, &env));
77+
assert_eq!(
78+
persisted_credentials,
79+
format!("{github_dummy}\n{openai_dummy}")
80+
);
81+
let mut filtered_env = env.clone();
82+
filtered_env.remove("OPENAI_API_KEY");
83+
let mut excluded_credentials = format!("{github_token}\n{openai_api_key}");
84+
assert!(!broker.virtualize_text(&mut excluded_credentials, &filtered_env));
85+
assert_eq!(excluded_credentials, format!("{github_dummy}\n"));
86+
let mut excluded_dummies = format!("{github_dummy}\n{openai_dummy}");
87+
assert!(!broker.virtualize_text(&mut excluded_dummies, &filtered_env));
88+
assert_eq!(excluded_dummies, format!("{github_dummy}\n"));
6689
let mut command = vec![
6790
format!("Authorization: Bearer {github_dummy}"),
6891
format!("Authorization: Bearer {openai_dummy}"),
6992
];
93+
let github_dummy = github_dummy.clone();
7094
let openai_dummy = openai_dummy.clone();
7195
env.insert("OPENAI_API_KEY".to_string(), "sk-user-override".to_string());
96+
env.insert(
97+
"GIT_CONFIG_VALUE_0".to_string(),
98+
format!("Authorization: Bearer {github_dummy}"),
99+
);
72100
assert_eq!(
73101
brokered_credential_dummy_env_keys(&env),
74102
vec!["GH_TOKEN".to_string()]
75103
);
76104

77105
broker.restore_child_env(&mut env, &mut command);
78106
assert_eq!(env.get("GH_TOKEN").map(String::as_str), Some(github_token));
107+
assert_eq!(
108+
env.get("HOMEBREW_GITHUB_API_TOKEN").map(String::as_str),
109+
Some(github_token)
110+
);
111+
assert_eq!(
112+
env.get("GH_ENTERPRISE_TOKEN").map(String::as_str),
113+
Some(github_token)
114+
);
115+
assert_eq!(env.get("AUTH_HEADER"), Some(&authorization));
79116
assert_eq!(
80117
env.get("OPENAI_API_KEY").map(String::as_str),
81118
Some("sk-user-override")
82119
);
120+
assert_eq!(
121+
env.get("GIT_CONFIG_VALUE_0"),
122+
Some(&format!("Authorization: Bearer {github_dummy}"))
123+
);
83124
assert_eq!(
84125
command,
85126
vec![
86-
format!("Authorization: Bearer {github_token}"),
127+
format!("Authorization: Bearer {github_dummy}"),
87128
format!("Authorization: Bearer {openai_dummy}"),
88129
]
89130
);
131+
132+
env.insert("GH_TOKEN".to_string(), openai_dummy.clone());
133+
env.insert("OPENAI_API_KEY".to_string(), github_dummy.clone());
134+
broker.restore_child_env(&mut env, &mut []);
135+
assert_eq!(env.get("GH_TOKEN"), Some(&openai_dummy));
136+
assert_eq!(env.get("OPENAI_API_KEY"), Some(&github_dummy));
90137
}
91138

92139
#[cfg(windows)]
@@ -130,6 +177,119 @@ fn virtualize_child_env_preserves_live_dummy_mappings() {
130177

131178
assert_eq!(authorization(&first_headers), Some("Bearer ghp-real-one"));
132179
assert_eq!(authorization(&second_headers), Some("Bearer ghp-real-two"));
180+
181+
let mut alias_only = env_map([("HOMEBREW_GITHUB_API_TOKEN", "ghp-real-one")]);
182+
broker.virtualize_child_env(&mut alias_only);
183+
assert_eq!(
184+
alias_only.get("HOMEBREW_GITHUB_API_TOKEN"),
185+
Some(first_dummy)
186+
);
187+
broker.restore_child_env(&mut alias_only, &mut []);
188+
assert_eq!(alias_only["HOMEBREW_GITHUB_API_TOKEN"], "ghp-real-one");
189+
190+
let mut overridden = env_map([
191+
("GH_TOKEN", "ghp-real-two"),
192+
("HOMEBREW_GITHUB_API_TOKEN", "ghp-real-one"),
193+
]);
194+
broker.virtualize_child_env(&mut overridden);
195+
assert_eq!(overridden.get("GH_TOKEN"), Some(second_dummy));
196+
assert_eq!(
197+
overridden.get("HOMEBREW_GITHUB_API_TOKEN"),
198+
Some(first_dummy)
199+
);
200+
201+
let mut cloud_alias = env_map([
202+
("GH_TOKEN", "ghp-real-one"),
203+
("GITHUB_TOKEN", "ghp-real-one"),
204+
]);
205+
broker.virtualize_child_env(&mut cloud_alias);
206+
cloud_alias.insert("GITHUB_TOKEN".to_string(), first_dummy.clone());
207+
broker.restore_child_env(&mut cloud_alias, &mut []);
208+
assert_eq!(cloud_alias["GITHUB_TOKEN"], "ghp-real-one");
209+
210+
let mut distinct_credentials = env_map([
211+
("GH_TOKEN", "ghp-primary-secret"),
212+
("GITHUB_TOKEN", "ghp-secondary-secret"),
213+
]);
214+
broker.virtualize_child_env(&mut distinct_credentials);
215+
let secondary_dummy = distinct_credentials["GITHUB_TOKEN"].clone();
216+
distinct_credentials.remove("GITHUB_TOKEN");
217+
distinct_credentials.insert("GH_TOKEN".to_string(), secondary_dummy.clone());
218+
broker.virtualize_child_env(&mut distinct_credentials);
219+
broker.restore_child_env(&mut distinct_credentials, &mut []);
220+
assert_eq!(distinct_credentials["GH_TOKEN"], secondary_dummy);
221+
}
222+
223+
#[test]
224+
fn virtualize_child_env_replaces_aliases_of_filtered_parent_credentials() {
225+
let broker = CredentialBroker::new(/*enabled*/ true);
226+
let github_token = "ghp_abcdefghijklmnopqrstuvwxyz1234567890";
227+
let authorization_header = format!("Bearer {github_token}");
228+
let parent_env = env_map([
229+
("GH_TOKEN", github_token),
230+
("HOMEBREW_GITHUB_API_TOKEN", github_token),
231+
]);
232+
let mut child_env = env_map([
233+
("HOMEBREW_GITHUB_API_TOKEN", github_token),
234+
("AUTH_HEADER", authorization_header.as_str()),
235+
]);
236+
237+
broker.discover_parent_credentials(&parent_env, &child_env);
238+
broker.virtualize_child_env(&mut child_env);
239+
240+
let dummy = child_env["HOMEBREW_GITHUB_API_TOKEN"].clone();
241+
assert_ne!(dummy, github_token);
242+
assert_eq!(child_env["AUTH_HEADER"], format!("Bearer {dummy}"));
243+
assert!(!child_env.contains_key("GH_TOKEN"));
244+
245+
let mut headers = headers_with_bearer(&dummy);
246+
broker.inject_request_headers("api.github.com", &mut headers);
247+
assert_eq!(authorization(&headers), Some(authorization_header.as_str()));
248+
249+
broker.restore_child_env(&mut child_env, &mut []);
250+
assert_eq!(child_env["HOMEBREW_GITHUB_API_TOKEN"], github_token);
251+
assert_eq!(child_env["AUTH_HEADER"], authorization_header);
252+
assert!(!child_env.contains_key("GH_TOKEN"));
253+
}
254+
255+
#[test]
256+
fn virtualize_child_env_binds_filtered_enterprise_credentials_to_child_host() {
257+
let github_token = "ghp_abcdefghijklmnopqrstuvwxyz1234567890";
258+
let authorization_header = format!("Bearer {github_token}");
259+
260+
for (parent_host, include_cloud_token) in [
261+
(None, false),
262+
(Some("github.previous.example"), false),
263+
(None, true),
264+
] {
265+
let broker = CredentialBroker::new(/*enabled*/ true);
266+
let mut parent_env = env_map([("GH_ENTERPRISE_TOKEN", github_token)]);
267+
if let Some(parent_host) = parent_host {
268+
parent_env.insert("GH_HOST".to_string(), parent_host.to_string());
269+
}
270+
if include_cloud_token {
271+
parent_env.insert("GH_TOKEN".to_string(), github_token.to_string());
272+
}
273+
let mut child_env = env_map([
274+
("GH_HOST", "github.current.example"),
275+
("AUTH_HEADER", authorization_header.as_str()),
276+
]);
277+
278+
broker.discover_parent_credentials(&parent_env, &child_env);
279+
broker.virtualize_child_env(&mut child_env);
280+
281+
assert_ne!(child_env["AUTH_HEADER"], authorization_header);
282+
let mut headers = headers_with_authorization(&child_env["AUTH_HEADER"]);
283+
broker.inject_request_headers("github.current.example", &mut headers);
284+
assert_eq!(authorization(&headers), Some(authorization_header.as_str()));
285+
286+
let mut previous_headers = headers_with_authorization(&child_env["AUTH_HEADER"]);
287+
broker.inject_request_headers("github.previous.example", &mut previous_headers);
288+
assert_eq!(
289+
authorization(&previous_headers),
290+
Some(child_env["AUTH_HEADER"].as_str())
291+
);
292+
}
133293
}
134294

135295
#[test]
@@ -192,6 +352,12 @@ fn virtualize_child_env_preserves_unbound_enterprise_token() {
192352
assert_eq!(env["GH_ENTERPRISE_TOKEN"], "ghp-enterprise-real");
193353
assert_eq!(headers, headers_with_bearer(inert_token));
194354
assert!(!broker.host_requires_mitm("attacker.example"));
355+
356+
env.insert("GH_HOST".to_string(), "github.example.com".to_string());
357+
broker.virtualize_child_env(&mut env);
358+
let mut headers = headers_with_bearer(&env["GH_ENTERPRISE_TOKEN"]);
359+
broker.inject_request_headers("github.example.com", &mut headers);
360+
assert_eq!(authorization(&headers), Some("Bearer ghp-enterprise-real"));
195361
}
196362

197363
#[test]
@@ -294,6 +460,7 @@ fn openai_credentials_bind_only_to_default_and_configured_trusted_hosts() {
294460
]);
295461
broker.virtualize_child_env(&mut env);
296462
assert!(brokered_credential_env_keys(&env).any(|key| key == "OPENAI_BASE_URL"));
463+
assert!(brokered_credential_binding_env_keys(&env).any(|key| key == "OPENAI_BASE_URL"));
297464
let dummy = &env["OPENAI_API_KEY"];
298465

299466
for (host, expected_credential) in [
@@ -330,6 +497,7 @@ fn github_cloud_credentials_match_ghe_com_host_hint() {
330497
let broker = CredentialBroker::new(/*enabled*/ true);
331498
let mut env = env_map([("GH_HOST", "astemu.ghe.com"), ("GH_TOKEN", "ghp-real")]);
332499
broker.virtualize_child_env(&mut env);
500+
assert!(!brokered_credential_binding_env_keys(&env).any(|key| key == "GH_HOST"));
333501
let github_token = env.get("GH_TOKEN").expect("dummy GitHub token");
334502
let mut headers = headers_with_bearer(github_token);
335503

@@ -362,18 +530,77 @@ fn github_enterprise_credentials_bind_to_gh_host() {
362530
let broker = CredentialBroker::new(/*enabled*/ true);
363531
let mut env = env_map([
364532
("GH_HOST", "github.example.com"),
533+
("GH_TOKEN", "ghp-enterprise-real"),
365534
("GH_ENTERPRISE_TOKEN", "ghp-enterprise-real"),
535+
("AUTH_HEADER", "Bearer ghp-enterprise-real"),
366536
]);
367537
broker.virtualize_child_env(&mut env);
538+
let github_dummy = env["GH_TOKEN"].clone();
368539
assert!(brokered_credential_env_keys(&env).any(|key| key == "GH_HOST"));
540+
assert!(brokered_credential_binding_env_keys(&env).any(|key| key == "GH_HOST"));
369541
let github_token = env
370542
.get("GH_ENTERPRISE_TOKEN")
371543
.expect("dummy GitHub enterprise token");
544+
assert_ne!(github_token, &github_dummy);
372545
let mut headers = headers_with_bearer(github_token);
373546

374547
broker.inject_request_headers("github.example.com", &mut headers);
375548

376549
assert_eq!(authorization(&headers), Some("Bearer ghp-enterprise-real"));
550+
assert_eq!(env["AUTH_HEADER"], format!("Bearer {github_token}"));
551+
let mut alias_headers = headers_with_authorization(&env["AUTH_HEADER"]);
552+
broker.inject_request_headers("github.example.com", &mut alias_headers);
553+
assert_eq!(
554+
authorization(&alias_headers),
555+
Some("Bearer ghp-enterprise-real")
556+
);
557+
let mut persisted_alias = "Bearer ghp-enterprise-real".to_string();
558+
assert!(broker.virtualize_text(&mut persisted_alias, &env));
559+
assert_eq!(persisted_alias, format!("Bearer {github_token}"));
560+
assert_eq!(
561+
brokered_credential_dummy_env_keys(&env).first(),
562+
Some(&"GH_ENTERPRISE_TOKEN".to_string())
563+
);
564+
let mut cloud_headers = headers_with_bearer(&github_dummy);
565+
broker.inject_request_headers("github.example.com", &mut cloud_headers);
566+
assert_eq!(cloud_headers, headers_with_bearer(&github_dummy));
567+
let mut enterprise_headers = headers_with_bearer(github_token);
568+
broker.inject_request_headers("api.github.com", &mut enterprise_headers);
569+
assert_eq!(enterprise_headers, headers_with_bearer(github_token));
570+
let mut cloud_only = env_map([
571+
("GH_HOST", "github.example.com"),
572+
("GH_TOKEN", "ghp-enterprise-real"),
573+
("AUTH_HEADER", "Bearer ghp-enterprise-real"),
574+
]);
575+
broker.virtualize_child_env(&mut cloud_only);
576+
assert_eq!(cloud_only["AUTH_HEADER"], format!("Bearer {github_dummy}"));
377577
assert!(broker.host_requires_mitm("github.example.com"));
378-
assert!(!broker.host_requires_mitm("api.github.com"));
578+
assert!(broker.host_requires_mitm("api.github.com"));
579+
580+
env.insert("GH_HOST".to_string(), "attacker.example".to_string());
581+
env.insert("GH_ENTERPRISE_TOKEN".to_string(), github_dummy.clone());
582+
broker.virtualize_child_env(&mut env);
583+
let mut attacker_headers = headers_with_bearer(&github_dummy);
584+
broker.inject_request_headers("attacker.example", &mut attacker_headers);
585+
assert_eq!(attacker_headers, headers_with_bearer(&github_dummy));
586+
assert!(!broker.host_requires_mitm("attacker.example"));
587+
588+
let mut alternate_enterprise_key = env_map([
589+
("GH_HOST", "github.alternate.example"),
590+
("GH_TOKEN", "ghp-alternate-real"),
591+
("GITHUB_ENTERPRISE_TOKEN", "ghp-alternate-real"),
592+
("AUTH_HEADER", "Bearer ghp-alternate-real"),
593+
]);
594+
broker.virtualize_child_env(&mut alternate_enterprise_key);
595+
assert_eq!(
596+
alternate_enterprise_key["AUTH_HEADER"],
597+
format!(
598+
"Bearer {}",
599+
alternate_enterprise_key["GITHUB_ENTERPRISE_TOKEN"]
600+
)
601+
);
602+
assert_eq!(
603+
brokered_credential_dummy_env_keys(&alternate_enterprise_key).first(),
604+
Some(&"GITHUB_ENTERPRISE_TOKEN".to_string())
605+
);
379606
}

codex-rs/network-proxy/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ pub use config::NetworkUnixSocketPermissions;
4141
pub use config::host_and_port_from_network_addr;
4242
pub use config::managed_proxy_ports;
4343
pub use credential_broker::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
44+
pub use credential_broker::brokered_credential_binding_env_keys;
4445
pub use credential_broker::brokered_credential_dummy_env_keys;
4546
pub use credential_broker::brokered_credential_env_keys;
4647
pub use credential_broker::credential_broker_provider_context_env_keys;
48+
pub use credential_broker::is_credential_broker_provider_env_key;
4749
pub use environment_policy::EnvironmentNetworkPolicy;
4850
pub use mitm_hook::InjectedHeaderConfig;
4951
pub use mitm_hook::MitmHookActionsConfig;

codex-rs/network-proxy/src/proxy.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,15 @@ impl NetworkProxy {
11091109
self.state.restore_child_credentials(env, command);
11101110
}
11111111

1112+
/// Replaces allowed credentials and removes credentials excluded from the environment.
1113+
pub fn virtualize_brokered_text(
1114+
&self,
1115+
text: &mut String,
1116+
env: &HashMap<String, String>,
1117+
) -> bool {
1118+
self.state.virtualize_brokered_text(text, env)
1119+
}
1120+
11121121
pub fn apply_to_env_for_environment(
11131122
&self,
11141123
env: &mut HashMap<String, String>,

0 commit comments

Comments
 (0)