mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
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:
@@ -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())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user