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
This commit is contained in:
Winston Howes
2026-08-24 22:10:44 +00:00
committed by copyberry
parent 4f6d43c267
commit 0280d73c1e
8 changed files with 566 additions and 42 deletions
+293 -39
View File
@@ -9,6 +9,7 @@ use std::sync::RwLock;
pub const CREDENTIAL_BROKER_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_CREDENTIAL_BROKER_ACTIVE";
pub(crate) const BROKERED_CREDENTIALS_ENV_KEY: &str = "CODEX_NETWORK_PROXY_BROKERED_CREDENTIALS";
const MIN_EMBEDDED_CREDENTIAL_LENGTH: usize = 16;
#[derive(Clone)]
pub(crate) struct CredentialBroker {
@@ -20,6 +21,7 @@ struct CredentialBrokerState {
enabled: bool,
openai_api_host: Option<String>,
credentials: Vec<CredentialRecord>,
credential_aliases: Vec<CredentialAlias>,
}
struct CredentialRecord {
@@ -30,6 +32,11 @@ struct CredentialRecord {
dummy_value: String,
}
struct CredentialAlias {
env_var: String,
dummy_value: String,
}
fn env_key_matches(candidate: &str, expected: &str) -> bool {
if cfg!(windows) {
candidate.eq_ignore_ascii_case(expected)
@@ -78,6 +85,7 @@ impl CredentialBroker {
if state.enabled != config.credential_broker {
state.enabled = config.credential_broker;
state.credentials.clear();
state.credential_aliases.clear();
}
if state.openai_api_host != config.credential_broker_openai_host {
state
@@ -89,6 +97,45 @@ impl CredentialBroker {
}
}
pub(crate) fn discover_parent_credentials(
&self,
parent_env: &HashMap<String, String>,
child_env: &HashMap<String, String>,
) {
let mut state = self.write_state();
if !state.enabled {
return;
}
for provider in providers::credential_providers() {
for source in provider.sources() {
let Some(host_binding) =
(source.host_binding)(child_env, state.openai_api_host.as_deref())
else {
continue;
};
for env_var in source.env_vars {
let Some(real_value) =
brokerable_credential_value(parent_env, &state, env_var, provider)
.map(str::to_string)
else {
continue;
};
if env_value(child_env, env_var) == Some(real_value.as_str()) {
continue;
}
if child_env.values().any(|value| {
value == &real_value
|| real_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
&& value.contains(&real_value)
}) {
state.register(env_var, provider, host_binding.clone(), &real_value);
}
}
}
}
}
pub(crate) fn virtualize_child_env(&self, env: &mut HashMap<String, String>) {
let mut state = self.write_state();
if !state.enabled {
@@ -100,45 +147,105 @@ impl CredentialBroker {
for provider in providers::credential_providers() {
for source in provider.sources() {
if let Some(host_binding) =
let Some(host_binding) =
(source.host_binding)(env, state.openai_api_host.as_deref())
{
for env_var in source.env_vars {
virtualize_env_var(
env,
&mut state,
env_var,
provider,
host_binding.clone(),
);
}
else {
continue;
};
for env_var in source.env_vars {
virtualize_env_var(env, &mut state, env_var, provider, host_binding.clone());
}
}
}
let credentials = prioritized_credentials(&state, env);
let mut credential_aliases = Vec::new();
for (key, value) in env.iter_mut() {
let mut virtualized = false;
for credential in &credentials {
if value == &credential.real_value {
value.clone_from(&credential.dummy_value);
virtualized = true;
} else if credential.real_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
&& value.contains(&credential.real_value)
{
*value = value.replace(&credential.real_value, &credential.dummy_value);
virtualized = true;
}
}
if virtualized {
credential_aliases.push(CredentialAlias {
env_var: key.clone(),
dummy_value: value.clone(),
});
}
}
for alias in credential_aliases {
if !state.credential_aliases.iter().any(|existing| {
env_key_matches(&existing.env_var, &alias.env_var)
&& existing.dummy_value == alias.dummy_value
}) {
state.credential_aliases.push(alias);
}
}
update_brokered_credentials_marker(&state, env);
}
pub(crate) fn restore_child_env(
&self,
env: &mut HashMap<String, String>,
command: &mut [String],
_command: &mut [String],
) {
let state = self.read_state();
if !state.enabled || env_value(env, CREDENTIAL_BROKER_ACTIVE_ENV_KEY) != Some("1") {
return;
}
for (key, value) in env {
if let Some(credential) = state.credentials.iter().find(|credential| {
env_key_matches(key, &credential.env_var) && value == &credential.dummy_value
}) {
for argument in command.iter_mut() {
if argument.contains(&credential.dummy_value) {
*argument =
argument.replace(&credential.dummy_value, &credential.real_value);
}
let credentials = state
.credentials
.iter()
.filter(|credential| {
env.iter().any(|(key, value)| {
!env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
&& (value == &credential.dummy_value
|| credential.real_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
&& value.contains(&credential.dummy_value))
})
})
.collect::<Vec<_>>();
for (key, value) in env.iter_mut() {
if env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|| env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
{
continue;
}
let canonical_credential = state
.credentials
.iter()
.any(|credential| env_key_matches(key, &credential.env_var));
if !canonical_credential
&& !state.credential_aliases.iter().any(|alias| {
env_key_matches(key, &alias.env_var) && value == &alias.dummy_value
})
{
continue;
}
for credential in &credentials {
if canonical_credential
&& !state.credentials.iter().any(|candidate| {
env_key_matches(key, &candidate.env_var)
&& std::ptr::eq(candidate.provider, credential.provider)
&& candidate.host_binding == credential.host_binding
&& candidate.real_value == credential.real_value
})
{
continue;
}
if value == &credential.dummy_value {
value.clone_from(&credential.real_value);
} else if credential.real_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH {
*value = value.replace(&credential.dummy_value, &credential.real_value);
}
*value = credential.real_value.clone();
}
}
}
@@ -153,6 +260,49 @@ impl CredentialBroker {
.any(|credential| credential.matches_host(&normalized_host))
}
pub(crate) fn virtualize_text(&self, text: &mut String, env: &HashMap<String, String>) -> bool {
let state = self.read_state();
if !state.enabled {
return true;
}
let allowed_keys = brokered_credential_dummy_env_keys(env);
let credentials = prioritized_credentials(&state, env);
let mut allowed = true;
for credential in &credentials {
let contains_real = text.as_str() == credential.real_value
|| credential.real_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
&& text.contains(&credential.real_value);
let contains_dummy = text.as_str() == credential.dummy_value
|| credential.dummy_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
&& text.contains(&credential.dummy_value);
if !contains_real && !contains_dummy {
continue;
}
let replacement = credentials.iter().copied().find(|candidate| {
std::ptr::eq(candidate.provider, credential.provider)
&& candidate.real_value == credential.real_value
&& allowed_keys.iter().any(|key| {
env_key_matches(key, &candidate.env_var)
&& env_value(env, key) == Some(candidate.dummy_value.as_str())
})
});
if replacement.is_none() {
allowed = false;
}
let replacement = replacement.map_or("", |candidate| candidate.dummy_value.as_str());
if contains_real {
*text = text.replace(&credential.real_value, replacement);
}
if contains_dummy && credential.dummy_value != replacement {
*text = text.replace(&credential.dummy_value, replacement);
}
}
allowed
}
pub(crate) fn inject_request_headers(&self, host: &str, headers: &mut HeaderMap) {
let normalized_host = normalize_host(host);
let state = self.read_state();
@@ -160,12 +310,8 @@ impl CredentialBroker {
return;
}
let matching_credentials = state
.credentials
.iter()
.filter(|credential| credential.matches_host(&normalized_host))
.collect::<Vec<_>>();
let Some((credential, header_value)) = select_credential(headers, &matching_credentials)
let Some((credential, header_value)) =
select_credential(headers, &normalized_host, &state.credentials)
else {
return;
};
@@ -194,11 +340,32 @@ fn virtualize_env_var(
provider: &'static providers::CredentialProvider,
host_binding: providers::CredentialHostBinding,
) {
let Some(real_value) = brokerable_credential_value(env, state, env_var, provider) else {
let Some(real_value) = brokerable_credential_value(env, state, env_var, provider)
.map(str::to_string)
.or_else(|| {
let dummy = env_value(env, env_var)?;
state
.credentials
.iter()
.find(|credential| {
credential.dummy_value == dummy
&& std::ptr::eq(credential.provider, provider)
&& !env_key_matches(&credential.env_var, env_var)
&& credential.host_binding == host_binding
&& state.credentials.iter().any(|candidate| {
env_key_matches(&candidate.env_var, env_var)
&& std::ptr::eq(candidate.provider, provider)
&& candidate.host_binding == host_binding
&& candidate.real_value == credential.real_value
})
})
.map(|credential| credential.real_value.clone())
})
else {
return;
};
let dummy_value = state.register(env_var, provider, host_binding, real_value);
let dummy_value = state.register(env_var, provider, host_binding, &real_value);
set_env_value(env, env_var, dummy_value);
}
@@ -261,21 +428,60 @@ impl CredentialRecord {
}
}
fn prioritized_credentials<'a>(
state: &'a CredentialBrokerState,
env: &HashMap<String, String>,
) -> Vec<&'a CredentialRecord> {
let mut credentials = state.credentials.iter().collect::<Vec<_>>();
credentials.sort_unstable_by_key(|credential| {
let active = env_entry(env, &credential.env_var)
.is_some_and(|(_, value)| value == credential.dummy_value);
let has_matching_host_binding = credential.provider.sources().iter().any(|source| {
!source.binding_env_vars.is_empty()
&& (source.host_binding)(env, state.openai_api_host.as_deref())
.is_some_and(|binding| binding == credential.host_binding)
});
(
std::cmp::Reverse(credential.real_value.len()),
std::cmp::Reverse(active),
std::cmp::Reverse(has_matching_host_binding),
)
});
credentials
}
fn select_credential<'a>(
headers: &HeaderMap,
matching_credentials: &[&'a CredentialRecord],
host: &str,
credentials: &'a [CredentialRecord],
) -> Option<(&'a CredentialRecord, rama_http::HeaderValue)> {
let mut translated_matches = matching_credentials
let mut translated_matches = credentials
.iter()
.copied()
.filter(|credential| credential.matches_host(host))
.filter_map(|credential| {
credential
.provider
.translate_request_header(headers, &credential.dummy_value, &credential.real_value)
credentials
.iter()
.filter(|candidate| {
std::ptr::eq(candidate.provider, credential.provider)
&& candidate.real_value == credential.real_value
&& candidate.matches_host(host)
})
.find_map(|candidate| {
credential.provider.translate_request_header(
headers,
&candidate.dummy_value,
&credential.real_value,
)
})
.map(|header_value| (credential, header_value))
});
let matched = translated_matches.next()?;
translated_matches.next().is_none().then_some(matched)
translated_matches
.all(|(credential, _)| {
std::ptr::eq(credential.provider, matched.0.provider)
&& credential.real_value == matched.0.real_value
})
.then_some(matched)
}
fn update_brokered_credentials_marker(
@@ -285,7 +491,14 @@ fn update_brokered_credentials_marker(
let brokered = providers::credential_env_keys()
.filter_map(|key| {
let (actual_key, value) = env_entry(env, key)?;
state.is_dummy_value(value).then_some((actual_key, value))
state
.credentials
.iter()
.any(|credential| {
env_key_matches(actual_key, &credential.env_var)
&& value == credential.dummy_value
})
.then_some((actual_key, value))
})
.collect::<Vec<_>>();
match serde_json::to_string(&brokered) {
@@ -317,15 +530,56 @@ pub fn brokered_credential_dummy_env_keys(env: &HashMap<String, String>) -> Vec<
(actual_value == dummy_value.as_str()).then(|| actual_key.to_string())
})
.collect::<Vec<_>>();
keys.sort_unstable();
let context_bound = |key: &str| {
providers::credential_providers().any(|provider| {
provider.sources().iter().any(|source| {
source
.env_vars
.iter()
.any(|candidate| env_key_matches(key, candidate))
&& source
.binding_env_vars
.iter()
.any(|context| env_value(env, context).is_some())
})
})
};
keys.sort_unstable_by(|left, right| {
context_bound(right)
.cmp(&context_bound(left))
.then_with(|| left.cmp(right))
});
keys
}
/// Returns environment keys used to bind currently brokered credentials to hosts.
pub fn brokered_credential_binding_env_keys(
env: &HashMap<String, String>,
) -> impl Iterator<Item = &'static str> {
let brokered_keys = brokered_credential_dummy_env_keys(env);
providers::credential_binding_env_keys(&brokered_keys)
.filter(|key| env_value(env, key).is_some())
.collect::<Vec<_>>()
.into_iter()
}
/// Returns environment keys used to bind registered credential providers to their destinations.
pub fn credential_broker_provider_context_env_keys() -> impl Iterator<Item = &'static str> {
providers::credential_providers().flat_map(|provider| provider.context_env_vars.iter().copied())
}
/// Returns whether an environment key belongs to a supported credential provider.
pub fn is_credential_broker_provider_env_key(key: &str) -> bool {
providers::credential_providers().any(|provider| {
provider
.sources()
.iter()
.flat_map(|source| source.env_vars.iter().copied())
.chain(provider.context_env_vars.iter().copied())
.any(|candidate| env_key_matches(key, candidate))
})
}
/// Returns credential keys plus provider context keys already present in an environment with an
/// active broker.
pub fn brokered_credential_env_keys(
@@ -36,6 +36,7 @@ pub(super) enum CredentialHostBinding {
pub(super) struct CredentialSource {
pub(super) env_vars: &'static [&'static str],
pub(super) binding_env_vars: &'static [&'static str],
pub(super) host_binding:
fn(&HashMap<String, String>, Option<&str>) -> Option<CredentialHostBinding>,
}
@@ -102,6 +103,21 @@ pub(super) fn credential_context_env_keys(
.flat_map(|provider| provider.context_env_vars.iter().copied())
}
pub(super) fn credential_binding_env_keys(
brokered_keys: &[String],
) -> impl Iterator<Item = &'static str> + '_ {
credential_providers()
.flat_map(CredentialProvider::sources)
.filter(move |source| {
source.env_vars.iter().any(|key| {
brokered_keys
.iter()
.any(|brokered_key| super::env_key_matches(brokered_key, key))
})
})
.flat_map(|source| source.binding_env_vars.iter().copied())
}
pub(super) fn credential_env_keys() -> impl Iterator<Item = &'static str> {
credential_providers()
.flat_map(CredentialProvider::sources)
@@ -24,10 +24,12 @@ pub(super) static PROVIDER: CredentialProvider = CredentialProvider {
sources: &[
CredentialSource {
env_vars: GITHUB_CLOUD_TOKEN_ENV_VARS,
binding_env_vars: &[],
host_binding: github_cloud_binding,
},
CredentialSource {
env_vars: GITHUB_ENTERPRISE_TOKEN_ENV_VARS,
binding_env_vars: &[GH_HOST_ENV_VAR],
host_binding: github_enterprise_binding,
},
],
@@ -18,6 +18,7 @@ pub(super) static PROVIDER: CredentialProvider = CredentialProvider {
context_env_vars: &[OPENAI_BASE_URL_ENV_VAR],
sources: &[CredentialSource {
env_vars: OPENAI_API_KEY_ENV_VARS,
binding_env_vars: &[OPENAI_BASE_URL_ENV_VAR],
host_binding,
}],
reset_on_configuration_change: true,
@@ -51,10 +51,13 @@ fn virtualize_child_env_replaces_supported_credentials() {
let broker = CredentialBroker::new(/*enabled*/ true);
let github_token = "github_pat_11AA0bbCC_abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGH";
let openai_api_key = "sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
let authorization = format!("Bearer {github_token}");
let mut env = env_map([
("GH_TOKEN", github_token),
("HOMEBREW_GITHUB_API_TOKEN", github_token),
("AUTH_HEADER", authorization.as_str()),
("OPENAI_API_KEY", openai_api_key),
("GH_ENTERPRISE_TOKEN", "ghp-enterprise-real"),
("GH_ENTERPRISE_TOKEN", github_token),
]);
broker.virtualize_child_env(&mut env);
@@ -63,12 +66,37 @@ fn virtualize_child_env_replaces_supported_credentials() {
let openai_dummy = env.get("OPENAI_API_KEY").expect("dummy OpenAI API key");
assert_credential_shape(github_token, github_dummy, "github_pat_");
assert_credential_shape(openai_api_key, openai_dummy, "sk-proj-");
assert_eq!(env.get("HOMEBREW_GITHUB_API_TOKEN"), Some(github_dummy));
assert_eq!(env.get("GH_ENTERPRISE_TOKEN"), Some(github_dummy));
assert_eq!(
env.get("AUTH_HEADER"),
Some(&format!("Bearer {github_dummy}"))
);
let mut persisted_credentials = format!("{github_token}\n{openai_api_key}");
assert!(broker.virtualize_text(&mut persisted_credentials, &env));
assert_eq!(
persisted_credentials,
format!("{github_dummy}\n{openai_dummy}")
);
let mut filtered_env = env.clone();
filtered_env.remove("OPENAI_API_KEY");
let mut excluded_credentials = format!("{github_token}\n{openai_api_key}");
assert!(!broker.virtualize_text(&mut excluded_credentials, &filtered_env));
assert_eq!(excluded_credentials, format!("{github_dummy}\n"));
let mut excluded_dummies = format!("{github_dummy}\n{openai_dummy}");
assert!(!broker.virtualize_text(&mut excluded_dummies, &filtered_env));
assert_eq!(excluded_dummies, format!("{github_dummy}\n"));
let mut command = vec![
format!("Authorization: Bearer {github_dummy}"),
format!("Authorization: Bearer {openai_dummy}"),
];
let github_dummy = github_dummy.clone();
let openai_dummy = openai_dummy.clone();
env.insert("OPENAI_API_KEY".to_string(), "sk-user-override".to_string());
env.insert(
"GIT_CONFIG_VALUE_0".to_string(),
format!("Authorization: Bearer {github_dummy}"),
);
assert_eq!(
brokered_credential_dummy_env_keys(&env),
vec!["GH_TOKEN".to_string()]
@@ -76,17 +104,36 @@ fn virtualize_child_env_replaces_supported_credentials() {
broker.restore_child_env(&mut env, &mut command);
assert_eq!(env.get("GH_TOKEN").map(String::as_str), Some(github_token));
assert_eq!(
env.get("HOMEBREW_GITHUB_API_TOKEN").map(String::as_str),
Some(github_token)
);
assert_eq!(
env.get("GH_ENTERPRISE_TOKEN").map(String::as_str),
Some(github_token)
);
assert_eq!(env.get("AUTH_HEADER"), Some(&authorization));
assert_eq!(
env.get("OPENAI_API_KEY").map(String::as_str),
Some("sk-user-override")
);
assert_eq!(
env.get("GIT_CONFIG_VALUE_0"),
Some(&format!("Authorization: Bearer {github_dummy}"))
);
assert_eq!(
command,
vec![
format!("Authorization: Bearer {github_token}"),
format!("Authorization: Bearer {github_dummy}"),
format!("Authorization: Bearer {openai_dummy}"),
]
);
env.insert("GH_TOKEN".to_string(), openai_dummy.clone());
env.insert("OPENAI_API_KEY".to_string(), github_dummy.clone());
broker.restore_child_env(&mut env, &mut []);
assert_eq!(env.get("GH_TOKEN"), Some(&openai_dummy));
assert_eq!(env.get("OPENAI_API_KEY"), Some(&github_dummy));
}
#[cfg(windows)]
@@ -130,6 +177,119 @@ fn virtualize_child_env_preserves_live_dummy_mappings() {
assert_eq!(authorization(&first_headers), Some("Bearer ghp-real-one"));
assert_eq!(authorization(&second_headers), Some("Bearer ghp-real-two"));
let mut alias_only = env_map([("HOMEBREW_GITHUB_API_TOKEN", "ghp-real-one")]);
broker.virtualize_child_env(&mut alias_only);
assert_eq!(
alias_only.get("HOMEBREW_GITHUB_API_TOKEN"),
Some(first_dummy)
);
broker.restore_child_env(&mut alias_only, &mut []);
assert_eq!(alias_only["HOMEBREW_GITHUB_API_TOKEN"], "ghp-real-one");
let mut overridden = env_map([
("GH_TOKEN", "ghp-real-two"),
("HOMEBREW_GITHUB_API_TOKEN", "ghp-real-one"),
]);
broker.virtualize_child_env(&mut overridden);
assert_eq!(overridden.get("GH_TOKEN"), Some(second_dummy));
assert_eq!(
overridden.get("HOMEBREW_GITHUB_API_TOKEN"),
Some(first_dummy)
);
let mut cloud_alias = env_map([
("GH_TOKEN", "ghp-real-one"),
("GITHUB_TOKEN", "ghp-real-one"),
]);
broker.virtualize_child_env(&mut cloud_alias);
cloud_alias.insert("GITHUB_TOKEN".to_string(), first_dummy.clone());
broker.restore_child_env(&mut cloud_alias, &mut []);
assert_eq!(cloud_alias["GITHUB_TOKEN"], "ghp-real-one");
let mut distinct_credentials = env_map([
("GH_TOKEN", "ghp-primary-secret"),
("GITHUB_TOKEN", "ghp-secondary-secret"),
]);
broker.virtualize_child_env(&mut distinct_credentials);
let secondary_dummy = distinct_credentials["GITHUB_TOKEN"].clone();
distinct_credentials.remove("GITHUB_TOKEN");
distinct_credentials.insert("GH_TOKEN".to_string(), secondary_dummy.clone());
broker.virtualize_child_env(&mut distinct_credentials);
broker.restore_child_env(&mut distinct_credentials, &mut []);
assert_eq!(distinct_credentials["GH_TOKEN"], secondary_dummy);
}
#[test]
fn virtualize_child_env_replaces_aliases_of_filtered_parent_credentials() {
let broker = CredentialBroker::new(/*enabled*/ true);
let github_token = "ghp_abcdefghijklmnopqrstuvwxyz1234567890";
let authorization_header = format!("Bearer {github_token}");
let parent_env = env_map([
("GH_TOKEN", github_token),
("HOMEBREW_GITHUB_API_TOKEN", github_token),
]);
let mut child_env = env_map([
("HOMEBREW_GITHUB_API_TOKEN", github_token),
("AUTH_HEADER", authorization_header.as_str()),
]);
broker.discover_parent_credentials(&parent_env, &child_env);
broker.virtualize_child_env(&mut child_env);
let dummy = child_env["HOMEBREW_GITHUB_API_TOKEN"].clone();
assert_ne!(dummy, github_token);
assert_eq!(child_env["AUTH_HEADER"], format!("Bearer {dummy}"));
assert!(!child_env.contains_key("GH_TOKEN"));
let mut headers = headers_with_bearer(&dummy);
broker.inject_request_headers("api.github.com", &mut headers);
assert_eq!(authorization(&headers), Some(authorization_header.as_str()));
broker.restore_child_env(&mut child_env, &mut []);
assert_eq!(child_env["HOMEBREW_GITHUB_API_TOKEN"], github_token);
assert_eq!(child_env["AUTH_HEADER"], authorization_header);
assert!(!child_env.contains_key("GH_TOKEN"));
}
#[test]
fn virtualize_child_env_binds_filtered_enterprise_credentials_to_child_host() {
let github_token = "ghp_abcdefghijklmnopqrstuvwxyz1234567890";
let authorization_header = format!("Bearer {github_token}");
for (parent_host, include_cloud_token) in [
(None, false),
(Some("github.previous.example"), false),
(None, true),
] {
let broker = CredentialBroker::new(/*enabled*/ true);
let mut parent_env = env_map([("GH_ENTERPRISE_TOKEN", github_token)]);
if let Some(parent_host) = parent_host {
parent_env.insert("GH_HOST".to_string(), parent_host.to_string());
}
if include_cloud_token {
parent_env.insert("GH_TOKEN".to_string(), github_token.to_string());
}
let mut child_env = env_map([
("GH_HOST", "github.current.example"),
("AUTH_HEADER", authorization_header.as_str()),
]);
broker.discover_parent_credentials(&parent_env, &child_env);
broker.virtualize_child_env(&mut child_env);
assert_ne!(child_env["AUTH_HEADER"], authorization_header);
let mut headers = headers_with_authorization(&child_env["AUTH_HEADER"]);
broker.inject_request_headers("github.current.example", &mut headers);
assert_eq!(authorization(&headers), Some(authorization_header.as_str()));
let mut previous_headers = headers_with_authorization(&child_env["AUTH_HEADER"]);
broker.inject_request_headers("github.previous.example", &mut previous_headers);
assert_eq!(
authorization(&previous_headers),
Some(child_env["AUTH_HEADER"].as_str())
);
}
}
#[test]
@@ -192,6 +352,12 @@ fn virtualize_child_env_preserves_unbound_enterprise_token() {
assert_eq!(env["GH_ENTERPRISE_TOKEN"], "ghp-enterprise-real");
assert_eq!(headers, headers_with_bearer(inert_token));
assert!(!broker.host_requires_mitm("attacker.example"));
env.insert("GH_HOST".to_string(), "github.example.com".to_string());
broker.virtualize_child_env(&mut env);
let mut headers = headers_with_bearer(&env["GH_ENTERPRISE_TOKEN"]);
broker.inject_request_headers("github.example.com", &mut headers);
assert_eq!(authorization(&headers), Some("Bearer ghp-enterprise-real"));
}
#[test]
@@ -294,6 +460,7 @@ fn openai_credentials_bind_only_to_default_and_configured_trusted_hosts() {
]);
broker.virtualize_child_env(&mut env);
assert!(brokered_credential_env_keys(&env).any(|key| key == "OPENAI_BASE_URL"));
assert!(brokered_credential_binding_env_keys(&env).any(|key| key == "OPENAI_BASE_URL"));
let dummy = &env["OPENAI_API_KEY"];
for (host, expected_credential) in [
@@ -330,6 +497,7 @@ fn github_cloud_credentials_match_ghe_com_host_hint() {
let broker = CredentialBroker::new(/*enabled*/ true);
let mut env = env_map([("GH_HOST", "astemu.ghe.com"), ("GH_TOKEN", "ghp-real")]);
broker.virtualize_child_env(&mut env);
assert!(!brokered_credential_binding_env_keys(&env).any(|key| key == "GH_HOST"));
let github_token = env.get("GH_TOKEN").expect("dummy GitHub token");
let mut headers = headers_with_bearer(github_token);
@@ -362,18 +530,77 @@ fn github_enterprise_credentials_bind_to_gh_host() {
let broker = CredentialBroker::new(/*enabled*/ true);
let mut env = env_map([
("GH_HOST", "github.example.com"),
("GH_TOKEN", "ghp-enterprise-real"),
("GH_ENTERPRISE_TOKEN", "ghp-enterprise-real"),
("AUTH_HEADER", "Bearer ghp-enterprise-real"),
]);
broker.virtualize_child_env(&mut env);
let github_dummy = env["GH_TOKEN"].clone();
assert!(brokered_credential_env_keys(&env).any(|key| key == "GH_HOST"));
assert!(brokered_credential_binding_env_keys(&env).any(|key| key == "GH_HOST"));
let github_token = env
.get("GH_ENTERPRISE_TOKEN")
.expect("dummy GitHub enterprise token");
assert_ne!(github_token, &github_dummy);
let mut headers = headers_with_bearer(github_token);
broker.inject_request_headers("github.example.com", &mut headers);
assert_eq!(authorization(&headers), Some("Bearer ghp-enterprise-real"));
assert_eq!(env["AUTH_HEADER"], format!("Bearer {github_token}"));
let mut alias_headers = headers_with_authorization(&env["AUTH_HEADER"]);
broker.inject_request_headers("github.example.com", &mut alias_headers);
assert_eq!(
authorization(&alias_headers),
Some("Bearer ghp-enterprise-real")
);
let mut persisted_alias = "Bearer ghp-enterprise-real".to_string();
assert!(broker.virtualize_text(&mut persisted_alias, &env));
assert_eq!(persisted_alias, format!("Bearer {github_token}"));
assert_eq!(
brokered_credential_dummy_env_keys(&env).first(),
Some(&"GH_ENTERPRISE_TOKEN".to_string())
);
let mut cloud_headers = headers_with_bearer(&github_dummy);
broker.inject_request_headers("github.example.com", &mut cloud_headers);
assert_eq!(cloud_headers, headers_with_bearer(&github_dummy));
let mut enterprise_headers = headers_with_bearer(github_token);
broker.inject_request_headers("api.github.com", &mut enterprise_headers);
assert_eq!(enterprise_headers, headers_with_bearer(github_token));
let mut cloud_only = env_map([
("GH_HOST", "github.example.com"),
("GH_TOKEN", "ghp-enterprise-real"),
("AUTH_HEADER", "Bearer ghp-enterprise-real"),
]);
broker.virtualize_child_env(&mut cloud_only);
assert_eq!(cloud_only["AUTH_HEADER"], format!("Bearer {github_dummy}"));
assert!(broker.host_requires_mitm("github.example.com"));
assert!(!broker.host_requires_mitm("api.github.com"));
assert!(broker.host_requires_mitm("api.github.com"));
env.insert("GH_HOST".to_string(), "attacker.example".to_string());
env.insert("GH_ENTERPRISE_TOKEN".to_string(), github_dummy.clone());
broker.virtualize_child_env(&mut env);
let mut attacker_headers = headers_with_bearer(&github_dummy);
broker.inject_request_headers("attacker.example", &mut attacker_headers);
assert_eq!(attacker_headers, headers_with_bearer(&github_dummy));
assert!(!broker.host_requires_mitm("attacker.example"));
let mut alternate_enterprise_key = env_map([
("GH_HOST", "github.alternate.example"),
("GH_TOKEN", "ghp-alternate-real"),
("GITHUB_ENTERPRISE_TOKEN", "ghp-alternate-real"),
("AUTH_HEADER", "Bearer ghp-alternate-real"),
]);
broker.virtualize_child_env(&mut alternate_enterprise_key);
assert_eq!(
alternate_enterprise_key["AUTH_HEADER"],
format!(
"Bearer {}",
alternate_enterprise_key["GITHUB_ENTERPRISE_TOKEN"]
)
);
assert_eq!(
brokered_credential_dummy_env_keys(&alternate_enterprise_key).first(),
Some(&"GITHUB_ENTERPRISE_TOKEN".to_string())
);
}
+2
View File
@@ -41,9 +41,11 @@ pub use config::NetworkUnixSocketPermissions;
pub use config::host_and_port_from_network_addr;
pub use config::managed_proxy_ports;
pub use credential_broker::CREDENTIAL_BROKER_ACTIVE_ENV_KEY;
pub use credential_broker::brokered_credential_binding_env_keys;
pub use credential_broker::brokered_credential_dummy_env_keys;
pub use credential_broker::brokered_credential_env_keys;
pub use credential_broker::credential_broker_provider_context_env_keys;
pub use credential_broker::is_credential_broker_provider_env_key;
pub use environment_policy::EnvironmentNetworkPolicy;
pub use mitm_hook::InjectedHeaderConfig;
pub use mitm_hook::MitmHookActionsConfig;
+9
View File
@@ -1109,6 +1109,15 @@ impl NetworkProxy {
self.state.restore_child_credentials(env, command);
}
/// Replaces allowed credentials and removes credentials excluded from the environment.
pub fn virtualize_brokered_text(
&self,
text: &mut String,
env: &HashMap<String, String>,
) -> bool {
self.state.virtualize_brokered_text(text, env)
}
pub fn apply_to_env_for_environment(
&self,
env: &mut HashMap<String, String>,
+13
View File
@@ -427,6 +427,11 @@ impl NetworkProxyState {
}
pub fn virtualize_child_credentials(&self, env: &mut HashMap<String, String>) {
let parent_env = std::env::vars_os()
.filter_map(|(key, value)| Some((key.into_string().ok()?, value.into_string().ok()?)))
.collect();
self.credential_broker
.discover_parent_credentials(&parent_env, env);
self.credential_broker.virtualize_child_env(env);
}
@@ -438,6 +443,14 @@ impl NetworkProxyState {
self.credential_broker.restore_child_env(env, command);
}
pub(crate) fn virtualize_brokered_text(
&self,
text: &mut String,
env: &HashMap<String, String>,
) -> bool {
self.credential_broker.virtualize_text(text, env)
}
pub fn inject_request_credentials(&self, host: &str, headers: &mut rama_http::HeaderMap) {
self.credential_broker.inject_request_headers(host, headers);
}