mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
Add configurable credential providers to the network proxy (#44056)
## What changed - Add `credential_providers` configuration for environment variables, credential patterns, static or environment-derived URL prefixes, and bearer, token, Basic, or custom header authentication. - Generate matching dummy credentials and restrict replacement to authorized schemes, hosts, ports, and path prefixes. Scope credentials and destination history to each environment. - Preserve real credentials for destinations that bypass the proxy with `allow_local_binding`, while providing snapshot redaction and alias matching. - Validate provider definitions and preserve unchanged providers across configuration reloads. ## Testing Add coverage for dummy generation, authentication translation, URL restrictions, environment isolation, configuration reloads, and credential restoration for local proxy bypass. GitOrigin-RevId: 5d60ecc684cf4e9aa7637af33af12179aa0c672a
This commit is contained in:
@@ -300,7 +300,7 @@ async fn http_connect_accept(
|
||||
return Err(text_response(StatusCode::INTERNAL_SERVER_ERROR, "error"));
|
||||
}
|
||||
};
|
||||
let host_mitm_requirement = match app_state.host_mitm_requirement(&host).await {
|
||||
let host_mitm_requirement = match app_state.host_mitm_requirement(&host, authority.port).await {
|
||||
Ok(requirement) => requirement,
|
||||
Err(err) => {
|
||||
error!("failed to inspect MITM requirements for {host}: {err}");
|
||||
@@ -785,7 +785,7 @@ async fn http_plain_proxy(
|
||||
}
|
||||
}
|
||||
|
||||
let host_mitm_requirement = match app_state.host_mitm_requirement(&host).await {
|
||||
let host_mitm_requirement = match app_state.host_mitm_requirement(&host, port).await {
|
||||
Ok(requirement) => requirement,
|
||||
Err(err) => {
|
||||
return Ok(internal_error("failed to inspect MITM requirements", err));
|
||||
@@ -880,7 +880,7 @@ async fn http_plain_proxy(
|
||||
}
|
||||
|
||||
if let Err(err) =
|
||||
inject_plaintext_credentials_if_enabled(app_state.as_ref(), &host, req.headers_mut()).await
|
||||
inject_forward_request_credentials(app_state.as_ref(), &request_ctx, &mut req).await
|
||||
{
|
||||
return Ok(internal_error(
|
||||
"failed to read plaintext credential injection config",
|
||||
@@ -917,13 +917,28 @@ async fn http_plain_proxy(
|
||||
}
|
||||
}
|
||||
|
||||
async fn inject_plaintext_credentials_if_enabled(
|
||||
async fn inject_forward_request_credentials(
|
||||
app_state: &NetworkProxyState,
|
||||
host: &str,
|
||||
headers: &mut HeaderMap,
|
||||
context: &RequestContext,
|
||||
req: &mut Request,
|
||||
) -> Result<()> {
|
||||
if app_state.plaintext_credential_injection_enabled().await? {
|
||||
app_state.inject_request_credentials(host, headers);
|
||||
let authority = context.host_with_port();
|
||||
let scheme = req.uri().scheme_str().unwrap_or("http");
|
||||
// Server-wide OPTIONS may use root-scoped credentials without changing its wire target.
|
||||
let request_path = req
|
||||
.uri()
|
||||
.path_and_query()
|
||||
.map(rama_http::uri::PathAndQuery::as_str)
|
||||
.filter(|path| *path != "*")
|
||||
.unwrap_or("/");
|
||||
let destination = format!("{scheme}://{authority}{request_path}");
|
||||
let unrestricted_plaintext = app_state.plaintext_credential_injection_enabled().await?;
|
||||
app_state.inject_request_credentials(&destination, req.headers_mut());
|
||||
if unrestricted_plaintext {
|
||||
app_state.inject_request_credentials(
|
||||
&normalize_host(&authority.host.to_string()),
|
||||
req.headers_mut(),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1167,12 +1182,14 @@ struct BlockedResponse<'a> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::CredentialProviderConfig;
|
||||
use crate::config::NetworkMode;
|
||||
use crate::config::NetworkProxyConfig;
|
||||
use crate::runtime::network_proxy_state_for_policy;
|
||||
use pretty_assertions::assert_eq;
|
||||
use rama_http::Method;
|
||||
use rama_http::Request;
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::net::TcpListener as StdTcpListener;
|
||||
@@ -1314,62 +1331,117 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn plaintext_credential_injection_requires_explicit_opt_in() {
|
||||
let real_token = "ghp-real";
|
||||
let mut disabled_network = NetworkProxyConfig {
|
||||
credential_broker: true,
|
||||
mitm: true,
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
disabled_network.set_allowed_domains(vec!["api.github.com".to_string()]);
|
||||
let disabled_state = Arc::new(network_proxy_state_for_policy(disabled_network));
|
||||
let mut disabled_env = HashMap::from([("GH_TOKEN".to_string(), real_token.to_string())]);
|
||||
disabled_state.virtualize_child_credentials(&mut disabled_env);
|
||||
let dummy_token = disabled_env.get("GH_TOKEN").expect("dummy GitHub token");
|
||||
let mut disabled_headers = HeaderMap::from_iter([(
|
||||
header::AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {dummy_token}"))
|
||||
.expect("valid authorization header"),
|
||||
)]);
|
||||
for enabled in [false, true] {
|
||||
let state = network_proxy_state_for_policy(NetworkProxyConfig {
|
||||
credential_broker: true,
|
||||
dangerously_allow_plaintext_credential_injection: enabled,
|
||||
mitm: true,
|
||||
..NetworkProxyConfig::default()
|
||||
});
|
||||
let mut env = HashMap::from([("GH_TOKEN".to_string(), real_token.to_string())]);
|
||||
state.virtualize_child_credentials(&mut env);
|
||||
let dummy = env.get("GH_TOKEN").expect("dummy GitHub token");
|
||||
let mut req = Request::builder()
|
||||
.uri("http://api.github.com/")
|
||||
.header(header::AUTHORIZATION, format!("Bearer {dummy}"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let context = RequestContext::try_from(&req).unwrap();
|
||||
|
||||
inject_plaintext_credentials_if_enabled(
|
||||
disabled_state.as_ref(),
|
||||
"api.github.com",
|
||||
&mut disabled_headers,
|
||||
)
|
||||
.await
|
||||
.expect("disabled plaintext injection check should succeed");
|
||||
assert_eq!(
|
||||
disabled_headers.get(header::AUTHORIZATION),
|
||||
Some(&HeaderValue::from_str(&format!("Bearer {dummy_token}")).unwrap())
|
||||
);
|
||||
inject_forward_request_credentials(&state, &context, &mut req)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut enabled_network = NetworkProxyConfig {
|
||||
credential_broker: true,
|
||||
dangerously_allow_plaintext_credential_injection: true,
|
||||
mitm: true,
|
||||
..NetworkProxyConfig::default()
|
||||
};
|
||||
enabled_network.set_allowed_domains(vec!["api.github.com".to_string()]);
|
||||
let enabled_state = Arc::new(network_proxy_state_for_policy(enabled_network));
|
||||
let mut enabled_env = HashMap::from([("GH_TOKEN".to_string(), real_token.to_string())]);
|
||||
enabled_state.virtualize_child_credentials(&mut enabled_env);
|
||||
let enabled_dummy = enabled_env.get("GH_TOKEN").expect("dummy GitHub token");
|
||||
let mut enabled_headers = HeaderMap::from_iter([(
|
||||
header::AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {enabled_dummy}"))
|
||||
.expect("valid authorization header"),
|
||||
)]);
|
||||
let expected = if enabled { real_token } else { dummy };
|
||||
assert_eq!(
|
||||
req.headers()[header::AUTHORIZATION],
|
||||
format!("Bearer {expected}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
inject_plaintext_credentials_if_enabled(
|
||||
enabled_state.as_ref(),
|
||||
"api.github.com",
|
||||
&mut enabled_headers,
|
||||
)
|
||||
.await
|
||||
.expect("enabled plaintext injection check should succeed");
|
||||
assert_eq!(
|
||||
enabled_headers.get(header::AUTHORIZATION),
|
||||
Some(&HeaderValue::from_str(&format!("Bearer {real_token}")).unwrap())
|
||||
);
|
||||
#[tokio::test]
|
||||
async fn configured_forward_requests_match_destination_scheme_and_port() {
|
||||
let real_token = "provider_abcdefghijklmnopqrstuvwx";
|
||||
for (host, port, prefix, scope) in [
|
||||
("localhost", 8080, ""),
|
||||
("127.0.0.1", 8081, ""),
|
||||
("api.provider.example", 443, "https://"),
|
||||
("api.provider.example", 8443, "https://"),
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(|(host, port, prefix)| ["/v1", "/"].map(move |scope| (host, port, prefix, scope)))
|
||||
{
|
||||
let state = network_proxy_state_for_policy(NetworkProxyConfig {
|
||||
credential_broker: true,
|
||||
dangerously_allow_plaintext_credential_injection: true,
|
||||
credential_providers: BTreeMap::from([(
|
||||
"custom".to_string(),
|
||||
CredentialProviderConfig {
|
||||
env: vec!["PROVIDER_TOKEN".to_string()],
|
||||
patterns: vec!["^provider_[a-z]{24}$".to_string()],
|
||||
url_prefix_from_env: Some("PROVIDER_URL".to_string()),
|
||||
..CredentialProviderConfig::default()
|
||||
},
|
||||
)]),
|
||||
..NetworkProxyConfig::default()
|
||||
});
|
||||
let mut env = HashMap::from([
|
||||
("PROVIDER_TOKEN".to_string(), real_token.to_string()),
|
||||
(
|
||||
"PROVIDER_URL".to_string(),
|
||||
format!("{prefix}{host}:{port}{scope}"),
|
||||
),
|
||||
]);
|
||||
state.virtualize_child_credentials(&mut env);
|
||||
let dummy = env.get("PROVIDER_TOKEN").expect("dummy provider token");
|
||||
assert_ne!(dummy, real_token);
|
||||
for (uri, inject) in [
|
||||
(format!("http://{host}:{port}/v1/models"), prefix.is_empty()),
|
||||
(format!("http://{host}:{}/v1/models", port + 1), false),
|
||||
(format!("https://{host}:{}/v1/models", port + 1), false),
|
||||
(
|
||||
format!("https://{host}:{port}/v1/models"),
|
||||
!prefix.is_empty(),
|
||||
),
|
||||
("/v1/models".to_string(), prefix.is_empty()),
|
||||
(
|
||||
format!("https://{host}:{port}/other"),
|
||||
!prefix.is_empty() && scope == "/",
|
||||
),
|
||||
("*".to_string(), prefix.is_empty() && scope == "/"),
|
||||
] {
|
||||
let request_uri: rama_http::Uri = uri.parse().unwrap();
|
||||
let host_header = request_uri
|
||||
.authority()
|
||||
.map(|authority| authority.as_str().to_string())
|
||||
.unwrap_or_else(|| format!("{host}:{port}"));
|
||||
let mut req = Request::builder()
|
||||
.method(if uri == "*" {
|
||||
Method::OPTIONS
|
||||
} else {
|
||||
Method::GET
|
||||
})
|
||||
.uri(request_uri)
|
||||
.header(header::HOST, host_header)
|
||||
.header(header::AUTHORIZATION, format!("Bearer {dummy}"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let context = RequestContext::try_from(&req).unwrap();
|
||||
|
||||
inject_forward_request_credentials(&state, &context, &mut req)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let expected = if inject { real_token } else { dummy };
|
||||
assert_eq!(
|
||||
req.headers()[header::AUTHORIZATION],
|
||||
format!("Bearer {expected}"),
|
||||
"request: {uri}, configured scope: {scope}"
|
||||
);
|
||||
assert_eq!(req.uri().to_string(), uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user