mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## Why A credential bound to a host with MITM hooks could also be used at an alias without hooks, allowing the proxy to inject the real credential without enforcing hook policy. ## What changed Reject requests to unhooked aliases when their selected brokered credentials are also bound to a hooked host. Check both HTTP forwarding and MITM requests before replacing dummy credentials, returning `403` with `blocked-by-mitm-hook` for denied requests. Use the same destination and credential-selection rules for alias checks and injection, including configured URL prefixes, safe paths, equivalent credentials, merged Basic authentication fields, and environment scope. ## Testing Add regression coverage for built-in and configured provider aliases, IPv6 destinations, credential selection, and unrelated credentials. Add an agent-started proxy test that verifies credential masking and alias denial. GitOrigin-RevId: 12be41654761cedcf87b170f7661d6566f7a864e
1670 lines
66 KiB
Rust
1670 lines
66 KiB
Rust
mod configured;
|
|
mod destination;
|
|
mod environment;
|
|
mod matching;
|
|
mod provider_config;
|
|
mod providers;
|
|
mod registry;
|
|
mod replacement;
|
|
|
|
use crate::config::NetworkProxyConfig;
|
|
use crate::policy::normalize_host;
|
|
use environment::update_brokered_credentials_marker;
|
|
use rama_http::HeaderMap;
|
|
use registry::ActiveCredentialSource;
|
|
use registry::BrokeredCredentialProvider;
|
|
use registry::active_credential_sources;
|
|
use registry::is_builtin_shaped_credential;
|
|
use registry::prioritized_credentials;
|
|
use registry::registered_credential_source;
|
|
use registry::select_credentials;
|
|
use replacement::Replacements;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
use std::collections::HashSet;
|
|
use std::sync::Arc;
|
|
use std::sync::RwLock;
|
|
use url::Url;
|
|
|
|
pub use environment::CredentialBrokerContext;
|
|
pub use environment::CredentialBrokerEnvironment;
|
|
pub use environment::brokered_credential_binding_env_keys;
|
|
pub use environment::brokered_credential_dummy_env_keys;
|
|
pub use environment::brokered_credential_env_keys;
|
|
pub use environment::brokered_credential_marker_env_keys;
|
|
pub use environment::brokered_credential_value_env_keys;
|
|
pub use environment::credential_broker_provider_context_env_keys;
|
|
pub use environment::credential_broker_provider_sources_allowed;
|
|
pub use environment::is_credential_broker_provider_env_key;
|
|
pub(crate) use environment::marked_credential_dummy_env_keys;
|
|
pub use provider_config::CredentialAuthMethod;
|
|
pub use provider_config::CredentialProviderConfig;
|
|
|
|
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;
|
|
const BROKERED_CREDENTIAL_ALIAS_MARKER_PREFIX: &str = "@alias:";
|
|
|
|
#[derive(Clone)]
|
|
pub(crate) struct CredentialBroker {
|
|
state: Arc<RwLock<CredentialBrokerState>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct CredentialBrokerState {
|
|
config_revision: u64,
|
|
enabled: bool,
|
|
allow_local_binding: bool,
|
|
openai_api_host: Option<String>,
|
|
context: CredentialBrokerContext,
|
|
configured_provider_configs: BTreeMap<String, CredentialProviderConfig>,
|
|
configured_providers: Vec<Arc<configured::ConfiguredCredentialProvider>>,
|
|
credentials: Vec<CredentialRecord>,
|
|
credential_owners: Vec<CredentialOwner>,
|
|
credential_aliases: Vec<CredentialAlias>,
|
|
}
|
|
|
|
struct CredentialOwner {
|
|
env_var: String,
|
|
real_value: String,
|
|
}
|
|
|
|
struct CredentialRecord {
|
|
env_var: String,
|
|
provider: BrokeredCredentialProvider,
|
|
host_binding: providers::CredentialHostBinding,
|
|
additional_host_bindings: Vec<providers::CredentialHostBinding>,
|
|
fallback_host_bindings: Vec<providers::CredentialHostBinding>,
|
|
environment_id: Option<String>,
|
|
real_value: String,
|
|
dummy_value: String,
|
|
// Canonical identities accompanying discovery, distinct from an alias's own credential.
|
|
source_values: Vec<String>,
|
|
generated_aliases: Arc<RwLock<Vec<replacement::GeneratedAlias>>>,
|
|
}
|
|
|
|
impl CredentialRecord {
|
|
fn has_carried_identity(
|
|
&self,
|
|
env: &HashMap<String, String>,
|
|
credentials: &[CredentialRecord],
|
|
sources: &[ActiveCredentialSource],
|
|
) -> bool {
|
|
let mut source_present = false;
|
|
let source_matches = sources
|
|
.iter()
|
|
.filter(|source| source_accepts_credential(source, self))
|
|
.flat_map(|source| &source.env_vars)
|
|
.filter_map(|key| env_value(env, key))
|
|
.any(|value| {
|
|
source_present = true;
|
|
let value = match &self.provider {
|
|
BrokeredCredentialProvider::Builtin(_) => value.trim(),
|
|
BrokeredCredentialProvider::Configured(_) => value,
|
|
};
|
|
let value = credentials
|
|
.iter()
|
|
.find(|source| source.dummy_value == value)
|
|
.map_or(value, |source| source.real_value.as_str());
|
|
value == self.real_value
|
|
|| value == self.dummy_value
|
|
|| self.source_values.iter().any(|source| source == value)
|
|
});
|
|
let source_changed = source_present && !source_matches;
|
|
env_contains_credential_value(env, &self.dummy_value)
|
|
|| (source_changed || self.invalidates_host_binding(env))
|
|
&& env.iter().any(|(key, value)| {
|
|
!env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
|
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
|
|
&& !crate::is_managed_proxy_env_var(key, value)
|
|
&& (value == &self.real_value
|
|
|| self.contains_embedded_value(value, &self.real_value))
|
|
})
|
|
}
|
|
|
|
fn invalidates_host_binding(&self, env: &HashMap<String, String>) -> bool {
|
|
match &self.provider {
|
|
BrokeredCredentialProvider::Builtin(provider) => {
|
|
provider.sources().iter().any(|source| {
|
|
source
|
|
.env_vars
|
|
.iter()
|
|
.any(|key| env_key_matches(key, &self.env_var))
|
|
&& (source.invalidates_host_binding)(env)
|
|
})
|
|
}
|
|
BrokeredCredentialProvider::Configured(provider) => {
|
|
provider
|
|
.config
|
|
.url_prefix_from_env
|
|
.as_deref()
|
|
.is_some_and(|key| env_value(env, key).is_some())
|
|
&& provider.dynamic_destination(env).is_none()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn observe_host_binding(
|
|
&mut self,
|
|
host_binding: providers::CredentialHostBinding,
|
|
env: &HashMap<String, String>,
|
|
) {
|
|
if self.invalidates_host_binding(env) {
|
|
self.additional_host_bindings.clear();
|
|
self.fallback_host_bindings.clear();
|
|
self.host_binding = host_binding;
|
|
return;
|
|
}
|
|
if self.host_binding == host_binding {
|
|
return;
|
|
}
|
|
// Track the latest source separately, but keep prior destinations usable by
|
|
// concurrent commands sharing this dummy in the same environment.
|
|
self.additional_host_bindings
|
|
.retain(|binding| binding != &host_binding);
|
|
self.additional_host_bindings
|
|
.push(std::mem::replace(&mut self.host_binding, host_binding));
|
|
}
|
|
|
|
fn host_bindings(&self) -> impl Iterator<Item = &providers::CredentialHostBinding> {
|
|
std::iter::once(&self.host_binding).chain(&self.additional_host_bindings)
|
|
}
|
|
|
|
fn belongs_to_environment(&self, environment_id: Option<&str>) -> bool {
|
|
self.environment_id.as_deref() == environment_id
|
|
}
|
|
}
|
|
|
|
fn source_accepts_credential(
|
|
source: &ActiveCredentialSource,
|
|
credential: &CredentialRecord,
|
|
) -> bool {
|
|
credential.provider.same_provider(&source.provider)
|
|
&& source
|
|
.env_vars
|
|
.iter()
|
|
.any(|env_var| env_key_matches(env_var, &credential.env_var))
|
|
}
|
|
|
|
fn source_tracks_credential(
|
|
source: &ActiveCredentialSource,
|
|
credential: &CredentialRecord,
|
|
env: &HashMap<String, String>,
|
|
) -> bool {
|
|
if !source_accepts_credential(source, credential) {
|
|
return false;
|
|
}
|
|
// Aliases may hold a different credential from the canonical source.
|
|
if source.host_binding == credential.host_binding {
|
|
return true;
|
|
}
|
|
let mut source_present = false;
|
|
let contains_credential = source
|
|
.env_vars
|
|
.iter()
|
|
.filter_map(|env_var| env_value(env, env_var))
|
|
.any(|value| {
|
|
source_present = true;
|
|
let value = match &source.provider {
|
|
BrokeredCredentialProvider::Builtin(_) => value.trim(),
|
|
BrokeredCredentialProvider::Configured(_) => value,
|
|
};
|
|
value == credential.dummy_value
|
|
|| value == credential.real_value
|
|
|| credential
|
|
.source_values
|
|
.iter()
|
|
.any(|source| source == value)
|
|
});
|
|
!source_present || contains_credential
|
|
}
|
|
|
|
struct CredentialAlias {
|
|
env_var: String,
|
|
dummy_value: String,
|
|
}
|
|
|
|
enum CredentialEnvironment {
|
|
Child,
|
|
Snapshot,
|
|
}
|
|
|
|
fn env_key_matches(candidate: &str, expected: &str) -> bool {
|
|
if cfg!(windows) {
|
|
candidate.eq_ignore_ascii_case(expected)
|
|
} else {
|
|
candidate == expected
|
|
}
|
|
}
|
|
|
|
fn env_entry<'a>(env: &'a HashMap<String, String>, key: &str) -> Option<(&'a str, &'a str)> {
|
|
env.iter()
|
|
.find(|(candidate, _)| env_key_matches(candidate, key))
|
|
.map(|(key, value)| (key.as_str(), value.as_str()))
|
|
}
|
|
|
|
pub(super) fn env_value<'a>(env: &'a HashMap<String, String>, key: &str) -> Option<&'a str> {
|
|
env_entry(env, key).map(|(_, value)| value)
|
|
}
|
|
|
|
fn set_env_value(env: &mut HashMap<String, String>, key: &str, value: String) {
|
|
if cfg!(windows) {
|
|
env.retain(|candidate, _| !env_key_matches(candidate, key));
|
|
}
|
|
env.insert(key.to_string(), value);
|
|
}
|
|
|
|
fn remove_env_value(env: &mut HashMap<String, String>, key: &str) {
|
|
if cfg!(windows) {
|
|
env.retain(|candidate, _| !env_key_matches(candidate, key));
|
|
} else {
|
|
env.remove(key);
|
|
}
|
|
}
|
|
|
|
fn env_contains_credential_value(env: &HashMap<String, String>, credential: &str) -> bool {
|
|
env.iter().any(|(key, value)| {
|
|
!env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
|
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
|
|
&& value.contains(credential)
|
|
&& !crate::is_managed_proxy_env_var(key, value)
|
|
})
|
|
}
|
|
|
|
impl CredentialBroker {
|
|
pub(crate) fn new(enabled: bool) -> Self {
|
|
Self {
|
|
state: Arc::new(RwLock::new(CredentialBrokerState {
|
|
enabled,
|
|
..CredentialBrokerState::default()
|
|
})),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn configure(&self, config: &NetworkProxyConfig) {
|
|
let mut state = self.write_state();
|
|
let previous_context_sources = (state.context != config.credential_broker_context)
|
|
.then(|| active_credential_sources(&state, &HashMap::new()));
|
|
if state.enabled != config.credential_broker
|
|
|| state.openai_api_host != config.credential_broker_openai_host
|
|
|| state.allow_local_binding != config.allow_local_binding()
|
|
|| state.configured_provider_configs != config.credential_providers
|
|
|| previous_context_sources.is_some()
|
|
{
|
|
state.config_revision += 1;
|
|
}
|
|
state.context.clone_from(&config.credential_broker_context);
|
|
state.allow_local_binding = config.allow_local_binding();
|
|
if state.enabled != config.credential_broker {
|
|
state.enabled = config.credential_broker;
|
|
state.credentials.clear();
|
|
state.credential_owners.clear();
|
|
state.credential_aliases.clear();
|
|
}
|
|
if state.openai_api_host != config.credential_broker_openai_host {
|
|
state
|
|
.openai_api_host
|
|
.clone_from(&config.credential_broker_openai_host);
|
|
state.credentials.retain(|credential| {
|
|
!matches!(
|
|
&credential.provider,
|
|
BrokeredCredentialProvider::Builtin(provider)
|
|
if provider.reset_on_configuration_change
|
|
)
|
|
});
|
|
}
|
|
if state.configured_provider_configs != config.credential_providers {
|
|
let mut configured_providers: Vec<Arc<configured::ConfiguredCredentialProvider>> =
|
|
Vec::new();
|
|
let mut provider_configs = config.credential_providers.iter().collect::<Vec<_>>();
|
|
provider_configs.sort_by_key(|(id, provider_config)| {
|
|
std::cmp::Reverse(
|
|
state.configured_provider_configs.get(id.as_str()) == Some(*provider_config)
|
|
&& state
|
|
.configured_providers
|
|
.iter()
|
|
.any(|provider| provider.id == id.as_str()),
|
|
)
|
|
});
|
|
for (id, provider_config) in provider_configs {
|
|
if configured_providers.iter().any(|existing| {
|
|
existing.config.env.iter().any(|existing_key| {
|
|
provider_config
|
|
.env
|
|
.iter()
|
|
.any(|key| env_key_matches(existing_key, key))
|
|
})
|
|
}) {
|
|
tracing::warn!(
|
|
provider = %id,
|
|
"ignoring credential provider with an overlapping environment source"
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let existing = (state.configured_provider_configs.get(id) == Some(provider_config))
|
|
.then(|| {
|
|
state
|
|
.configured_providers
|
|
.iter()
|
|
.find(|provider| provider.id == *id)
|
|
.cloned()
|
|
})
|
|
.flatten();
|
|
match existing {
|
|
Some(provider) => configured_providers.push(provider),
|
|
None => {
|
|
match configured::ConfiguredCredentialProvider::compile(id, provider_config)
|
|
{
|
|
Ok(provider) => configured_providers.push(Arc::new(provider)),
|
|
Err(error) => {
|
|
tracing::warn!(provider = %id, %error, "ignoring invalid credential provider");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
state
|
|
.credentials
|
|
.retain(|credential| match &credential.provider {
|
|
BrokeredCredentialProvider::Builtin(_) => true,
|
|
BrokeredCredentialProvider::Configured(provider) => configured_providers
|
|
.iter()
|
|
.any(|current| Arc::ptr_eq(current, provider)),
|
|
});
|
|
state.configured_providers = configured_providers;
|
|
state
|
|
.configured_provider_configs
|
|
.clone_from(&config.credential_providers);
|
|
}
|
|
if let Some(previous_sources) = previous_context_sources {
|
|
// Update registrations that used the old fallback, leaving distinct captured bindings intact.
|
|
let context = state.context.with_fallbacks(&HashMap::new()).into_owned();
|
|
let sources = active_credential_sources(&state, &context);
|
|
state.credentials.retain_mut(|credential| {
|
|
let Some(previous_source) = previous_sources.iter().find(|source| {
|
|
source_accepts_credential(source, credential)
|
|
&& credential
|
|
.host_bindings()
|
|
.any(|binding| binding == &source.host_binding)
|
|
}) else {
|
|
return true;
|
|
};
|
|
let source = sources
|
|
.iter()
|
|
.find(|source| source_accepts_credential(source, credential));
|
|
if source.is_some_and(|source| source.host_binding == previous_source.host_binding)
|
|
{
|
|
return true;
|
|
}
|
|
if !credential
|
|
.fallback_host_bindings
|
|
.contains(&previous_source.host_binding)
|
|
{
|
|
credential
|
|
.fallback_host_bindings
|
|
.push(previous_source.host_binding.clone());
|
|
}
|
|
if source.is_none() || credential.invalidates_host_binding(&context) {
|
|
// Clearing a fallback revokes its history, not independently captured hosts.
|
|
credential
|
|
.additional_host_bindings
|
|
.retain(|binding| !credential.fallback_host_bindings.contains(binding));
|
|
if credential
|
|
.fallback_host_bindings
|
|
.contains(&credential.host_binding)
|
|
{
|
|
let replacement = source
|
|
.map(|source| source.host_binding.clone())
|
|
.or_else(|| credential.additional_host_bindings.pop());
|
|
let Some(replacement) = replacement else {
|
|
return false;
|
|
};
|
|
credential.host_binding = replacement;
|
|
}
|
|
credential.fallback_host_bindings.clear();
|
|
}
|
|
if credential.host_binding == previous_source.host_binding {
|
|
let Some(source) = source else {
|
|
return false;
|
|
};
|
|
credential.observe_host_binding(source.host_binding.clone(), &context);
|
|
} else {
|
|
// A captured primary destination must stay primary when its older fallback changes.
|
|
if let Some(source) = source
|
|
&& !credential
|
|
.host_bindings()
|
|
.any(|binding| binding == &source.host_binding)
|
|
{
|
|
credential
|
|
.additional_host_bindings
|
|
.push(source.host_binding.clone());
|
|
}
|
|
}
|
|
if let Some(source) = source
|
|
&& !credential
|
|
.fallback_host_bindings
|
|
.contains(&source.host_binding)
|
|
{
|
|
credential
|
|
.fallback_host_bindings
|
|
.push(source.host_binding.clone());
|
|
}
|
|
true
|
|
});
|
|
}
|
|
}
|
|
|
|
pub(crate) fn config_revision(&self) -> u64 {
|
|
self.read_state().config_revision
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn discover_parent_credentials(
|
|
&self,
|
|
parent_env: &HashMap<String, String>,
|
|
child_env: &HashMap<String, String>,
|
|
) {
|
|
self.discover_parent_credentials_for_environment(
|
|
parent_env, child_env, /*environment_id*/ None,
|
|
);
|
|
}
|
|
|
|
pub(crate) fn discover_parent_credentials_for_environment(
|
|
&self,
|
|
parent_env: &HashMap<String, String>,
|
|
child_env: &HashMap<String, String>,
|
|
environment_id: Option<&str>,
|
|
) {
|
|
let mut state = self.write_state();
|
|
if !state.enabled {
|
|
return;
|
|
}
|
|
state.observe_credential_owners(parent_env);
|
|
|
|
let active_sources = active_credential_sources(&state, child_env);
|
|
for source in &active_sources {
|
|
for env_var in &source.env_vars {
|
|
let Some(real_value) =
|
|
brokerable_credential_value(parent_env, &state, env_var, &source.provider)
|
|
.map(str::to_string)
|
|
else {
|
|
continue;
|
|
};
|
|
// Reconcile carried identities in virtualize_env, preserving their registered
|
|
// destinations instead of binding the parent's value to a rotated child source.
|
|
if state.credentials.iter().any(|credential| {
|
|
env_key_matches(&credential.env_var, env_var)
|
|
&& credential.provider.same_provider(&source.provider)
|
|
&& credential.real_value == real_value
|
|
&& credential.has_carried_identity(
|
|
child_env,
|
|
&state.credentials,
|
|
&active_sources,
|
|
)
|
|
}) {
|
|
continue;
|
|
}
|
|
if env_value(child_env, env_var) != Some(real_value.as_str())
|
|
&& child_env.values().any(|value| {
|
|
value == &real_value
|
|
|| is_builtin_shaped_credential(&real_value)
|
|
&& value.contains(&real_value)
|
|
|| source.provider.contains_embedded_value(value, &real_value)
|
|
})
|
|
{
|
|
let _ = state.register(
|
|
env_var,
|
|
source.provider.clone(),
|
|
source.host_binding.clone(),
|
|
environment_id,
|
|
&real_value,
|
|
parent_env,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn virtualize_child_env(&self, env: &mut HashMap<String, String>) {
|
|
self.virtualize_child_env_for_environment(env, /*environment_id*/ None);
|
|
}
|
|
|
|
pub(crate) fn virtualize_child_env_for_environment(
|
|
&self,
|
|
env: &mut HashMap<String, String>,
|
|
environment_id: Option<&str>,
|
|
) {
|
|
self.virtualize_env(env, environment_id, CredentialEnvironment::Child);
|
|
}
|
|
|
|
pub(crate) fn virtualize_snapshot_env(
|
|
&self,
|
|
env: &mut HashMap<String, String>,
|
|
environment_id: Option<&str>,
|
|
) {
|
|
self.virtualize_env(env, environment_id, CredentialEnvironment::Snapshot);
|
|
}
|
|
|
|
fn virtualize_env(
|
|
&self,
|
|
env: &mut HashMap<String, String>,
|
|
environment_id: Option<&str>,
|
|
destination: CredentialEnvironment,
|
|
) {
|
|
let mut state = self.write_state();
|
|
if !state.enabled {
|
|
remove_env_value(env, CREDENTIAL_BROKER_ACTIVE_ENV_KEY);
|
|
remove_env_value(env, BROKERED_CREDENTIALS_ENV_KEY);
|
|
return;
|
|
}
|
|
set_env_value(env, CREDENTIAL_BROKER_ACTIVE_ENV_KEY, "1".to_string());
|
|
state.observe_credential_owners(env);
|
|
|
|
let active_sources = active_credential_sources(&state, env);
|
|
let carried_dummies = state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
credential.has_carried_identity(env, &state.credentials, &active_sources)
|
|
})
|
|
.map(|credential| credential.dummy_value.clone())
|
|
.collect::<HashSet<_>>();
|
|
for credential in state.credentials.iter_mut().filter(|credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& carried_dummies.contains(&credential.dummy_value)
|
|
}) {
|
|
if let Some(source) = registered_credential_source(credential, &active_sources, env)
|
|
.filter(|source| {
|
|
credential.invalidates_host_binding(env)
|
|
|| source_tracks_credential(source, credential, env)
|
|
})
|
|
{
|
|
credential.observe_host_binding(source.host_binding, env);
|
|
}
|
|
}
|
|
let stale_credentials = state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& carried_dummies.contains(&credential.dummy_value)
|
|
&& !registered_credential_source(credential, &active_sources, env)
|
|
.is_some_and(|source| source_tracks_credential(&source, credential, env))
|
|
})
|
|
.map(|credential| {
|
|
(
|
|
credential.dummy_value.clone(),
|
|
credential.real_value.clone(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
state.replace_child_env_dummies(env, &stale_credentials);
|
|
state.credentials.retain(|credential| {
|
|
!credential.belongs_to_environment(environment_id)
|
|
|| !stale_credentials
|
|
.iter()
|
|
.any(|(dummy_value, _)| credential.dummy_value == *dummy_value)
|
|
});
|
|
|
|
let mut owned_dummies = state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& carried_dummies.contains(&credential.dummy_value)
|
|
})
|
|
.map(|credential| credential.dummy_value.clone())
|
|
.collect::<HashSet<_>>();
|
|
let unbound_inherited_credentials = state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
!credential.belongs_to_environment(environment_id)
|
|
&& !owned_dummies.contains(&credential.dummy_value)
|
|
&& env_contains_credential_value(env, &credential.dummy_value)
|
|
&& registered_credential_source(credential, &active_sources, env).is_none()
|
|
})
|
|
.map(|credential| {
|
|
(
|
|
credential.dummy_value.clone(),
|
|
credential.real_value.clone(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
state.replace_child_env_dummies(env, &unbound_inherited_credentials);
|
|
|
|
let inherited_credentials = state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
!credential.belongs_to_environment(environment_id)
|
|
&& !owned_dummies.contains(&credential.dummy_value)
|
|
&& carried_dummies.contains(&credential.dummy_value)
|
|
})
|
|
.filter_map(|credential| {
|
|
registered_credential_source(credential, &active_sources, env).map(|source| {
|
|
(
|
|
credential.env_var.clone(),
|
|
source.provider,
|
|
source.host_binding,
|
|
credential.real_value.clone(),
|
|
credential.dummy_value.clone(),
|
|
credential.source_values.clone(),
|
|
Arc::clone(&credential.generated_aliases),
|
|
)
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut rebound_dummies = Vec::new();
|
|
for (
|
|
env_var,
|
|
provider,
|
|
host_binding,
|
|
real_value,
|
|
inherited_dummy,
|
|
source_values,
|
|
generated_aliases,
|
|
) in inherited_credentials
|
|
{
|
|
let current_dummy = if let Some(credential) =
|
|
state.credentials.iter_mut().find(|credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& credential.provider.same_provider(&provider)
|
|
&& env_key_matches(&credential.env_var, &env_var)
|
|
&& credential.real_value == real_value
|
|
}) {
|
|
credential.observe_host_binding(host_binding, env);
|
|
credential.dummy_value.clone()
|
|
} else {
|
|
state.credentials.push(CredentialRecord {
|
|
env_var,
|
|
provider,
|
|
host_binding,
|
|
additional_host_bindings: Vec::new(),
|
|
fallback_host_bindings: Vec::new(),
|
|
environment_id: environment_id.map(str::to_string),
|
|
real_value,
|
|
dummy_value: inherited_dummy.clone(),
|
|
source_values,
|
|
generated_aliases,
|
|
});
|
|
inherited_dummy.clone()
|
|
};
|
|
if current_dummy != inherited_dummy {
|
|
rebound_dummies.push((inherited_dummy, current_dummy.clone()));
|
|
}
|
|
owned_dummies.insert(current_dummy);
|
|
}
|
|
state.replace_child_env_dummies(env, &rebound_dummies);
|
|
for credential in state
|
|
.credentials
|
|
.iter_mut()
|
|
.filter(|credential| credential.belongs_to_environment(environment_id))
|
|
{
|
|
for source in &mut credential.source_values {
|
|
if let Some((_, rebound)) =
|
|
rebound_dummies.iter().find(|(dummy, _)| dummy == source)
|
|
{
|
|
source.clone_from(rebound);
|
|
}
|
|
}
|
|
}
|
|
|
|
for source in &active_sources {
|
|
for env_var in &source.env_vars {
|
|
virtualize_env_var(
|
|
env,
|
|
&mut state,
|
|
env_var,
|
|
source.provider.clone(),
|
|
source.host_binding.clone(),
|
|
environment_id,
|
|
);
|
|
}
|
|
}
|
|
let provider_context_keys = state.environment(env).provider_context_keys;
|
|
let mut known_registrations = Vec::new();
|
|
let binding_env = state.context.with_fallbacks(env);
|
|
let discoverable_values = env
|
|
.iter()
|
|
.filter(|(key, value)| {
|
|
!key.eq_ignore_ascii_case("PATH")
|
|
&& !key.to_ascii_uppercase().ends_with("_PATH")
|
|
&& !crate::is_managed_proxy_env_var(key, value)
|
|
&& !provider_context_keys
|
|
.iter()
|
|
.any(|context_key| env_key_matches(key, context_key))
|
|
})
|
|
.map(|(_, value)| {
|
|
let known = matching::known_credential_matches(&state, value, env);
|
|
for matched in &known {
|
|
if matched.value == matched.real_value
|
|
&& !state.credentials.iter().any(|credential| {
|
|
owned_dummies.contains(&credential.dummy_value)
|
|
&& credential.real_value == matched.real_value
|
|
&& credential.provider.same_provider(&matched.provider)
|
|
&& env_key_matches(&credential.env_var, matched.env_var)
|
|
})
|
|
&& let Some(source) = active_sources.iter().find(|source| {
|
|
source.provider.same_provider(&matched.provider)
|
|
&& source
|
|
.env_vars
|
|
.iter()
|
|
.any(|key| env_key_matches(key, matched.env_var))
|
|
})
|
|
{
|
|
known_registrations.push((
|
|
matched.env_var.to_string(),
|
|
matched.provider.clone(),
|
|
source.host_binding.clone(),
|
|
matched.real_value.to_string(),
|
|
));
|
|
}
|
|
}
|
|
matching::mask_known_credentials(value, &known)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
// Keep normal destination reconciliation for exact identities before discovering new ones.
|
|
for (env_var, provider, host_binding, real_value) in known_registrations {
|
|
let _ = state.register(
|
|
&env_var,
|
|
provider,
|
|
host_binding,
|
|
environment_id,
|
|
&real_value,
|
|
env,
|
|
);
|
|
}
|
|
let configured_discoveries = state
|
|
.configured_providers
|
|
.iter()
|
|
.flat_map(|provider| {
|
|
discoverable_values.iter().flat_map(move |value| {
|
|
provider
|
|
.find_discoverable_credentials(value)
|
|
.filter(|matched| !value[matched.range.clone()].contains('\0'))
|
|
.map(move |matched| (Arc::clone(provider), value, matched))
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut builtin_spans = Vec::new();
|
|
for provider in providers::credential_providers() {
|
|
let brokered_provider = BrokeredCredentialProvider::Builtin(provider);
|
|
let Some((source, host_binding)) = provider.sources().iter().rev().find_map(|source| {
|
|
(source.host_binding)(&binding_env, state.openai_api_host.as_deref())
|
|
.map(|binding| (source, binding))
|
|
}) else {
|
|
continue;
|
|
};
|
|
for value in &discoverable_values {
|
|
for prefix in provider.credential_prefixes {
|
|
for (start, _) in value.match_indices(prefix) {
|
|
let credential =
|
|
matching::builtin_credential_candidate(provider, value, start);
|
|
let configured_start = configured_discoveries
|
|
.iter()
|
|
.filter(|(_, candidate_value, matched)| {
|
|
*candidate_value == value
|
|
&& matched.has_distinctive_prefix
|
|
&& matched.range.start > start
|
|
&& matched.range.start < start + credential.len()
|
|
})
|
|
.map(|(_, _, matched)| matched.range.start - start)
|
|
.filter(|end| {
|
|
credential[..*end].trim_end_matches(['_', '-']).len()
|
|
>= provider.minimum_credential_len
|
|
})
|
|
.min();
|
|
let credential = configured_start.map_or(credential, |end| {
|
|
credential[..end].trim_end_matches(['_', '-'])
|
|
});
|
|
if credential.len() < provider.minimum_credential_len
|
|
|| matching::is_operational_path_match(
|
|
value,
|
|
start,
|
|
start + credential.len(),
|
|
)
|
|
|| provider.ignored_credential_prefixes.iter().any(|ignored| {
|
|
credential.starts_with(ignored)
|
|
&& provider
|
|
.credential_watermark
|
|
.is_none_or(|watermark| !credential.contains(watermark))
|
|
})
|
|
|| provider.request_header_value(credential).is_none()
|
|
{
|
|
continue;
|
|
}
|
|
// Retain ownership spans even when registration fails or is ambiguous.
|
|
let span = start..start + credential.len();
|
|
builtin_spans.push((value, span.clone()));
|
|
if configured_discoveries
|
|
.iter()
|
|
.any(|(_, candidate_value, matched)| {
|
|
*candidate_value == value
|
|
&& matched.range.start == start
|
|
&& matched.range.end >= span.end
|
|
})
|
|
|| state.is_dummy_value(credential)
|
|
|| state.credentials.iter().any(|existing| {
|
|
existing.belongs_to_environment(environment_id)
|
|
&& existing.provider.same_provider(&brokered_provider)
|
|
&& existing.host_binding == host_binding
|
|
&& existing.real_value == credential
|
|
})
|
|
|| state.credential_owners.iter().any(|existing| {
|
|
existing.real_value == credential
|
|
&& (source.binding_env_vars.is_empty()
|
|
&& !source
|
|
.env_vars
|
|
.iter()
|
|
.any(|key| env_key_matches(key, &existing.env_var))
|
|
|| !provider.sources().iter().any(|candidate| {
|
|
candidate
|
|
.env_vars
|
|
.iter()
|
|
.any(|key| env_key_matches(key, &existing.env_var))
|
|
}))
|
|
})
|
|
|| provider.sources().iter().any(|candidate| {
|
|
candidate
|
|
.env_vars
|
|
.iter()
|
|
.any(|key| env_value(env, key) == Some(credential))
|
|
&& (candidate.host_binding)(
|
|
&binding_env,
|
|
state.openai_api_host.as_deref(),
|
|
)
|
|
.is_none()
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
let _ = state.register(
|
|
source.env_vars[0],
|
|
brokered_provider.clone(),
|
|
host_binding.clone(),
|
|
environment_id,
|
|
credential,
|
|
env,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (provider, value, matched) in &configured_discoveries {
|
|
let Some(host_binding) = provider.host_binding(&binding_env) else {
|
|
continue;
|
|
};
|
|
let Some(env_var) = provider.config.env.first() else {
|
|
continue;
|
|
};
|
|
let brokered_provider = BrokeredCredentialProvider::Configured(provider.clone());
|
|
let range = &matched.range;
|
|
let credential = &value[range.clone()];
|
|
if matching::is_operational_path_match(value, range.start, range.end)
|
|
|| builtin_spans.iter().any(|(candidate_value, span)| {
|
|
candidate_value == value && span.start < range.end && range.start < span.end
|
|
})
|
|
|| configured_discoveries
|
|
.iter()
|
|
.any(|(other, candidate_value, matched)| {
|
|
!Arc::ptr_eq(provider, other)
|
|
&& candidate_value == value
|
|
&& matched.range.start < range.end
|
|
&& range.start < matched.range.end
|
|
})
|
|
|| state.is_dummy_value(credential)
|
|
|| state.credentials.iter().any(|existing| {
|
|
existing.belongs_to_environment(environment_id)
|
|
&& existing.provider.same_provider(&brokered_provider)
|
|
&& existing.host_binding == host_binding
|
|
&& existing.real_value == credential
|
|
})
|
|
|| credential_provider_definitions(&state).any(|other| {
|
|
!other.same_provider(&brokered_provider)
|
|
&& other.recognizes_credential_in(credential)
|
|
})
|
|
|| brokered_provider.request_header_value(credential).is_none()
|
|
{
|
|
continue;
|
|
}
|
|
let _ = state.register(
|
|
env_var,
|
|
brokered_provider.clone(),
|
|
host_binding.clone(),
|
|
environment_id,
|
|
credential,
|
|
env,
|
|
);
|
|
}
|
|
let credentials = prioritized_credentials(&state, env)
|
|
.into_iter()
|
|
.filter(|credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& (owned_dummies.contains(&credential.dummy_value)
|
|
|| active_sources.iter().any(|source| {
|
|
credential.provider.same_provider(&source.provider)
|
|
&& credential.host_binding == source.host_binding
|
|
}))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
let mut credential_aliases = Vec::new();
|
|
for (key, value) in env.iter_mut() {
|
|
if crate::is_managed_proxy_env_var(key, value) {
|
|
continue;
|
|
}
|
|
let mut replacements = Replacements::default();
|
|
for credential in &credentials {
|
|
for original in [&credential.real_value, &credential.dummy_value] {
|
|
replacements.add(
|
|
credential.value_match_ranges(value, original),
|
|
&credential.dummy_value,
|
|
Some(credential),
|
|
);
|
|
}
|
|
}
|
|
if replacements.render(value) {
|
|
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);
|
|
}
|
|
}
|
|
if state.allow_local_binding && matches!(destination, CredentialEnvironment::Child) {
|
|
// Clients bypass the proxy for local destinations, so their credentials must stay real.
|
|
state.restore_child_env(env, |credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& credential
|
|
.host_bindings()
|
|
.any(providers::CredentialHostBinding::bypasses_proxy_with_local_binding)
|
|
});
|
|
}
|
|
update_brokered_credentials_marker(&state, env);
|
|
}
|
|
|
|
pub(crate) fn restore_child_env(
|
|
&self,
|
|
env: &mut HashMap<String, String>,
|
|
_command: &mut [String],
|
|
) {
|
|
let state = self.read_state();
|
|
if !state.enabled || env_value(env, CREDENTIAL_BROKER_ACTIVE_ENV_KEY) != Some("1") {
|
|
return;
|
|
}
|
|
state.restore_child_env(env, |_| true);
|
|
}
|
|
|
|
pub(crate) fn restore_and_disable_child_env(
|
|
&self,
|
|
env: &mut HashMap<String, String>,
|
|
command: &mut [String],
|
|
) {
|
|
self.restore_child_env(env, command);
|
|
remove_env_value(env, CREDENTIAL_BROKER_ACTIVE_ENV_KEY);
|
|
remove_env_value(env, BROKERED_CREDENTIALS_ENV_KEY);
|
|
}
|
|
|
|
pub(crate) fn child_alias_matches(
|
|
&self,
|
|
key: &str,
|
|
value: &str,
|
|
snapshot_value: &str,
|
|
environment_id: Option<&str>,
|
|
) -> bool {
|
|
let state = self.read_state();
|
|
if !state.enabled {
|
|
return false;
|
|
}
|
|
let mut expected = HashMap::from([(key.to_string(), snapshot_value.to_string())]);
|
|
state.restore_child_env(&mut expected, |_| true);
|
|
let referenced_credentials = |text: &str| {
|
|
state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
text == credential.dummy_value
|
|
|| credential.contains_embedded_value(text, &credential.dummy_value)
|
|
})
|
|
.collect::<Vec<_>>()
|
|
};
|
|
let expected_credentials = referenced_credentials(snapshot_value);
|
|
if expected_credentials.is_empty() {
|
|
return false;
|
|
}
|
|
state.credential_aliases.iter().any(|alias| {
|
|
if !env_key_matches(key, &alias.env_var) {
|
|
return false;
|
|
}
|
|
let alias_credentials = referenced_credentials(&alias.dummy_value);
|
|
let same_owner = |left: &&CredentialRecord, right: &&CredentialRecord| {
|
|
left.provider.same_provider(&right.provider)
|
|
&& env_key_matches(&left.env_var, &right.env_var)
|
|
&& left.real_value == right.real_value
|
|
};
|
|
if !expected_credentials.iter().all(|expected| {
|
|
alias_credentials
|
|
.iter()
|
|
.any(|actual| same_owner(expected, actual))
|
|
}) || !alias_credentials.iter().all(|actual| {
|
|
expected_credentials
|
|
.iter()
|
|
.any(|expected| same_owner(expected, actual))
|
|
}) {
|
|
return false;
|
|
}
|
|
let mut candidate = HashMap::from([(key.to_string(), alias.dummy_value.clone())]);
|
|
let mut identity = candidate.clone();
|
|
if state.allow_local_binding {
|
|
state.restore_child_env(&mut candidate, |credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& credential.host_bindings().any(
|
|
providers::CredentialHostBinding::bypasses_proxy_with_local_binding,
|
|
)
|
|
});
|
|
}
|
|
if env_value(&candidate, key) != Some(value) {
|
|
return false;
|
|
}
|
|
state.restore_child_env(&mut identity, |_| true);
|
|
identity == expected
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn host_requires_mitm(&self, host: &str, port: u16) -> bool {
|
|
self.host_protocols_for_environment(host, port, /*environment_id*/ None)
|
|
.tls
|
|
}
|
|
|
|
pub(crate) fn host_protocols_for_environment(
|
|
&self,
|
|
host: &str,
|
|
port: u16,
|
|
environment_id: Option<&str>,
|
|
) -> crate::brokered_tunnel::BrokeredProtocols {
|
|
let normalized_host = normalize_host(host);
|
|
let state = self.read_state();
|
|
let mut protocols = crate::brokered_tunnel::BrokeredProtocols::default();
|
|
if state.enabled {
|
|
for credential in state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| credential.belongs_to_environment(environment_id))
|
|
{
|
|
for binding in credential.host_bindings() {
|
|
protocols.tls |= binding.requires_mitm(&normalized_host, port);
|
|
if let providers::CredentialHostBinding::ConfiguredHosts(destinations) = binding
|
|
{
|
|
protocols.http |= destinations.iter().any(|destination| {
|
|
destination.requires_http_interception(&normalized_host, port)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
protocols
|
|
}
|
|
|
|
pub(crate) fn virtualize_text(&self, text: &mut String, env: &HashMap<String, String>) -> bool {
|
|
let state = self.read_state();
|
|
matching::virtualize_text(&state, text, env)
|
|
}
|
|
|
|
pub(crate) fn restore_text(&self, text: &mut String) -> bool {
|
|
let state = self.read_state();
|
|
if !state.enabled {
|
|
return false;
|
|
}
|
|
|
|
let mut credentials = state.credentials.iter().collect::<Vec<_>>();
|
|
credentials
|
|
.sort_unstable_by_key(|credential| std::cmp::Reverse(credential.dummy_value.len()));
|
|
let mut replacements = Replacements::default();
|
|
for credential in credentials {
|
|
replacements.add(
|
|
credential.value_match_ranges(text, &credential.dummy_value),
|
|
&credential.real_value,
|
|
/*dummy*/ None,
|
|
);
|
|
}
|
|
replacements.render(text)
|
|
}
|
|
|
|
pub(crate) fn environment(&self, env: &HashMap<String, String>) -> CredentialBrokerEnvironment {
|
|
self.read_state().environment(env)
|
|
}
|
|
|
|
pub(crate) fn environment_for_text(
|
|
&self,
|
|
text: &str,
|
|
env: &HashMap<String, String>,
|
|
) -> CredentialBrokerEnvironment {
|
|
let state = self.read_state();
|
|
let mut scoped_env = env.clone();
|
|
for credential in &state.credentials {
|
|
remove_env_value(&mut scoped_env, &credential.env_var);
|
|
}
|
|
for credential in &state.credentials {
|
|
if text == credential.dummy_value
|
|
|| credential.contains_embedded_value(text, &credential.dummy_value)
|
|
{
|
|
set_env_value(
|
|
&mut scoped_env,
|
|
&credential.env_var,
|
|
credential.dummy_value.clone(),
|
|
);
|
|
}
|
|
}
|
|
update_brokered_credentials_marker(&state, &mut scoped_env);
|
|
state.environment(&scoped_env)
|
|
}
|
|
|
|
pub(crate) fn source_matches_text(&self, source: &str, source_value: &str, text: &str) -> bool {
|
|
let state = self.read_state();
|
|
state.enabled
|
|
&& state.credentials.iter().any(|credential| {
|
|
env_key_matches(&credential.env_var, source)
|
|
&& credential.real_value == source_value
|
|
&& (text == credential.real_value
|
|
|| credential.contains_embedded_value(text, &credential.real_value))
|
|
})
|
|
}
|
|
|
|
pub(crate) fn provider_sources_allowed(
|
|
&self,
|
|
value: &str,
|
|
virtualized: &str,
|
|
source_env: &HashMap<String, String>,
|
|
is_allowed: impl Fn(&str) -> bool,
|
|
) -> bool {
|
|
let state = self.read_state();
|
|
let known = matching::known_credential_matches(&state, value, source_env);
|
|
if known.iter().any(|matched| {
|
|
!known.iter().any(|equivalent| {
|
|
equivalent.range == matched.range
|
|
&& equivalent.provider.same_provider(&matched.provider)
|
|
&& equivalent.real_value == matched.real_value
|
|
&& is_allowed(equivalent.env_var)
|
|
})
|
|
}) {
|
|
return false;
|
|
}
|
|
let uncovered = matching::mask_known_credentials(value, &known);
|
|
let value = uncovered.as_str();
|
|
let builtin_recognized = providers::credential_providers().any(|provider| {
|
|
provider.credential_prefixes.iter().any(|prefix| {
|
|
value.match_indices(*prefix).any(|(start, _)| {
|
|
matching::recognized_credential_match(provider, value, virtualized, start)
|
|
.is_some()
|
|
})
|
|
})
|
|
});
|
|
if builtin_recognized
|
|
&& !credential_broker_provider_sources_allowed(
|
|
value,
|
|
virtualized,
|
|
source_env,
|
|
&is_allowed,
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
let mut configured_recognized = false;
|
|
for provider in &state.configured_providers {
|
|
for source in &provider.config.env {
|
|
if let Some(credential) = env_value(source_env, source)
|
|
&& provider.matches_value(credential)
|
|
&& !provider
|
|
.credential_value_match_ranges(value, credential)
|
|
.is_empty()
|
|
&& provider
|
|
.credential_value_match_ranges(virtualized, credential)
|
|
.is_empty()
|
|
{
|
|
configured_recognized = true;
|
|
if !provider.config.env.iter().any(|equivalent| {
|
|
env_value(source_env, equivalent) == Some(credential)
|
|
&& is_allowed(equivalent)
|
|
}) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
for credential in provider.find_credentials(value).filter(|credential| {
|
|
!credential.contains('\0') && !virtualized.contains(credential)
|
|
}) {
|
|
configured_recognized = true;
|
|
let sources = provider
|
|
.config
|
|
.env
|
|
.iter()
|
|
.filter(|source| env_value(source_env, source) == Some(credential))
|
|
.collect::<Vec<_>>();
|
|
let allowed = if sources.is_empty() {
|
|
provider.config.env.iter().all(|source| is_allowed(source))
|
|
} else {
|
|
sources.iter().any(|source| is_allowed(source))
|
|
};
|
|
if !allowed {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
!known.is_empty() || builtin_recognized || configured_recognized
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn inject_request_headers(&self, destination: &str, headers: &mut HeaderMap) {
|
|
self.inject_request_headers_for_environment(
|
|
destination,
|
|
headers,
|
|
/*environment_id*/ None,
|
|
);
|
|
}
|
|
|
|
pub(crate) fn inject_request_headers_for_environment(
|
|
&self,
|
|
destination: &str,
|
|
headers: &mut HeaderMap,
|
|
environment_id: Option<&str>,
|
|
) {
|
|
let state = self.read_state();
|
|
if !state.enabled {
|
|
return;
|
|
}
|
|
|
|
let credentials =
|
|
select_credentials(headers, destination, &state.credentials, environment_id);
|
|
for (credential, header_name, header_value) in credentials {
|
|
credential
|
|
.provider
|
|
.insert_request_header(headers, header_name, header_value);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn request_matches_hooked_host_alias(
|
|
&self,
|
|
destination: &str,
|
|
headers: &HeaderMap,
|
|
hooked_hosts: &[String],
|
|
environment_id: Option<&str>,
|
|
) -> bool {
|
|
let request = if destination.contains("://") {
|
|
Url::parse(destination).ok()
|
|
} else {
|
|
None
|
|
};
|
|
let normalized_host = normalize_host(
|
|
request
|
|
.as_ref()
|
|
.and_then(Url::host_str)
|
|
.unwrap_or(destination),
|
|
);
|
|
let state = self.read_state();
|
|
if !state.enabled {
|
|
return false;
|
|
}
|
|
|
|
let selected_headers =
|
|
select_credentials(headers, destination, &state.credentials, environment_id)
|
|
.into_iter()
|
|
.map(|(_, name, _)| name)
|
|
.collect::<Vec<_>>();
|
|
if selected_headers.is_empty() {
|
|
return false;
|
|
}
|
|
let matching_credentials = state
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| {
|
|
credential.belongs_to_environment(environment_id)
|
|
&& credential
|
|
.host_bindings()
|
|
.any(|binding| binding.matches_request(&normalized_host, request.as_ref()))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// Ambiguity removes the whole header, so every translation for a surviving header
|
|
// contributed through equivalent deduplication or Basic-field merging.
|
|
matching_credentials.iter().any(|credential| {
|
|
hooked_hosts.iter().any(|hooked_host| {
|
|
hooked_host != &normalized_host
|
|
&& credential
|
|
.host_bindings()
|
|
.any(|binding| binding.matches_hostname(hooked_host))
|
|
}) && matching_credentials.iter().any(|candidate| {
|
|
candidate.provider.same_provider(&credential.provider)
|
|
&& candidate.real_value == credential.real_value
|
|
&& credential
|
|
.provider
|
|
.translate_request_headers(
|
|
headers,
|
|
&candidate.dummy_value,
|
|
&credential.real_value,
|
|
)
|
|
.iter()
|
|
.any(|(name, _)| selected_headers.contains(name))
|
|
})
|
|
})
|
|
}
|
|
|
|
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<String, String>,
|
|
state: &mut CredentialBrokerState,
|
|
env_var: &str,
|
|
provider: BrokeredCredentialProvider,
|
|
host_binding: providers::CredentialHostBinding,
|
|
environment_id: Option<&str>,
|
|
) {
|
|
let previous_dummy = env_value(env, env_var)
|
|
.filter(|value| state.is_dummy_value(value))
|
|
.map(str::to_string);
|
|
let Some(real_value) = brokerable_credential_value(env, state, env_var, &provider)
|
|
.map(str::to_string)
|
|
.or_else(|| {
|
|
let dummy = previous_dummy.as_deref()?;
|
|
state
|
|
.credentials
|
|
.iter()
|
|
.find(|credential| {
|
|
credential.dummy_value == dummy
|
|
&& credential.provider.same_provider(&provider)
|
|
&& env_key_matches(&credential.env_var, env_var)
|
|
})
|
|
.map(|credential| credential.real_value.clone())
|
|
})
|
|
else {
|
|
return;
|
|
};
|
|
|
|
if let Some(dummy_value) = state.register(
|
|
env_var,
|
|
provider,
|
|
host_binding,
|
|
environment_id,
|
|
&real_value,
|
|
env,
|
|
) {
|
|
if let Some(previous_dummy) = previous_dummy
|
|
&& previous_dummy != dummy_value
|
|
{
|
|
state.replace_child_env_dummies(env, &[(previous_dummy, dummy_value.clone())]);
|
|
}
|
|
set_env_value(env, env_var, dummy_value);
|
|
}
|
|
}
|
|
|
|
fn brokerable_credential_value<'a>(
|
|
env: &'a HashMap<String, String>,
|
|
state: &CredentialBrokerState,
|
|
env_var: &str,
|
|
provider: &BrokeredCredentialProvider,
|
|
) -> Option<&'a str> {
|
|
let real_value = env_value(env, env_var)?;
|
|
let real_value = match provider {
|
|
BrokeredCredentialProvider::Builtin(_) => real_value.trim(),
|
|
BrokeredCredentialProvider::Configured(_) => real_value,
|
|
};
|
|
(!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 restore_child_env(
|
|
&self,
|
|
env: &mut HashMap<String, String>,
|
|
should_restore: impl Fn(&CredentialRecord) -> bool,
|
|
) {
|
|
let credentials = self
|
|
.credentials
|
|
.iter()
|
|
.filter(|credential| should_restore(credential))
|
|
.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.contains_embedded_value(value, &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 = self
|
|
.credentials
|
|
.iter()
|
|
.any(|credential| env_key_matches(key, &credential.env_var));
|
|
if !canonical_credential
|
|
&& !self.credential_aliases.iter().any(|alias| {
|
|
env_key_matches(key, &alias.env_var) && value == &alias.dummy_value
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
let mut replacements = Replacements::default();
|
|
for credential in &credentials {
|
|
if canonical_credential
|
|
&& !self.credentials.iter().any(|candidate| {
|
|
env_key_matches(key, &candidate.env_var)
|
|
&& candidate.provider.same_provider(&credential.provider)
|
|
&& candidate.host_binding == credential.host_binding
|
|
&& candidate.real_value == credential.real_value
|
|
})
|
|
{
|
|
continue;
|
|
}
|
|
replacements.add(
|
|
credential.value_match_ranges(value, &credential.dummy_value),
|
|
&credential.real_value,
|
|
/*dummy*/ None,
|
|
);
|
|
}
|
|
// Carry surviving dummy spans through partial local-destination restoration.
|
|
for credential in &self.credentials {
|
|
replacements.add(
|
|
credential.value_match_ranges(value, &credential.dummy_value),
|
|
&credential.dummy_value,
|
|
Some(credential),
|
|
);
|
|
}
|
|
replacements.render(value);
|
|
}
|
|
}
|
|
|
|
fn observe_credential_owners(&mut self, env: &HashMap<String, String>) {
|
|
// Ownership is known even when a destination is missing or invalid.
|
|
let sources = providers::credential_providers()
|
|
.flat_map(|provider| {
|
|
provider.sources().iter().flat_map(move |source| {
|
|
source
|
|
.env_vars
|
|
.iter()
|
|
.map(move |key| (*key, BrokeredCredentialProvider::Builtin(provider)))
|
|
})
|
|
})
|
|
.chain(self.configured_providers.iter().flat_map(|provider| {
|
|
provider.config.env.iter().map(move |key| {
|
|
(
|
|
key.as_str(),
|
|
BrokeredCredentialProvider::Configured(Arc::clone(provider)),
|
|
)
|
|
})
|
|
}));
|
|
let owners = sources
|
|
.filter_map(|(key, provider)| {
|
|
brokerable_credential_value(env, self, key, &provider)
|
|
.map(|value| (key.to_string(), value.to_string()))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
for (key, value) in owners {
|
|
self.remember_credential_owner(&key, &value);
|
|
}
|
|
}
|
|
|
|
fn remember_credential_owner(&mut self, env_var: &str, real_value: &str) {
|
|
if !self
|
|
.credential_owners
|
|
.iter()
|
|
.any(|owner| env_key_matches(&owner.env_var, env_var) && owner.real_value == real_value)
|
|
{
|
|
self.credential_owners.push(CredentialOwner {
|
|
env_var: env_var.to_string(),
|
|
real_value: real_value.to_string(),
|
|
});
|
|
}
|
|
}
|
|
|
|
fn register(
|
|
&mut self,
|
|
env_var: &str,
|
|
provider: BrokeredCredentialProvider,
|
|
host_binding: providers::CredentialHostBinding,
|
|
environment_id: Option<&str>,
|
|
real_value: &str,
|
|
existing_env: &HashMap<String, String>,
|
|
) -> Option<String> {
|
|
self.remember_credential_owner(env_var, real_value);
|
|
if let Some(existing) = self.credentials.iter_mut().find(|credential| {
|
|
env_key_matches(&credential.env_var, env_var)
|
|
&& credential.provider.same_provider(&provider)
|
|
&& credential.belongs_to_environment(environment_id)
|
|
&& credential.real_value == real_value
|
|
}) {
|
|
// Existing dummies were reconciled before fresh-value discovery.
|
|
if !env_contains_credential_value(existing_env, &existing.dummy_value) {
|
|
existing.observe_host_binding(host_binding, existing_env);
|
|
}
|
|
return Some(existing.dummy_value.clone());
|
|
}
|
|
if self.credentials.iter().any(|credential| {
|
|
is_builtin_shaped_credential(real_value) && credential.dummy_value.contains(real_value)
|
|
|| provider.contains_embedded_value(&credential.dummy_value, real_value)
|
|
}) {
|
|
tracing::warn!(
|
|
env_var,
|
|
"credential brokerage skipped: credential overlaps an existing dummy"
|
|
);
|
|
return None;
|
|
}
|
|
|
|
let Some(dummy_value) = (0..64).find_map(|_| {
|
|
let candidate = provider.dummy_value(real_value)?;
|
|
(candidate != real_value
|
|
// These records restore embedded dummies without configured regex boundaries.
|
|
&& (!is_builtin_shaped_credential(real_value)
|
|
|| candidate.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH)
|
|
&& !existing_env
|
|
.values()
|
|
.any(|value| value.contains(&candidate))
|
|
&& !self.credentials.iter().any(|credential| {
|
|
credential.dummy_value == candidate
|
|
|| credential.real_value == candidate
|
|
|| credential.contains_embedded_value(&candidate, &credential.dummy_value)
|
|
|| credential.contains_embedded_value(&candidate, &credential.real_value)
|
|
})
|
|
&& !credential_provider_definitions(self).any(|other| {
|
|
!other.same_provider(&provider)
|
|
&& other.recognizes_strictly_embedded_dummy_collision_in(&candidate)
|
|
}))
|
|
.then_some(candidate)
|
|
}) else {
|
|
tracing::warn!(
|
|
env_var,
|
|
"credential brokerage skipped: unable to generate a unique dummy credential"
|
|
);
|
|
return None;
|
|
};
|
|
let source_values = env_value(existing_env, env_var)
|
|
.map(|value| {
|
|
let value = match &provider {
|
|
BrokeredCredentialProvider::Builtin(_) => value.trim(),
|
|
BrokeredCredentialProvider::Configured(_) => value,
|
|
};
|
|
if value == real_value {
|
|
vec![real_value.to_string(), dummy_value.clone()]
|
|
} else if let Some(source) = self
|
|
.credentials
|
|
.iter()
|
|
.find(|source| source.dummy_value == value)
|
|
{
|
|
vec![source.real_value.clone(), source.dummy_value.clone()]
|
|
} else {
|
|
vec![value.to_string()]
|
|
}
|
|
})
|
|
.unwrap_or_default();
|
|
self.credentials.push(CredentialRecord {
|
|
env_var: env_var.to_string(),
|
|
provider,
|
|
host_binding,
|
|
additional_host_bindings: Vec::new(),
|
|
fallback_host_bindings: Vec::new(),
|
|
environment_id: environment_id.map(str::to_string),
|
|
real_value: real_value.to_string(),
|
|
dummy_value: dummy_value.clone(),
|
|
source_values,
|
|
generated_aliases: Arc::default(),
|
|
});
|
|
Some(dummy_value)
|
|
}
|
|
|
|
fn is_dummy_value(&self, value: &str) -> bool {
|
|
self.credentials
|
|
.iter()
|
|
.any(|credential| credential.dummy_value == value)
|
|
}
|
|
}
|
|
|
|
fn credential_provider_definitions(
|
|
state: &CredentialBrokerState,
|
|
) -> impl Iterator<Item = BrokeredCredentialProvider> + '_ {
|
|
providers::credential_providers()
|
|
.map(BrokeredCredentialProvider::Builtin)
|
|
.chain(
|
|
state
|
|
.configured_providers
|
|
.iter()
|
|
.cloned()
|
|
.map(BrokeredCredentialProvider::Configured),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "credential_broker_tests.rs"]
|
|
mod tests;
|
|
|
|
#[cfg(test)]
|
|
#[path = "credential_broker/configured_tests.rs"]
|
|
mod configured_tests;
|