mod providers; use crate::policy::normalize_host; use rama_http::HeaderMap; use std::collections::HashMap; use std::sync::Arc; 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"; #[derive(Clone)] pub(crate) struct CredentialBroker { state: Arc>, } #[derive(Default)] struct CredentialBrokerState { enabled: bool, credentials: Vec, } struct CredentialRecord { env_var: String, provider: &'static providers::CredentialProvider, host_binding: providers::CredentialHostBinding, real_value: String, dummy_value: String, } impl CredentialBroker { pub(crate) fn new(enabled: bool) -> Self { Self { state: Arc::new(RwLock::new(CredentialBrokerState { enabled, ..CredentialBrokerState::default() })), } } pub(crate) fn enabled(&self) -> bool { self.read_state().enabled } pub(crate) fn virtualize_child_env(&self, env: &mut HashMap) { let mut state = self.write_state(); if !state.enabled { env.remove(CREDENTIAL_BROKER_ACTIVE_ENV_KEY); env.remove(BROKERED_CREDENTIALS_ENV_KEY); return; } env.insert( CREDENTIAL_BROKER_ACTIVE_ENV_KEY.to_string(), "1".to_string(), ); for provider in providers::credential_providers() { for source in provider.sources() { if let Some(host_binding) = (source.host_binding)(env) { for env_var in source.env_vars { virtualize_env_var( env, &mut state, env_var, provider, host_binding.clone(), ); } } } } update_brokered_credentials_marker(&state, env); } pub(crate) fn host_requires_mitm(&self, host: &str) -> bool { let normalized_host = normalize_host(host); let state = self.read_state(); state.enabled && state .credentials .iter() .any(|credential| credential.matches_host(&normalized_host)) } pub(crate) fn inject_request_headers(&self, host: &str, headers: &mut HeaderMap) { let normalized_host = normalize_host(host); let state = self.read_state(); if !state.enabled { return; } let matching_credentials = state .credentials .iter() .filter(|credential| credential.matches_host(&normalized_host)) .collect::>(); let Some(credential) = select_credential(headers, &matching_credentials) else { return; }; let Some(header_value) = credential .provider .request_header_value(&credential.real_value) else { return; }; credential .provider .insert_request_header(headers, header_value); } fn read_state(&self) -> std::sync::RwLockReadGuard<'_, CredentialBrokerState> { self.state .read() .unwrap_or_else(std::sync::PoisonError::into_inner) } fn write_state(&self) -> std::sync::RwLockWriteGuard<'_, CredentialBrokerState> { self.state .write() .unwrap_or_else(std::sync::PoisonError::into_inner) } } fn virtualize_env_var( env: &mut HashMap, state: &mut CredentialBrokerState, env_var: &str, provider: &'static providers::CredentialProvider, host_binding: providers::CredentialHostBinding, ) { let Some(real_value) = brokerable_credential_value(env, state, env_var, provider) else { return; }; let dummy_value = state.register(env_var, provider, host_binding, real_value); env.insert(env_var.to_string(), dummy_value); } fn brokerable_credential_value<'a>( env: &'a HashMap, state: &CredentialBrokerState, env_var: &str, provider: &providers::CredentialProvider, ) -> Option<&'a str> { let real_value = env.get(env_var)?.trim(); (!real_value.is_empty() && !state.is_dummy_value(real_value) && provider.request_header_value(real_value).is_some()) .then_some(real_value) } impl CredentialBrokerState { fn register( &mut self, env_var: &str, provider: &'static providers::CredentialProvider, host_binding: providers::CredentialHostBinding, real_value: &str, ) -> String { if let Some(existing) = self.credentials.iter().find(|credential| { credential.env_var == env_var && std::ptr::eq(credential.provider, provider) && credential.host_binding == host_binding && credential.real_value == real_value }) { return existing.dummy_value.clone(); } let dummy_value = loop { let candidate = provider.dummy_value(real_value); if candidate != real_value && !self.is_dummy_value(&candidate) { break candidate; } }; self.credentials.push(CredentialRecord { env_var: env_var.to_string(), provider, host_binding, real_value: real_value.to_string(), dummy_value: dummy_value.clone(), }); dummy_value } fn is_dummy_value(&self, value: &str) -> bool { self.credentials .iter() .any(|credential| credential.dummy_value == value) } } impl CredentialRecord { fn matches_host(&self, host: &str) -> bool { self.host_binding.matches_host(host) } } fn select_credential<'a>( headers: &HeaderMap, matching_credentials: &[&'a CredentialRecord], ) -> Option<&'a CredentialRecord> { let dummy_matches = matching_credentials .iter() .copied() .filter(|credential| { credential .provider .request_header(headers) .and_then(|value| value.to_str().ok()) .is_some_and(|value| value.contains(&credential.dummy_value)) }) .collect::>(); match dummy_matches.as_slice() { [credential] => Some(*credential), [] | [_, _, ..] => None, } } fn update_brokered_credentials_marker( state: &CredentialBrokerState, env: &mut HashMap, ) { let brokered = providers::credential_broker_env_keys() .filter_map(|key| { let value = env.get(key)?; state.is_dummy_value(value).then_some((key, value.as_str())) }) .collect::>(); match serde_json::to_string(&brokered) { Ok(marker) => { env.insert(BROKERED_CREDENTIALS_ENV_KEY.to_string(), marker); } Err(_) => { env.remove(BROKERED_CREDENTIALS_ENV_KEY); } } } /// Returns supported environment keys whose current values still match the child-scoped dummy /// values recorded by the credential broker. /// /// The broker marker is treated as untrusted: malformed metadata, unsupported keys, and values /// replaced by the user are ignored. The environment is not mutated; callers own the decision to /// remove the returned keys. pub fn brokered_credential_dummy_env_keys(env: &HashMap) -> Vec { env.get(BROKERED_CREDENTIALS_ENV_KEY) .and_then(|marker| serde_json::from_str::>(marker).ok()) .unwrap_or_default() .into_iter() .filter_map(|(key, dummy_value)| { (providers::credential_broker_env_keys().any(|candidate| candidate == key.as_str()) && env.get(&key) == Some(&dummy_value)) .then_some(key) }) .collect() } /// Returns supported credential keys only for an environment with an active broker. pub fn brokered_credential_env_keys( env: &HashMap, ) -> impl Iterator { let active = env .get(CREDENTIAL_BROKER_ACTIVE_ENV_KEY) .is_some_and(|value| value == "1"); providers::credential_broker_env_keys().filter(move |_| active) } #[cfg(test)] #[path = "credential_broker_tests.rs"] mod tests;