mirror of
https://github.com/openai/codex.git
synced 2026-10-24 15:53:38 +00:00
Extend configured credential brokerage to embedded aliases (#44066)
## What changed - Discover configured provider credentials in child environment values even when canonical credential variables are absent. Match complete tokens while preserving regex word boundaries and avoiding ambiguous or overlapping provider matches. - Replace and restore credentials by their original spans, preserving adjacent tokens and generated dummy aliases across destination rebinding. Reject dummy values that embed another provider's credentials. - Extend text virtualization and provider source checks to configured credentials, including short credentials with distinctive prefixes, while preserving operational paths. - Expose trusted provider metadata and active bindings through `CredentialBrokerEnvironment`, and remove marked configured credential variables when stripping the managed proxy environment. ## Testing Add regression tests for alias discovery, regex boundaries and alternatives, overlapping and adjacent credentials, destination rebinding, dummy restoration, disallowed credential sources, and operational path preservation. GitOrigin-RevId: dd85c595ce9550fbfdb07ef296a23e458ca165dd
This commit is contained in:
Generated
+1
@@ -4076,6 +4076,7 @@ dependencies = [
|
||||
"rand 0.9.3",
|
||||
"rand_regex",
|
||||
"regex",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
"rustls-native-certs",
|
||||
"schannel",
|
||||
|
||||
@@ -421,6 +421,7 @@ rcgen = { version = "0.14.7", default-features = false, features = [
|
||||
"pem",
|
||||
] }
|
||||
regex = "1.12.3"
|
||||
regex-automata = { version = "0.4.13", default-features = false, features = ["std", "syntax", "unicode", "nfa-pikevm"] }
|
||||
regex-lite = "0.1.8"
|
||||
regex-syntax = "0.8.8"
|
||||
reqwest = { version = "0.12", features = ["cookies"] }
|
||||
|
||||
@@ -25,6 +25,7 @@ opentelemetry = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_regex = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
regex-automata = { workspace = true }
|
||||
regex-syntax = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
@@ -5,6 +5,7 @@ mod matching;
|
||||
mod provider_config;
|
||||
mod providers;
|
||||
mod registry;
|
||||
mod replacement;
|
||||
|
||||
use crate::config::NetworkProxyConfig;
|
||||
use crate::policy::normalize_host;
|
||||
@@ -13,14 +14,17 @@ 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::select_credentials;
|
||||
use replacement::Replacements;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
use url::Url;
|
||||
|
||||
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;
|
||||
@@ -29,6 +33,7 @@ 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;
|
||||
|
||||
@@ -68,6 +73,7 @@ struct CredentialRecord {
|
||||
environment_id: Option<String>,
|
||||
real_value: String,
|
||||
dummy_value: String,
|
||||
generated_aliases: Arc<RwLock<Vec<replacement::GeneratedAlias>>>,
|
||||
}
|
||||
|
||||
impl CredentialRecord {
|
||||
@@ -79,9 +85,7 @@ impl CredentialRecord {
|
||||
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
&& !crate::is_managed_proxy_env_var(key, value)
|
||||
&& (value == &self.real_value
|
||||
|| self
|
||||
.provider
|
||||
.contains_embedded_value(value, &self.real_value))
|
||||
|| self.contains_embedded_value(value, &self.real_value))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -366,6 +370,8 @@ impl CredentialBroker {
|
||||
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)
|
||||
})
|
||||
{
|
||||
@@ -447,13 +453,7 @@ impl CredentialBroker {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (dummy_value, real_value) in &stale_credentials {
|
||||
for value in env.values_mut() {
|
||||
if value.contains(dummy_value) {
|
||||
*value = value.replace(dummy_value, real_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.replace_child_env_dummies(env, &stale_credentials);
|
||||
state.credentials.retain(|credential| {
|
||||
!credential.belongs_to_environment(environment_id)
|
||||
|| !stale_credentials
|
||||
@@ -478,13 +478,7 @@ impl CredentialBroker {
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (dummy_value, real_value) in unbound_inherited_credentials {
|
||||
for value in env.values_mut() {
|
||||
if value.contains(&dummy_value) {
|
||||
*value = value.replace(&dummy_value, &real_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
state.replace_child_env_dummies(env, &unbound_inherited_credentials);
|
||||
|
||||
let inherited_credentials = active_sources
|
||||
.iter()
|
||||
@@ -504,11 +498,14 @@ impl CredentialBroker {
|
||||
source.host_binding.clone(),
|
||||
credential.real_value.clone(),
|
||||
credential.dummy_value.clone(),
|
||||
Arc::clone(&credential.generated_aliases),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for (env_var, provider, host_binding, real_value, inherited_dummy) in inherited_credentials
|
||||
let mut rebound_dummies = Vec::new();
|
||||
for (env_var, provider, host_binding, real_value, inherited_dummy, generated_aliases) in
|
||||
inherited_credentials
|
||||
{
|
||||
let current_dummy = if let Some(credential) =
|
||||
state.credentials.iter_mut().find(|credential| {
|
||||
@@ -528,17 +525,15 @@ impl CredentialBroker {
|
||||
environment_id: environment_id.map(str::to_string),
|
||||
real_value,
|
||||
dummy_value: inherited_dummy.clone(),
|
||||
generated_aliases,
|
||||
});
|
||||
inherited_dummy.clone()
|
||||
};
|
||||
if current_dummy != inherited_dummy {
|
||||
for value in env.values_mut() {
|
||||
if value.contains(&inherited_dummy) {
|
||||
*value = value.replace(&inherited_dummy, ¤t_dummy);
|
||||
}
|
||||
}
|
||||
rebound_dummies.push((inherited_dummy, current_dummy));
|
||||
}
|
||||
}
|
||||
state.replace_child_env_dummies(env, &rebound_dummies);
|
||||
|
||||
for source in &active_sources {
|
||||
for env_var in &source.env_vars {
|
||||
@@ -552,15 +547,8 @@ impl CredentialBroker {
|
||||
);
|
||||
}
|
||||
}
|
||||
let provider_context_keys = credential_broker_provider_context_env_keys()
|
||||
.map(str::to_string)
|
||||
.chain(
|
||||
state
|
||||
.configured_providers
|
||||
.iter()
|
||||
.filter_map(|provider| provider.config.url_prefix_from_env.as_ref().cloned()),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
let provider_context_keys = state.environment(env).provider_context_keys;
|
||||
let mut known_registrations = Vec::new();
|
||||
let discoverable_values = env
|
||||
.iter()
|
||||
.filter(|(key, value)| {
|
||||
@@ -571,8 +559,62 @@ impl CredentialBroker {
|
||||
.iter()
|
||||
.any(|context_key| env_key_matches(key, context_key))
|
||||
})
|
||||
.map(|(_, value)| value)
|
||||
.map(|(_, value)| {
|
||||
let known = matching::known_credential_matches(&state, value, env);
|
||||
for matched in &known {
|
||||
if matched.value == matched.real_value
|
||||
&& env_value(env, matched.env_var).is_none_or(|value| {
|
||||
!state.is_dummy_value(value)
|
||||
|| state.credentials.iter().any(|credential| {
|
||||
credential.dummy_value == 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| {
|
||||
@@ -586,11 +628,24 @@ impl CredentialBroker {
|
||||
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
|
||||
|| state
|
||||
.configured_providers
|
||||
.iter()
|
||||
.any(|configured| configured.matches_value(credential))
|
||||
|| matching::is_operational_path_match(
|
||||
value,
|
||||
start,
|
||||
@@ -602,6 +657,20 @@ impl CredentialBroker {
|
||||
.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)
|
||||
@@ -609,14 +678,20 @@ impl CredentialBroker {
|
||||
&& existing.host_binding == host_binding
|
||||
&& existing.real_value == credential
|
||||
})
|
||||
|| source.binding_env_vars.is_empty()
|
||||
&& state.credential_owners.iter().any(|existing| {
|
||||
!source
|
||||
.env_vars
|
||||
.iter()
|
||||
.any(|key| env_key_matches(key, &existing.env_var))
|
||||
&& 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
|
||||
@@ -628,7 +703,6 @@ impl CredentialBroker {
|
||||
)
|
||||
.is_none()
|
||||
})
|
||||
|| provider.request_header_value(credential).is_none()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -644,6 +718,52 @@ impl CredentialBroker {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (provider, value, matched) in &configured_discoveries {
|
||||
let Some(host_binding) = provider.host_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| {
|
||||
@@ -659,26 +779,17 @@ impl CredentialBroker {
|
||||
if crate::is_managed_proxy_env_var(key, value) {
|
||||
continue;
|
||||
}
|
||||
let mut virtualized = false;
|
||||
let mut replacements = Replacements::default();
|
||||
for credential in &credentials {
|
||||
if value == &credential.real_value {
|
||||
value.clone_from(&credential.dummy_value);
|
||||
virtualized = true;
|
||||
} else if credential
|
||||
.provider
|
||||
.contains_embedded_value(value, &credential.real_value)
|
||||
{
|
||||
*value = credential.provider.replace_embedded_value(
|
||||
value,
|
||||
&credential.real_value,
|
||||
for original in [&credential.real_value, &credential.dummy_value] {
|
||||
replacements.add(
|
||||
credential.value_match_ranges(value, original),
|
||||
&credential.dummy_value,
|
||||
Some(credential),
|
||||
);
|
||||
virtualized = true;
|
||||
} else if value.contains(&credential.dummy_value) {
|
||||
virtualized = true;
|
||||
}
|
||||
}
|
||||
if virtualized {
|
||||
if replacements.render(value) {
|
||||
credential_aliases.push(CredentialAlias {
|
||||
env_var: key.clone(),
|
||||
dummy_value: value.clone(),
|
||||
@@ -746,9 +857,7 @@ impl CredentialBroker {
|
||||
.iter()
|
||||
.filter(|credential| {
|
||||
text == credential.dummy_value
|
||||
|| credential
|
||||
.provider
|
||||
.contains_embedded_value(text, &credential.dummy_value)
|
||||
|| credential.contains_embedded_value(text, &credential.dummy_value)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
@@ -831,24 +940,104 @@ impl CredentialBroker {
|
||||
let mut credentials = state.credentials.iter().collect::<Vec<_>>();
|
||||
credentials
|
||||
.sort_unstable_by_key(|credential| std::cmp::Reverse(credential.dummy_value.len()));
|
||||
let mut restored = false;
|
||||
let mut replacements = Replacements::default();
|
||||
for credential in credentials {
|
||||
if text.as_str() == credential.dummy_value {
|
||||
text.clone_from(&credential.real_value);
|
||||
restored = true;
|
||||
} else if credential
|
||||
.provider
|
||||
.contains_embedded_value(text, &credential.dummy_value)
|
||||
{
|
||||
*text = credential.provider.replace_embedded_value(
|
||||
text,
|
||||
&credential.dummy_value,
|
||||
&credential.real_value,
|
||||
);
|
||||
restored = true;
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
restored
|
||||
|
||||
!known.is_empty() || builtin_recognized || configured_recognized
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -969,11 +1158,7 @@ fn virtualize_env_var(
|
||||
if let Some(previous_dummy) = previous_dummy
|
||||
&& previous_dummy != dummy_value
|
||||
{
|
||||
for value in env.values_mut() {
|
||||
if value.contains(&previous_dummy) {
|
||||
*value = value.replace(&previous_dummy, &dummy_value);
|
||||
}
|
||||
}
|
||||
state.replace_child_env_dummies(env, &[(previous_dummy, dummy_value.clone())]);
|
||||
}
|
||||
set_env_value(env, env_var, dummy_value);
|
||||
}
|
||||
@@ -1011,9 +1196,7 @@ impl CredentialBrokerState {
|
||||
!env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
||||
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
&& (value == &credential.dummy_value
|
||||
|| credential
|
||||
.provider
|
||||
.contains_embedded_value(value, &credential.dummy_value))
|
||||
|| credential.contains_embedded_value(value, &credential.dummy_value))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -1034,6 +1217,7 @@ impl CredentialBrokerState {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut replacements = Replacements::default();
|
||||
for credential in &credentials {
|
||||
if canonical_credential
|
||||
&& !self.credentials.iter().any(|candidate| {
|
||||
@@ -1045,19 +1229,21 @@ impl CredentialBrokerState {
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if value == &credential.dummy_value {
|
||||
value.clone_from(&credential.real_value);
|
||||
} else if credential
|
||||
.provider
|
||||
.contains_embedded_value(value, &credential.dummy_value)
|
||||
{
|
||||
*value = credential.provider.replace_embedded_value(
|
||||
value,
|
||||
&credential.dummy_value,
|
||||
&credential.real_value,
|
||||
);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1123,14 +1309,35 @@ impl CredentialBrokerState {
|
||||
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.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 {
|
||||
@@ -1148,6 +1355,7 @@ impl CredentialBrokerState {
|
||||
environment_id: environment_id.map(str::to_string),
|
||||
real_value: real_value.to_string(),
|
||||
dummy_value: dummy_value.clone(),
|
||||
generated_aliases: Arc::default(),
|
||||
});
|
||||
Some(dummy_value)
|
||||
}
|
||||
@@ -1159,6 +1367,20 @@ impl CredentialBrokerState {
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -13,7 +13,13 @@ use rama_http::HeaderName;
|
||||
use rama_http::HeaderValue;
|
||||
use rama_http::header::AUTHORIZATION;
|
||||
use rand::Rng as _;
|
||||
use regex::Regex;
|
||||
use regex::RegexBuilder;
|
||||
use regex_automata::Anchored;
|
||||
use regex_automata::Input;
|
||||
use regex_automata::MatchKind;
|
||||
use regex_automata::nfa::thompson;
|
||||
use regex_automata::nfa::thompson::pikevm::PikeVM;
|
||||
use regex_syntax::hir::Class;
|
||||
use regex_syntax::hir::ClassBytes;
|
||||
use regex_syntax::hir::ClassBytesRange;
|
||||
@@ -27,6 +33,7 @@ use std::collections::HashMap;
|
||||
const MAX_REGEX_BYTES: usize = 2048;
|
||||
const MAX_REGEX_REPEAT: u32 = 64;
|
||||
const MAX_DUMMY_ATTEMPTS: usize = 64;
|
||||
const MIN_DISTINCTIVE_CREDENTIAL_PREFIX_LENGTH: usize = 4;
|
||||
const MAX_DUMMY_VALUE_BYTES: usize = 2048;
|
||||
|
||||
pub(super) struct ConfiguredCredentialProvider {
|
||||
@@ -37,9 +44,18 @@ pub(super) struct ConfiguredCredentialProvider {
|
||||
}
|
||||
|
||||
struct ConfiguredCredentialPattern {
|
||||
full_matcher: regex::Regex,
|
||||
matcher: Regex,
|
||||
longest_matcher: PikeVM,
|
||||
known_value_matcher: Regex,
|
||||
full_matcher: Regex,
|
||||
ascii_generator: Option<rand_regex::Regex>,
|
||||
generator: rand_regex::Regex,
|
||||
has_distinctive_prefix: bool,
|
||||
}
|
||||
|
||||
pub(super) struct DiscoveredCredential {
|
||||
pub(super) range: std::ops::Range<usize>,
|
||||
pub(super) has_distinctive_prefix: bool,
|
||||
}
|
||||
|
||||
impl ConfiguredCredentialPattern {
|
||||
@@ -59,9 +75,10 @@ impl ConfiguredCredentialPattern {
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum DummyAlphabet {
|
||||
Ascii,
|
||||
Unicode,
|
||||
enum PatternPurpose {
|
||||
Discovery,
|
||||
AsciiDummy,
|
||||
UnicodeDummy,
|
||||
}
|
||||
|
||||
impl ConfiguredCredentialProvider {
|
||||
@@ -127,13 +144,38 @@ impl ConfiguredCredentialProvider {
|
||||
"credential pattern exceeds {MAX_REGEX_BYTES} bytes"
|
||||
);
|
||||
let parsed = regex_syntax::parse(pattern)?;
|
||||
let searchable =
|
||||
prepare_credential_pattern(parsed.clone(), PatternPurpose::Discovery);
|
||||
let has_distinctive_prefix = regex_syntax::hir::literal::Extractor::new()
|
||||
.extract(&searchable)
|
||||
.literals()
|
||||
.is_some_and(|prefixes| {
|
||||
!prefixes.is_empty()
|
||||
&& prefixes.iter().all(|prefix| {
|
||||
prefix.len() >= MIN_DISTINCTIVE_CREDENTIAL_PREFIX_LENGTH
|
||||
})
|
||||
});
|
||||
let searchable_pattern = searchable.to_string();
|
||||
let matcher = RegexBuilder::new(&searchable_pattern)
|
||||
.size_limit(1 << 20)
|
||||
.dfa_size_limit(1 << 20)
|
||||
.build()?;
|
||||
let longest_matcher = PikeVM::builder()
|
||||
.configure(PikeVM::config().match_kind(MatchKind::All))
|
||||
.thompson(thompson::Config::new().nfa_size_limit(Some(1 << 20)))
|
||||
.build(&searchable_pattern)?;
|
||||
let known_value_matcher =
|
||||
RegexBuilder::new(&format!(r"\A(?s:.)(?:{searchable_pattern})(?s:.)\z"))
|
||||
.size_limit(1 << 20)
|
||||
.dfa_size_limit(1 << 20)
|
||||
.build()?;
|
||||
let ascii_generator = rand_regex::Regex::with_hir(
|
||||
dummy_credential_pattern(parsed.clone(), DummyAlphabet::Ascii),
|
||||
prepare_credential_pattern(parsed.clone(), PatternPurpose::AsciiDummy),
|
||||
MAX_REGEX_REPEAT,
|
||||
)
|
||||
.ok();
|
||||
let generator = rand_regex::Regex::with_hir(
|
||||
dummy_credential_pattern(parsed.clone(), DummyAlphabet::Unicode),
|
||||
prepare_credential_pattern(parsed.clone(), PatternPurpose::UnicodeDummy),
|
||||
MAX_REGEX_REPEAT,
|
||||
)?;
|
||||
let full_pattern =
|
||||
@@ -144,7 +186,10 @@ impl ConfiguredCredentialProvider {
|
||||
.dfa_size_limit(1 << 20)
|
||||
.build()?;
|
||||
ensure!(
|
||||
!full_matcher.is_match(""),
|
||||
searchable
|
||||
.properties()
|
||||
.minimum_len()
|
||||
.is_some_and(|length| length > 0),
|
||||
"credential pattern must not match an empty value"
|
||||
);
|
||||
ensure!(
|
||||
@@ -152,9 +197,13 @@ impl ConfiguredCredentialProvider {
|
||||
"credential pattern must generate bounded UTF-8 values"
|
||||
);
|
||||
let compiled = ConfiguredCredentialPattern {
|
||||
matcher,
|
||||
longest_matcher,
|
||||
known_value_matcher,
|
||||
full_matcher,
|
||||
ascii_generator,
|
||||
generator,
|
||||
has_distinctive_prefix,
|
||||
};
|
||||
ensure!(
|
||||
compiled.candidates().next().is_some(),
|
||||
@@ -179,6 +228,98 @@ impl ConfiguredCredentialProvider {
|
||||
.any(|pattern| pattern.full_matcher.is_match(value))
|
||||
}
|
||||
|
||||
pub(super) fn find_credential_matches<'a>(
|
||||
&'a self,
|
||||
value: &'a str,
|
||||
) -> impl Iterator<Item = std::ops::Range<usize>> + 'a {
|
||||
self.credential_matches(value).map(|matched| matched.range)
|
||||
}
|
||||
|
||||
fn credential_matches<'a>(
|
||||
&'a self,
|
||||
value: &'a str,
|
||||
) -> impl Iterator<Item = DiscoveredCredential> + 'a {
|
||||
self.patterns.iter().flat_map(move |pattern| {
|
||||
let mut offset = 0;
|
||||
let mut cache = pattern.longest_matcher.create_cache();
|
||||
std::iter::from_fn(move || {
|
||||
let start = pattern.matcher.find_at(value, offset)?.start();
|
||||
// Anchor at the first match, then explore every alternative to its longest end.
|
||||
// Keep the full input so word-boundary assertions retain their context.
|
||||
let input = Input::new(value).range(start..).anchored(Anchored::Yes);
|
||||
let matched = pattern.longest_matcher.find(&mut cache, input)?;
|
||||
offset = matched.end();
|
||||
Some(matched.range())
|
||||
})
|
||||
.filter(move |matched| {
|
||||
matched.len() >= super::MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| pattern.has_distinctive_prefix
|
||||
})
|
||||
.map(|range| DiscoveredCredential {
|
||||
range,
|
||||
has_distinctive_prefix: pattern.has_distinctive_prefix,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn find_discoverable_credentials<'a>(
|
||||
&'a self,
|
||||
value: &'a str,
|
||||
) -> impl Iterator<Item = DiscoveredCredential> + 'a {
|
||||
// Registration needs a complete token; redaction must still catch embedded matches.
|
||||
self.credential_matches(value).filter(move |matched| {
|
||||
value
|
||||
.as_bytes()
|
||||
.get(matched.range.end)
|
||||
.is_none_or(|byte| !byte.is_ascii_alphanumeric() && !matches!(byte, b'_' | b'-'))
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn credential_value_match_ranges(
|
||||
&self,
|
||||
text: &str,
|
||||
credential: &str,
|
||||
) -> Vec<std::ops::Range<usize>> {
|
||||
let Some(first) = credential.chars().next() else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut offset = 0;
|
||||
let mut ranges = Vec::new();
|
||||
while let Some(relative) = text[offset..].find(credential) {
|
||||
let start = offset + relative;
|
||||
let end = start + credential.len();
|
||||
// Force the full known span to match while retaining adjacent word boundaries.
|
||||
let before = text[..start].chars().next_back().unwrap_or('\0');
|
||||
let after = text[end..].chars().next().unwrap_or('\0');
|
||||
let context = format!("{before}{credential}{after}");
|
||||
offset = if self
|
||||
.patterns
|
||||
.iter()
|
||||
.any(|pattern| pattern.known_value_matcher.is_match(&context))
|
||||
{
|
||||
ranges.push(start..end);
|
||||
end
|
||||
} else {
|
||||
start + first.len_utf8()
|
||||
};
|
||||
}
|
||||
ranges
|
||||
}
|
||||
|
||||
pub(super) fn contains_strictly_embedded_pattern_match(&self, value: &str) -> bool {
|
||||
self.patterns.iter().any(|pattern| {
|
||||
pattern
|
||||
.matcher
|
||||
.find_iter(value)
|
||||
.any(|matched| matched.start() > 0 || matched.end() < value.len())
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn find_credentials<'a>(&'a self, value: &'a str) -> impl Iterator<Item = &'a str> {
|
||||
self.find_credential_matches(value)
|
||||
.map(|matched| &value[matched])
|
||||
}
|
||||
|
||||
pub(super) fn host_binding(
|
||||
&self,
|
||||
env: &HashMap<String, String>,
|
||||
@@ -348,18 +489,18 @@ impl ConfiguredCredentialProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn dummy_credential_pattern(hir: Hir, alphabet: DummyAlphabet) -> Hir {
|
||||
fn prepare_credential_pattern(hir: Hir, purpose: PatternPurpose) -> Hir {
|
||||
match hir.into_kind() {
|
||||
HirKind::Empty => Hir::empty(),
|
||||
HirKind::Literal(literal) => {
|
||||
if matches!(alphabet, DummyAlphabet::Ascii) && !literal.0.is_ascii() {
|
||||
if matches!(purpose, PatternPurpose::AsciiDummy) && !literal.0.is_ascii() {
|
||||
Hir::fail()
|
||||
} else {
|
||||
Hir::literal(literal.0)
|
||||
}
|
||||
}
|
||||
HirKind::Class(mut class) => {
|
||||
if matches!(alphabet, DummyAlphabet::Ascii) {
|
||||
if matches!(purpose, PatternPurpose::AsciiDummy) {
|
||||
match &mut class {
|
||||
Class::Unicode(class) => {
|
||||
class.intersect(&ClassUnicode::new([ClassUnicodeRange::new(' ', '~')]))
|
||||
@@ -371,25 +512,45 @@ fn dummy_credential_pattern(hir: Hir, alphabet: DummyAlphabet) -> Hir {
|
||||
}
|
||||
Hir::class(class)
|
||||
}
|
||||
HirKind::Look(_) => Hir::empty(),
|
||||
HirKind::Look(look) => {
|
||||
if matches!(purpose, PatternPurpose::Discovery)
|
||||
&& !matches!(
|
||||
look,
|
||||
Look::Start
|
||||
| Look::End
|
||||
| Look::StartLF
|
||||
| Look::EndLF
|
||||
| Look::StartCRLF
|
||||
| Look::EndCRLF
|
||||
)
|
||||
{
|
||||
Hir::look(look)
|
||||
} else {
|
||||
Hir::empty()
|
||||
}
|
||||
}
|
||||
HirKind::Repetition(mut repetition) => {
|
||||
repetition.sub = Box::new(dummy_credential_pattern(*repetition.sub, alphabet));
|
||||
if matches!(purpose, PatternPurpose::Discovery) {
|
||||
// Discovery needs the whole credential, not a lazy prefix of it.
|
||||
repetition.greedy = true;
|
||||
}
|
||||
repetition.sub = Box::new(prepare_credential_pattern(*repetition.sub, purpose));
|
||||
Hir::repetition(repetition)
|
||||
}
|
||||
HirKind::Capture(mut capture) => {
|
||||
capture.sub = Box::new(dummy_credential_pattern(*capture.sub, alphabet));
|
||||
capture.sub = Box::new(prepare_credential_pattern(*capture.sub, purpose));
|
||||
Hir::capture(capture)
|
||||
}
|
||||
HirKind::Concat(expressions) => Hir::concat(
|
||||
expressions
|
||||
.into_iter()
|
||||
.map(|expression| dummy_credential_pattern(expression, alphabet))
|
||||
.map(|expression| prepare_credential_pattern(expression, purpose))
|
||||
.collect(),
|
||||
),
|
||||
HirKind::Alternation(expressions) => {
|
||||
let mut expressions = expressions
|
||||
.into_iter()
|
||||
.map(|expression| dummy_credential_pattern(expression, alphabet))
|
||||
.map(|expression| prepare_credential_pattern(expression, purpose))
|
||||
.filter(|expression| expression.properties().minimum_len().is_some())
|
||||
.collect::<Vec<_>>();
|
||||
expressions.sort_by_key(|expression| {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,15 +9,102 @@ use super::env_key_matches;
|
||||
use super::env_value;
|
||||
use super::matching;
|
||||
use super::providers;
|
||||
use super::registry::BrokeredCredentialProvider;
|
||||
use super::remove_env_value;
|
||||
use super::set_env_value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Trusted provider metadata and active credential bindings for one child environment.
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct CredentialBrokerEnvironment {
|
||||
pub credential_keys: Vec<String>,
|
||||
pub binding_keys: Vec<String>,
|
||||
pub context_keys: Vec<String>,
|
||||
pub provider_keys: Vec<String>,
|
||||
pub provider_context_keys: Vec<String>,
|
||||
}
|
||||
|
||||
impl CredentialBrokerState {
|
||||
pub(super) fn environment(&self, env: &HashMap<String, String>) -> CredentialBrokerEnvironment {
|
||||
let mut metadata = CredentialBrokerEnvironment::default();
|
||||
for key in
|
||||
providers::credential_env_keys().chain(credential_broker_provider_context_env_keys())
|
||||
{
|
||||
push_unique_key(&mut metadata.provider_keys, key);
|
||||
}
|
||||
for key in credential_broker_provider_context_env_keys() {
|
||||
push_unique_key(&mut metadata.provider_context_keys, key);
|
||||
}
|
||||
for provider in &self.configured_providers {
|
||||
for key in &provider.config.env {
|
||||
push_unique_key(&mut metadata.provider_keys, key);
|
||||
}
|
||||
if let Some(key) = provider.config.url_prefix_from_env.as_deref() {
|
||||
push_unique_key(&mut metadata.provider_keys, key);
|
||||
push_unique_key(&mut metadata.provider_context_keys, key);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.enabled {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
metadata.credential_keys = brokered_credential_dummy_env_keys(env);
|
||||
let marked_credentials = env_value(env, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
.and_then(|marker| serde_json::from_str::<Vec<(String, String)>>(marker).ok())
|
||||
.unwrap_or_default();
|
||||
for credential in &self.credentials {
|
||||
if matches!(
|
||||
credential.provider,
|
||||
BrokeredCredentialProvider::Configured(_)
|
||||
) && env_value(env, &credential.env_var) == Some(credential.dummy_value.as_str())
|
||||
&& marked_credentials.iter().any(|(key, value)| {
|
||||
env_key_matches(key, &credential.env_var) && value == &credential.dummy_value
|
||||
})
|
||||
{
|
||||
push_unique_key(&mut metadata.credential_keys, &credential.env_var);
|
||||
}
|
||||
}
|
||||
|
||||
for key in providers::credential_context_env_keys(&metadata.credential_keys) {
|
||||
if env_value(env, key).is_some() {
|
||||
push_unique_key(&mut metadata.context_keys, key);
|
||||
}
|
||||
}
|
||||
for key in providers::credential_binding_env_keys(&metadata.credential_keys) {
|
||||
if env_value(env, key).is_some() {
|
||||
push_unique_key(&mut metadata.binding_keys, key);
|
||||
}
|
||||
}
|
||||
for provider in &self.configured_providers {
|
||||
if provider.config.env.iter().any(|provider_key| {
|
||||
metadata
|
||||
.credential_keys
|
||||
.iter()
|
||||
.any(|credential_key| env_key_matches(provider_key, credential_key))
|
||||
}) && let Some(key) = provider.config.url_prefix_from_env.as_deref()
|
||||
&& env_value(env, key).is_some()
|
||||
{
|
||||
push_unique_key(&mut metadata.context_keys, key);
|
||||
push_unique_key(&mut metadata.binding_keys, key);
|
||||
}
|
||||
}
|
||||
|
||||
metadata
|
||||
}
|
||||
}
|
||||
|
||||
fn push_unique_key(keys: &mut Vec<String>, key: &str) {
|
||||
if !keys.iter().any(|candidate| env_key_matches(candidate, key)) {
|
||||
keys.push(key.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn update_brokered_credentials_marker(
|
||||
state: &CredentialBrokerState,
|
||||
env: &mut HashMap<String, String>,
|
||||
) {
|
||||
let mut brokered = state
|
||||
let credentials = state
|
||||
.credentials
|
||||
.iter()
|
||||
.filter(|credential| {
|
||||
@@ -25,26 +112,28 @@ pub(super) fn update_brokered_credentials_marker(
|
||||
!env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
||||
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
&& (value == &credential.dummy_value
|
||||
|| credential.dummy_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
&& value.contains(&credential.dummy_value))
|
||||
|| credential.contains_embedded_value(value, &credential.dummy_value))
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut brokered = credentials
|
||||
.iter()
|
||||
.map(|credential| (credential.env_var.clone(), credential.dummy_value.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let brokered_dummy_values = brokered
|
||||
.iter()
|
||||
.map(|(_, dummy_value)| dummy_value.clone())
|
||||
.collect::<Vec<_>>();
|
||||
brokered.extend(state.credential_aliases.iter().filter_map(|alias| {
|
||||
let dummy_value = brokered_dummy_values.iter().find(|&dummy_value| {
|
||||
alias.dummy_value == *dummy_value
|
||||
|| dummy_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
&& alias.dummy_value.contains(dummy_value)
|
||||
})?;
|
||||
Some((
|
||||
format!("{BROKERED_CREDENTIAL_ALIAS_MARKER_PREFIX}{}", alias.env_var),
|
||||
dummy_value.clone(),
|
||||
))
|
||||
brokered.extend(state.credential_aliases.iter().flat_map(|alias| {
|
||||
credentials
|
||||
.iter()
|
||||
.filter(|credential| {
|
||||
alias.dummy_value == credential.dummy_value
|
||||
|| credential
|
||||
.contains_embedded_value(&alias.dummy_value, &credential.dummy_value)
|
||||
})
|
||||
.map(|credential| {
|
||||
(
|
||||
format!("{BROKERED_CREDENTIAL_ALIAS_MARKER_PREFIX}{}", alias.env_var),
|
||||
credential.dummy_value.clone(),
|
||||
)
|
||||
})
|
||||
}));
|
||||
brokered.sort_unstable();
|
||||
brokered.dedup();
|
||||
@@ -65,16 +154,10 @@ pub(super) fn update_brokered_credentials_marker(
|
||||
/// 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<String, String>) -> Vec<String> {
|
||||
let mut keys = env_value(env, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
.and_then(|marker| serde_json::from_str::<Vec<(String, String)>>(marker).ok())
|
||||
.unwrap_or_default()
|
||||
let mut keys = marked_credential_dummy_env_keys(env)
|
||||
.into_iter()
|
||||
.filter_map(|(key, dummy_value)| {
|
||||
providers::credential_env_keys()
|
||||
.any(|candidate| env_key_matches(&key, candidate))
|
||||
.then_some(())?;
|
||||
let (actual_key, actual_value) = env_entry(env, &key)?;
|
||||
(actual_value == dummy_value.as_str()).then(|| actual_key.to_string())
|
||||
.filter(|key| {
|
||||
providers::credential_env_keys().any(|candidate| env_key_matches(key, candidate))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let context_bound = |key: &str| {
|
||||
@@ -99,6 +182,18 @@ pub fn brokered_credential_dummy_env_keys(env: &HashMap<String, String>) -> Vec<
|
||||
keys
|
||||
}
|
||||
|
||||
pub(crate) fn marked_credential_dummy_env_keys(env: &HashMap<String, String>) -> Vec<String> {
|
||||
env_value(env, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
.and_then(|marker| serde_json::from_str::<Vec<(String, String)>>(marker).ok())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|(key, dummy_value)| {
|
||||
let (actual_key, actual_value) = env_entry(env, &key)?;
|
||||
(actual_value == dummy_value.as_str()).then(|| actual_key.to_string())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Returns canonical credential and known alias keys recorded for an active brokered child,
|
||||
/// including configured provider keys and keys that are currently absent.
|
||||
pub fn brokered_credential_marker_env_keys(env: &HashMap<String, String>) -> Vec<String> {
|
||||
@@ -120,11 +215,9 @@ pub fn brokered_credential_marker_env_keys(env: &HashMap<String, String>) -> Vec
|
||||
return (!value.is_empty()).then_some(key);
|
||||
}
|
||||
let alias_key = key.strip_prefix(BROKERED_CREDENTIAL_ALIAS_MARKER_PREFIX)?;
|
||||
let known_alias = dummy_values.iter().any(|dummy_value| {
|
||||
value == *dummy_value
|
||||
|| dummy_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
&& value.contains(dummy_value)
|
||||
});
|
||||
let known_alias = dummy_values
|
||||
.iter()
|
||||
.any(|dummy_value| value.contains(dummy_value));
|
||||
(known_alias && valid_environment_key(alias_key)).then(|| alias_key.to_string())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
@@ -139,12 +232,16 @@ pub fn brokered_credential_value_env_keys(env: &HashMap<String, String>) -> Vec<
|
||||
if env_value(env, CREDENTIAL_BROKER_ACTIVE_ENV_KEY) != Some("1") {
|
||||
return Vec::new();
|
||||
}
|
||||
let dummy_values = env_value(env, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
let marked_values = env_value(env, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
.and_then(|marker| serde_json::from_str::<Vec<(String, String)>>(marker).ok())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|(key, dummy_value)| {
|
||||
(valid_environment_key(&key) && !dummy_value.is_empty()).then_some(dummy_value)
|
||||
.filter(|(key, dummy_value)| {
|
||||
!dummy_value.is_empty()
|
||||
&& (valid_environment_key(key)
|
||||
|| key
|
||||
.strip_prefix(BROKERED_CREDENTIAL_ALIAS_MARKER_PREFIX)
|
||||
.is_some_and(valid_environment_key))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut keys = env
|
||||
@@ -153,10 +250,13 @@ pub fn brokered_credential_value_env_keys(env: &HashMap<String, String>) -> Vec<
|
||||
!env_key_matches(key, CREDENTIAL_BROKER_ACTIVE_ENV_KEY)
|
||||
&& !env_key_matches(key, BROKERED_CREDENTIALS_ENV_KEY)
|
||||
})
|
||||
.filter(|(_, value)| {
|
||||
dummy_values.iter().any(|dummy| {
|
||||
.filter(|(key, value)| {
|
||||
marked_values.iter().any(|(marked_key, dummy)| {
|
||||
value.as_str() == dummy.as_str()
|
||||
|| dummy.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| (dummy.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| marked_key
|
||||
.strip_prefix(BROKERED_CREDENTIAL_ALIAS_MARKER_PREFIX)
|
||||
.is_some_and(|alias_key| env_key_matches(key, alias_key)))
|
||||
&& value.contains(dummy.as_str())
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,35 +1,189 @@
|
||||
use super::CredentialBrokerState;
|
||||
use super::MIN_EMBEDDED_CREDENTIAL_LENGTH;
|
||||
use super::brokered_credential_dummy_env_keys;
|
||||
use super::env_key_matches;
|
||||
use super::env_value;
|
||||
use super::prioritized_credentials;
|
||||
use super::providers;
|
||||
use super::registry::BrokeredCredentialProvider;
|
||||
use super::registry::is_builtin_shaped_credential;
|
||||
use super::replacement::Replacements;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use url::Position;
|
||||
use url::Url;
|
||||
|
||||
pub(super) struct KnownCredentialMatch<'a> {
|
||||
pub(super) range: std::ops::Range<usize>,
|
||||
pub(super) env_var: &'a str,
|
||||
pub(super) provider: BrokeredCredentialProvider,
|
||||
pub(super) real_value: &'a str,
|
||||
pub(super) value: &'a str,
|
||||
}
|
||||
|
||||
pub(super) fn known_credential_matches<'a>(
|
||||
state: &'a CredentialBrokerState,
|
||||
text: &str,
|
||||
source_env: &'a HashMap<String, String>,
|
||||
) -> Vec<KnownCredentialMatch<'a>> {
|
||||
let mut matches = Vec::new();
|
||||
let mut add = |env_var: &'a str,
|
||||
provider: &BrokeredCredentialProvider,
|
||||
real_value: &'a str,
|
||||
value: &'a str| {
|
||||
if value.is_empty() {
|
||||
return;
|
||||
}
|
||||
let ranges = if text == value {
|
||||
std::iter::once(0..value.len()).collect()
|
||||
} else if value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
&& (is_builtin_shaped_credential(real_value)
|
||||
|| matches!(provider, BrokeredCredentialProvider::Builtin(_)))
|
||||
{
|
||||
text.match_indices(value)
|
||||
.map(|(start, _)| start..start + value.len())
|
||||
.collect()
|
||||
} else if let BrokeredCredentialProvider::Configured(provider) = provider {
|
||||
// Configured short values retain their pattern boundaries and path exclusions.
|
||||
provider
|
||||
.credential_value_match_ranges(text, value)
|
||||
.into_iter()
|
||||
.filter(|range| {
|
||||
value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| !is_operational_path_match(text, range.start, range.end)
|
||||
// A complete longer configured token still needs its own source permission.
|
||||
&& !provider.find_discoverable_credentials(text).any(|candidate| {
|
||||
candidate.range.start <= range.start && candidate.range.end > range.end
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
matches.extend(ranges.into_iter().map(|range| KnownCredentialMatch {
|
||||
range,
|
||||
env_var,
|
||||
provider: provider.clone(),
|
||||
real_value,
|
||||
value,
|
||||
}));
|
||||
};
|
||||
for credential in &state.credentials {
|
||||
for value in [&credential.real_value, &credential.dummy_value] {
|
||||
add(
|
||||
&credential.env_var,
|
||||
&credential.provider,
|
||||
&credential.real_value,
|
||||
value,
|
||||
);
|
||||
}
|
||||
}
|
||||
for provider in super::credential_provider_definitions(state) {
|
||||
let owns_key = |key: &str| match &provider {
|
||||
BrokeredCredentialProvider::Builtin(provider) => {
|
||||
provider.sources().iter().any(|source| {
|
||||
source
|
||||
.env_vars
|
||||
.iter()
|
||||
.any(|source| env_key_matches(source, key))
|
||||
})
|
||||
}
|
||||
BrokeredCredentialProvider::Configured(provider) => provider
|
||||
.config
|
||||
.env
|
||||
.iter()
|
||||
.any(|source| env_key_matches(source, key)),
|
||||
};
|
||||
for owner in &state.credential_owners {
|
||||
if owns_key(&owner.env_var) {
|
||||
add(
|
||||
&owner.env_var,
|
||||
&provider,
|
||||
&owner.real_value,
|
||||
&owner.real_value,
|
||||
);
|
||||
}
|
||||
}
|
||||
for key in source_env.keys() {
|
||||
if owns_key(key)
|
||||
&& let Some(real) =
|
||||
super::brokerable_credential_value(source_env, state, key, &provider)
|
||||
{
|
||||
add(key, &provider, real, real);
|
||||
}
|
||||
}
|
||||
}
|
||||
for credential in &state.credentials {
|
||||
matches.extend(
|
||||
credential
|
||||
.generated_dummy_ranges(text)
|
||||
.into_iter()
|
||||
.map(|range| KnownCredentialMatch {
|
||||
range,
|
||||
env_var: &credential.env_var,
|
||||
provider: credential.provider.clone(),
|
||||
real_value: &credential.real_value,
|
||||
value: &credential.dummy_value,
|
||||
}),
|
||||
);
|
||||
}
|
||||
// Longest exact identities win over overlapping shorter identities, as in replacement.
|
||||
matches.sort_unstable_by_key(|matched| {
|
||||
(std::cmp::Reverse(matched.range.len()), matched.range.start)
|
||||
});
|
||||
let mut selected = Vec::<KnownCredentialMatch<'_>>::new();
|
||||
for matched in matches {
|
||||
if selected.iter().any(|known| {
|
||||
known.range.start < matched.range.end
|
||||
&& matched.range.start < known.range.end
|
||||
&& (known.range != matched.range
|
||||
|| known.provider.same_provider(&matched.provider)
|
||||
&& env_key_matches(known.env_var, matched.env_var)
|
||||
&& known.real_value == matched.real_value)
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
selected.push(matched);
|
||||
}
|
||||
selected
|
||||
}
|
||||
|
||||
pub(super) fn mask_known_credentials(text: &str, matches: &[KnownCredentialMatch<'_>]) -> String {
|
||||
let mut uncovered = text.to_string();
|
||||
for matched in matches {
|
||||
uncovered.replace_range(matched.range.clone(), &"\0".repeat(matched.range.len()));
|
||||
}
|
||||
uncovered
|
||||
}
|
||||
|
||||
pub(super) fn virtualize_text(
|
||||
state: &CredentialBrokerState,
|
||||
text: &mut String,
|
||||
output: &mut String,
|
||||
env: &HashMap<String, String>,
|
||||
) -> bool {
|
||||
if !state.enabled {
|
||||
return true;
|
||||
}
|
||||
|
||||
let allowed_keys = brokered_credential_dummy_env_keys(env);
|
||||
let allowed_keys = state.environment(env).credential_keys;
|
||||
let credentials = prioritized_credentials(state, env);
|
||||
let text = output.as_str();
|
||||
let source_env = HashMap::new();
|
||||
let known = known_credential_matches(state, text, &source_env);
|
||||
let mut replacements = Replacements::default();
|
||||
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 {
|
||||
let ranges = known
|
||||
.iter()
|
||||
.filter(|matched| {
|
||||
matched.provider.same_provider(&credential.provider)
|
||||
&& env_key_matches(matched.env_var, &credential.env_var)
|
||||
&& matched.real_value == credential.real_value
|
||||
&& (matched.value == credential.real_value
|
||||
|| matched.value == credential.dummy_value)
|
||||
})
|
||||
.map(|matched| matched.range.clone())
|
||||
.collect::<Vec<_>>();
|
||||
if ranges.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -42,28 +196,33 @@ pub(super) fn virtualize_text(
|
||||
}) || state.credential_aliases.iter().any(|alias| {
|
||||
env_value(env, &alias.env_var) == Some(alias.dummy_value.as_str())
|
||||
&& (alias.dummy_value == candidate.dummy_value
|
||||
|| candidate.dummy_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
&& alias.dummy_value.contains(&candidate.dummy_value))
|
||||
|| candidate.contains_embedded_value(
|
||||
&alias.dummy_value,
|
||||
&candidate.dummy_value,
|
||||
))
|
||||
&& !state.credentials.iter().any(|other| {
|
||||
other.dummy_value != candidate.dummy_value
|
||||
&& (alias.dummy_value == other.dummy_value
|
||||
|| other.dummy_value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
&& alias.dummy_value.contains(&other.dummy_value))
|
||||
|| other.contains_embedded_value(
|
||||
&alias.dummy_value,
|
||||
&other.dummy_value,
|
||||
))
|
||||
})
|
||||
}))
|
||||
});
|
||||
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);
|
||||
}
|
||||
replacements.add(
|
||||
ranges,
|
||||
replacement.map_or("", |candidate| candidate.dummy_value.as_str()),
|
||||
replacement,
|
||||
);
|
||||
}
|
||||
|
||||
let original = text;
|
||||
let mut uncovered = replacements.masked(original);
|
||||
let text = &mut uncovered;
|
||||
// Startup can copy a supported credential before unsetting its source variable.
|
||||
for provider in providers::credential_providers() {
|
||||
for prefix in provider.credential_prefixes {
|
||||
@@ -73,10 +232,19 @@ pub(super) fn virtualize_text(
|
||||
if let Some(length) = state
|
||||
.credentials
|
||||
.iter()
|
||||
.flat_map(|credential| [&credential.real_value, &credential.dummy_value])
|
||||
.filter(|credential| credential.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH)
|
||||
.filter(|credential| text[start..].starts_with(credential.as_str()))
|
||||
.map(String::len)
|
||||
.flat_map(|credential| {
|
||||
[&credential.real_value, &credential.dummy_value]
|
||||
.map(|value| (credential, value))
|
||||
})
|
||||
.filter(|(_, value)| text[start..].starts_with(value.as_str()))
|
||||
.filter(|(credential, value)| {
|
||||
value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| matches!(&credential.provider, BrokeredCredentialProvider::Configured(provider)
|
||||
if provider.credential_value_match_ranges(text, value)
|
||||
.iter().any(|range| range.start == start
|
||||
&& !is_operational_path_match(text, range.start, range.end)))
|
||||
})
|
||||
.map(|(_, value)| value.len())
|
||||
.max()
|
||||
{
|
||||
offset = start + length;
|
||||
@@ -136,7 +304,9 @@ pub(super) fn virtualize_text(
|
||||
let ignored_credential_match =
|
||||
ignored_credential_match(provider, text, start, credential);
|
||||
if length >= provider.minimum_credential_len && !ignored_credential_match {
|
||||
text.replace_range(start..end, "");
|
||||
replacements.add(std::iter::once(start..end), "", /*dummy*/ None);
|
||||
text.replace_range(start..end, &"\0".repeat(length));
|
||||
offset = end;
|
||||
allowed = false;
|
||||
} else {
|
||||
offset = start
|
||||
@@ -150,6 +320,55 @@ pub(super) fn virtualize_text(
|
||||
}
|
||||
}
|
||||
|
||||
for provider in &state.configured_providers {
|
||||
if provider.host_binding(env).is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut matches = provider
|
||||
.find_credential_matches(text)
|
||||
.map(|matched| (matched.start, matched.end))
|
||||
.collect::<Vec<_>>();
|
||||
matches.sort_unstable_by_key(|(start, end)| (std::cmp::Reverse(end - start), *start));
|
||||
matches.dedup();
|
||||
let mut removed_ranges = Vec::<(usize, usize)>::new();
|
||||
for (start, end) in matches {
|
||||
let credential = &text[start..end];
|
||||
if credential.contains('\0')
|
||||
|| !provider
|
||||
.credential_value_match_ranges(original, credential)
|
||||
.contains(&(start..end))
|
||||
|| is_operational_path_match(original, start, end)
|
||||
|| state
|
||||
.credentials
|
||||
.iter()
|
||||
.flat_map(|known| [&known.real_value, &known.dummy_value])
|
||||
.flat_map(|known| {
|
||||
text.match_indices(known)
|
||||
.map(move |(index, _)| (index, known))
|
||||
})
|
||||
.any(|(index, known)| index <= start && end <= index + known.len())
|
||||
|| provider
|
||||
.config
|
||||
.env
|
||||
.iter()
|
||||
.any(|key| env_value(env, key).is_some_and(|value| value == credential))
|
||||
|| removed_ranges.iter().any(|(removed_start, removed_end)| {
|
||||
start < *removed_end && *removed_start < end
|
||||
})
|
||||
{
|
||||
continue;
|
||||
}
|
||||
removed_ranges.push((start, end));
|
||||
allowed = false;
|
||||
}
|
||||
removed_ranges.sort_unstable_by_key(|(start, _)| std::cmp::Reverse(*start));
|
||||
for (start, end) in removed_ranges {
|
||||
replacements.add(std::iter::once(start..end), "", /*dummy*/ None);
|
||||
text.replace_range(start..end, &"\0".repeat(end - start));
|
||||
}
|
||||
}
|
||||
|
||||
replacements.render(output);
|
||||
allowed
|
||||
}
|
||||
|
||||
@@ -311,5 +530,11 @@ pub(super) fn builtin_credential_candidate<'a>(
|
||||
{
|
||||
length = separator;
|
||||
}
|
||||
&value[start..start + length]
|
||||
let candidate = &value[start..start + length];
|
||||
if value.as_bytes().get(start + length) == Some(&b'\0') {
|
||||
// A masked known span may follow an alias separator, not part of this builtin token.
|
||||
candidate.trim_end_matches(['_', '-'])
|
||||
} else {
|
||||
candidate
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use super::MIN_EMBEDDED_CREDENTIAL_LENGTH;
|
||||
use super::configured::ConfiguredCredentialProvider;
|
||||
use super::env_entry;
|
||||
use super::env_value;
|
||||
use super::matching;
|
||||
use super::matching::is_operational_path_match;
|
||||
use super::providers;
|
||||
use base64::Engine;
|
||||
use rama_http::HeaderMap;
|
||||
@@ -44,17 +46,42 @@ impl BrokeredCredentialProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn contains_embedded_value(&self, text: &str, value: &str) -> bool {
|
||||
value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH && text.contains(value)
|
||||
pub(super) fn recognizes_credential_in(&self, value: &str) -> bool {
|
||||
match self {
|
||||
Self::Builtin(provider) => provider.credential_prefixes.iter().any(|prefix| {
|
||||
value.match_indices(*prefix).any(|(start, _)| {
|
||||
matching::recognized_credential_match(provider, value, "", start).is_some()
|
||||
})
|
||||
}),
|
||||
Self::Configured(provider) => provider.find_credential_matches(value).next().is_some(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn replace_embedded_value(
|
||||
&self,
|
||||
text: &str,
|
||||
value: &str,
|
||||
replacement: &str,
|
||||
) -> String {
|
||||
text.replace(value, replacement)
|
||||
pub(super) fn recognizes_strictly_embedded_dummy_collision_in(&self, value: &str) -> bool {
|
||||
match self {
|
||||
Self::Builtin(provider) => provider.credential_prefixes.iter().any(|prefix| {
|
||||
value.match_indices(*prefix).any(|(start, _)| {
|
||||
matching::recognized_credential_match(provider, value, "", start)
|
||||
.is_some_and(|credential| start > 0 || credential.len() < value.len())
|
||||
})
|
||||
}),
|
||||
Self::Configured(provider) => provider.contains_strictly_embedded_pattern_match(value),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn contains_embedded_value(&self, text: &str, value: &str) -> bool {
|
||||
match self {
|
||||
Self::Builtin(_) => {
|
||||
value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH && text.contains(value)
|
||||
}
|
||||
Self::Configured(provider) => provider
|
||||
.credential_value_match_ranges(text, value)
|
||||
.into_iter()
|
||||
.any(|range| {
|
||||
value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| !is_operational_path_match(text, range.start, range.end)
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn request_header_value(&self, value: &str) -> Option<HeaderValue> {
|
||||
@@ -104,6 +131,58 @@ impl BrokeredCredentialProvider {
|
||||
}
|
||||
}
|
||||
|
||||
impl CredentialRecord {
|
||||
pub(super) fn contains_embedded_value(&self, text: &str, value: &str) -> bool {
|
||||
!self.value_match_ranges(text, value).is_empty()
|
||||
}
|
||||
|
||||
pub(super) fn value_match_ranges(
|
||||
&self,
|
||||
text: &str,
|
||||
value: &str,
|
||||
) -> Vec<std::ops::Range<usize>> {
|
||||
let mut ranges = if text == value {
|
||||
std::iter::once(0..text.len()).collect()
|
||||
} else if is_builtin_shaped_credential(&self.real_value)
|
||||
|| matches!(self.provider, BrokeredCredentialProvider::Builtin(_))
|
||||
&& value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
{
|
||||
text.match_indices(value)
|
||||
.map(|(start, _)| start..start + value.len())
|
||||
.collect()
|
||||
} else if let BrokeredCredentialProvider::Configured(provider) = &self.provider {
|
||||
provider
|
||||
.credential_value_match_ranges(text, value)
|
||||
.into_iter()
|
||||
.filter(|range| {
|
||||
value.len() >= MIN_EMBEDDED_CREDENTIAL_LENGTH
|
||||
|| !is_operational_path_match(text, range.start, range.end)
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
if value == self.dummy_value {
|
||||
ranges.extend(self.generated_dummy_ranges(text));
|
||||
ranges.sort_by_key(|range| range.start);
|
||||
ranges.dedup();
|
||||
}
|
||||
ranges
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn is_builtin_shaped_credential(value: &str) -> bool {
|
||||
providers::credential_providers().any(|provider| {
|
||||
value.len() >= provider.minimum_credential_len
|
||||
&& provider
|
||||
.credential_prefixes
|
||||
.iter()
|
||||
.any(|prefix| value.starts_with(prefix))
|
||||
&& matching::recognized_credential_match(provider, value, "", /*start*/ 0)
|
||||
.is_some_and(|credential| credential == value)
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn select_credentials<'a>(
|
||||
headers: &HeaderMap,
|
||||
host: &str,
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
use super::CredentialBrokerState;
|
||||
use super::CredentialRecord;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use std::collections::HashMap;
|
||||
use std::ops::Range;
|
||||
|
||||
impl CredentialBrokerState {
|
||||
pub(super) fn replace_child_env_dummies(
|
||||
&self,
|
||||
env: &mut HashMap<String, String>,
|
||||
replacements: &[(String, String)],
|
||||
) {
|
||||
if replacements.is_empty() {
|
||||
return;
|
||||
}
|
||||
let credentials = self
|
||||
.credentials
|
||||
.iter()
|
||||
.map(|credential| {
|
||||
let replacement = replacements
|
||||
.iter()
|
||||
.find(|(dummy, _)| *dummy == credential.dummy_value)
|
||||
.map_or(credential.dummy_value.as_str(), |(_, replacement)| {
|
||||
replacement
|
||||
});
|
||||
let target = self
|
||||
.credentials
|
||||
.iter()
|
||||
.find(|candidate| candidate.dummy_value == replacement);
|
||||
(credential, replacement, target)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for value in env.values_mut() {
|
||||
let mut spans = Replacements::default();
|
||||
for (credential, replacement, target) in &credentials {
|
||||
spans.add(
|
||||
credential.value_match_ranges(value, &credential.dummy_value),
|
||||
replacement,
|
||||
*target,
|
||||
);
|
||||
}
|
||||
spans.render(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retain emitted context without keeping another copy of any real credentials in it.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub(super) struct GeneratedAlias {
|
||||
digest: [u8; 32],
|
||||
len: usize,
|
||||
range: Range<usize>,
|
||||
}
|
||||
|
||||
impl CredentialRecord {
|
||||
pub(super) fn generated_dummy_ranges(&self, text: &str) -> Vec<Range<usize>> {
|
||||
let aliases = self
|
||||
.generated_aliases
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
text.char_indices()
|
||||
.filter(|(start, _)| text[*start..].starts_with(&self.dummy_value))
|
||||
.filter_map(|(start, _)| {
|
||||
aliases
|
||||
.iter()
|
||||
.any(|alias| {
|
||||
start.checked_sub(alias.range.start).is_some_and(|offset| {
|
||||
text.get(offset..offset + alias.len).is_some_and(|context| {
|
||||
<[u8; 32]>::from(Sha256::digest(context)) == alias.digest
|
||||
})
|
||||
})
|
||||
})
|
||||
.then_some(start..start + self.dummy_value.len())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Replacements<'a> {
|
||||
spans: Vec<(Range<usize>, &'a str, Option<&'a CredentialRecord>)>,
|
||||
}
|
||||
|
||||
impl<'a> Replacements<'a> {
|
||||
pub(super) fn add(
|
||||
&mut self,
|
||||
ranges: impl IntoIterator<Item = Range<usize>>,
|
||||
value: &'a str,
|
||||
dummy: Option<&'a CredentialRecord>,
|
||||
) {
|
||||
self.spans
|
||||
.extend(ranges.into_iter().map(|range| (range, value, dummy)));
|
||||
}
|
||||
|
||||
pub(super) fn masked(&self, text: &str) -> String {
|
||||
let mut masked = text.to_string();
|
||||
for (range, _, _) in &self.spans {
|
||||
masked.replace_range(range.clone(), &"\0".repeat(range.len()));
|
||||
}
|
||||
masked
|
||||
}
|
||||
|
||||
pub(super) fn render(mut self, text: &mut String) -> bool {
|
||||
if self.spans.is_empty() {
|
||||
return false;
|
||||
}
|
||||
self.spans
|
||||
.sort_by_key(|(range, _, _)| std::cmp::Reverse(range.len()));
|
||||
let mut selected = Vec::<(Range<usize>, &str, Option<&CredentialRecord>)>::new();
|
||||
for span in self.spans {
|
||||
if !selected
|
||||
.iter()
|
||||
.any(|(range, _, _)| range.start < span.0.end && span.0.start < range.end)
|
||||
{
|
||||
selected.push(span);
|
||||
}
|
||||
}
|
||||
selected.sort_by_key(|(range, _, _)| range.start);
|
||||
let mut output = String::with_capacity(text.len());
|
||||
let mut previous = 0;
|
||||
let mut dummies = Vec::new();
|
||||
for (range, value, dummy) in selected {
|
||||
output.push_str(&text[previous..range.start]);
|
||||
let start = output.len();
|
||||
output.push_str(value);
|
||||
if let Some(dummy) = dummy {
|
||||
dummies.push((dummy, start..output.len()));
|
||||
}
|
||||
previous = range.end;
|
||||
}
|
||||
output.push_str(&text[previous..]);
|
||||
for (dummy, range) in dummies {
|
||||
if !dummy
|
||||
.value_match_ranges(&output, &dummy.dummy_value)
|
||||
.contains(&range)
|
||||
{
|
||||
let alias = GeneratedAlias {
|
||||
digest: Sha256::digest(&output).into(),
|
||||
len: output.len(),
|
||||
range,
|
||||
};
|
||||
let mut aliases = dummy
|
||||
.generated_aliases
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
if !aliases.contains(&alias) {
|
||||
aliases.push(alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
*text = output;
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -254,7 +254,7 @@ fn virtualize_child_env_replaces_supported_credentials() {
|
||||
assert!(!broker.virtualize_text(&mut embedded_credential, &env));
|
||||
assert_eq!(embedded_credential, format!("Bearer {prefix}"));
|
||||
}
|
||||
for component in ["cafe\u{e9}task", "cafe\u{301}task"] {
|
||||
for component in ["\u{e9}task", "e\u{301}task"] {
|
||||
let mut unicode_adjacent_path = format!("/workspace/{component}-proj-{hash}/bin");
|
||||
assert!(!broker.virtualize_text(&mut unicode_adjacent_path, &env));
|
||||
assert_eq!(
|
||||
@@ -300,8 +300,8 @@ fn virtualize_child_env_replaces_supported_credentials() {
|
||||
"harddisk",
|
||||
"MY_Task",
|
||||
"Flask",
|
||||
"cafe\u{e9}_task",
|
||||
"cafe\u{301}_task",
|
||||
"\u{e9}_task",
|
||||
"e\u{301}_task",
|
||||
] {
|
||||
for suffix in ["", "proj-", "admin-", "svcacct-"] {
|
||||
for path in [
|
||||
@@ -662,6 +662,29 @@ fn virtualize_child_env_discovers_credentials_without_canonical_variables() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtualize_child_env_preserves_operational_paths_during_credential_discovery() {
|
||||
let broker = CredentialBroker::new(/*enabled*/ true);
|
||||
let hash = "a".repeat(64);
|
||||
let virtual_env = format!("/workspace/multitask-proj-{hash}/bin");
|
||||
let github_host = format!("multitask-proj-{hash}.enterprise.example");
|
||||
let token = "sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
let authorization_header = format!("Bearer {token}");
|
||||
let mut env = env_map([
|
||||
("VIRTUAL_ENV", virtual_env.as_str()),
|
||||
("GH_HOST", github_host.as_str()),
|
||||
("NO_PROXY", github_host.as_str()),
|
||||
("AUTH_HEADER", authorization_header.as_str()),
|
||||
]);
|
||||
|
||||
broker.virtualize_child_env(&mut env);
|
||||
|
||||
assert_eq!(env["VIRTUAL_ENV"], virtual_env);
|
||||
assert_eq!(env["GH_HOST"], github_host);
|
||||
assert_eq!(env["NO_PROXY"], github_host);
|
||||
assert_ne!(env["AUTH_HEADER"], authorization_header);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn virtualize_child_env_keeps_adjacent_provider_credentials_separate() {
|
||||
let broker = CredentialBroker::new(/*enabled*/ true);
|
||||
|
||||
@@ -45,6 +45,7 @@ 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::CredentialAuthMethod;
|
||||
pub use credential_broker::CredentialBrokerEnvironment;
|
||||
pub use credential_broker::CredentialProviderConfig;
|
||||
pub use credential_broker::brokered_credential_binding_env_keys;
|
||||
pub use credential_broker::brokered_credential_dummy_env_keys;
|
||||
|
||||
@@ -656,7 +656,7 @@ pub fn is_managed_proxy_env_var(key: &str, value: &str) -> bool {
|
||||
|
||||
pub fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
|
||||
let brokered_credential_dummy_env_keys =
|
||||
crate::credential_broker::brokered_credential_dummy_env_keys(env);
|
||||
crate::credential_broker::marked_credential_dummy_env_keys(env);
|
||||
env.retain(|key, value| {
|
||||
!brokered_credential_dummy_env_keys.contains(key) && !is_managed_proxy_env_var(key, value)
|
||||
});
|
||||
@@ -1194,6 +1194,26 @@ impl NetworkProxy {
|
||||
self.state.virtualize_brokered_text(text, env)
|
||||
}
|
||||
|
||||
/// Returns trusted provider metadata and active bindings for a child environment.
|
||||
pub fn credential_broker_environment(
|
||||
&self,
|
||||
env: &HashMap<String, String>,
|
||||
) -> crate::CredentialBrokerEnvironment {
|
||||
self.state.credential_broker_environment(env)
|
||||
}
|
||||
|
||||
/// Checks whether recognized credentials originate from permitted provider variables.
|
||||
pub fn credential_broker_sources_allowed(
|
||||
&self,
|
||||
value: &str,
|
||||
virtualized: &str,
|
||||
source_env: &HashMap<String, String>,
|
||||
is_allowed: impl Fn(&str) -> bool,
|
||||
) -> bool {
|
||||
self.state
|
||||
.credential_broker_sources_allowed(value, virtualized, source_env, is_allowed)
|
||||
}
|
||||
|
||||
/// Restores known dummy credentials in trusted text captured for fail-open execution.
|
||||
pub fn restore_brokered_text(&self, text: &mut String) -> bool {
|
||||
self.state.restore_brokered_text(text)
|
||||
|
||||
@@ -515,6 +515,24 @@ impl NetworkProxyState {
|
||||
self.credential_broker.virtualize_text(text, env)
|
||||
}
|
||||
|
||||
pub(crate) fn credential_broker_environment(
|
||||
&self,
|
||||
env: &HashMap<String, String>,
|
||||
) -> crate::CredentialBrokerEnvironment {
|
||||
self.credential_broker.environment(env)
|
||||
}
|
||||
|
||||
pub(crate) fn credential_broker_sources_allowed(
|
||||
&self,
|
||||
value: &str,
|
||||
virtualized: &str,
|
||||
source_env: &HashMap<String, String>,
|
||||
is_allowed: impl Fn(&str) -> bool,
|
||||
) -> bool {
|
||||
self.credential_broker
|
||||
.provider_sources_allowed(value, virtualized, source_env, is_allowed)
|
||||
}
|
||||
|
||||
pub(crate) fn restore_brokered_text(&self, text: &mut String) -> bool {
|
||||
self.credential_broker.restore_text(text)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user