mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
core: attribute network approvals to owning tool calls
Thread a stable network owner id through managed proxy credentials and use it to resolve the exact active tool call for blocked network requests. This makes concurrent network approvals attribute deterministically and preserves parent tool item ids downstream. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
db8bb7236d
commit
b84b970653
@@ -1693,6 +1693,7 @@ impl CodexMessageProcessor {
|
||||
network: started_network_proxy
|
||||
.as_ref()
|
||||
.map(codex_core::config::StartedNetworkProxy::proxy),
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: self
|
||||
|
||||
@@ -732,6 +732,7 @@ mod tests {
|
||||
cwd: PathBuf::from("."),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
expiration: ExecExpiration::DefaultTimeout,
|
||||
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::WindowsRestrictedToken,
|
||||
@@ -845,6 +846,7 @@ mod tests {
|
||||
cwd: PathBuf::from("."),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
expiration: ExecExpiration::Cancellation(CancellationToken::new()),
|
||||
capture_policy: codex_core::exec::ExecCapturePolicy::ShellTool,
|
||||
sandbox: SandboxType::None,
|
||||
|
||||
@@ -5033,6 +5033,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
@@ -5051,6 +5052,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
.config
|
||||
|
||||
@@ -128,6 +128,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
|
||||
@@ -81,6 +81,7 @@ pub struct ExecParams {
|
||||
pub capture_policy: ExecCapturePolicy,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
pub network_owner_id: Option<String>,
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel,
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
@@ -263,6 +264,7 @@ pub fn build_exec_request(
|
||||
expiration,
|
||||
capture_policy,
|
||||
network,
|
||||
network_owner_id,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
@@ -270,7 +272,7 @@ pub fn build_exec_request(
|
||||
arg0: _,
|
||||
} = params;
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref());
|
||||
}
|
||||
let (program, args) = command.split_first().ok_or_else(|| {
|
||||
CodexErr::Io(io::Error::new(
|
||||
@@ -301,6 +303,7 @@ pub fn build_exec_request(
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network,
|
||||
network: network.as_ref(),
|
||||
network_owner_id: network_owner_id.as_deref(),
|
||||
sandbox_policy_cwd: sandbox_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -324,6 +327,7 @@ pub(crate) async fn execute_exec_request(
|
||||
cwd,
|
||||
env,
|
||||
network,
|
||||
network_owner_id,
|
||||
expiration,
|
||||
capture_policy,
|
||||
sandbox,
|
||||
@@ -345,6 +349,7 @@ pub(crate) async fn execute_exec_request(
|
||||
capture_policy,
|
||||
env,
|
||||
network: network.clone(),
|
||||
network_owner_id,
|
||||
sandbox_permissions,
|
||||
windows_sandbox_level,
|
||||
windows_sandbox_private_desktop,
|
||||
@@ -448,6 +453,7 @@ async fn exec_windows_sandbox(
|
||||
cwd,
|
||||
mut env,
|
||||
network,
|
||||
network_owner_id,
|
||||
expiration,
|
||||
capture_policy,
|
||||
windows_sandbox_level,
|
||||
@@ -455,7 +461,7 @@ async fn exec_windows_sandbox(
|
||||
..
|
||||
} = params;
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref());
|
||||
}
|
||||
|
||||
// TODO(iceweasel-oai): run_windows_sandbox_capture should support all
|
||||
@@ -838,6 +844,7 @@ async fn exec(
|
||||
cwd,
|
||||
mut env,
|
||||
network,
|
||||
network_owner_id,
|
||||
arg0,
|
||||
expiration,
|
||||
capture_policy,
|
||||
@@ -845,7 +852,7 @@ async fn exec(
|
||||
..
|
||||
} = params;
|
||||
if let Some(network) = network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
network.apply_to_env_for_owner(&mut env, network_owner_id.as_deref());
|
||||
}
|
||||
|
||||
let (program, args) = command.split_first().ok_or_else(|| {
|
||||
|
||||
@@ -259,6 +259,7 @@ async fn exec_full_buffer_capture_ignores_expiration() -> Result<()> {
|
||||
capture_policy: ExecCapturePolicy::FullBuffer,
|
||||
env,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -298,6 +299,7 @@ async fn exec_full_buffer_capture_keeps_io_drain_timeout_when_descendant_holds_p
|
||||
capture_policy: ExecCapturePolicy::FullBuffer,
|
||||
env: std::env::vars().collect(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -348,6 +350,7 @@ async fn process_exec_tool_call_preserves_full_buffer_capture_policy() -> Result
|
||||
capture_policy: ExecCapturePolicy::FullBuffer,
|
||||
env: std::env::vars().collect(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -588,6 +591,7 @@ async fn kill_child_process_group_kills_grandchildren_on_timeout() -> Result<()>
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -646,6 +650,7 @@ async fn process_exec_tool_call_respects_cancellation_token() -> Result<()> {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -156,6 +156,7 @@ fn denied_network_policy_message_requires_deny_decision() {
|
||||
let blocked = BlockedRequest {
|
||||
host: "example.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
network_owner_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
@@ -173,6 +174,7 @@ fn denied_network_policy_message_for_denylist_block_is_explicit() {
|
||||
let blocked = BlockedRequest {
|
||||
host: "example.com".to_string(),
|
||||
reason: "denied".to_string(),
|
||||
network_owner_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
|
||||
@@ -68,6 +68,7 @@ pub struct ExecRequest {
|
||||
pub cwd: PathBuf,
|
||||
pub env: HashMap<String, String>,
|
||||
pub network: Option<NetworkProxy>,
|
||||
pub network_owner_id: Option<String>,
|
||||
pub expiration: ExecExpiration,
|
||||
pub capture_policy: ExecCapturePolicy,
|
||||
pub sandbox: SandboxType,
|
||||
@@ -94,6 +95,7 @@ pub(crate) struct SandboxTransformRequest<'a> {
|
||||
// TODO(viyatb): Evaluate switching this to Option<Arc<NetworkProxy>>
|
||||
// to make shared ownership explicit across runtime/sandbox plumbing.
|
||||
pub network: Option<&'a NetworkProxy>,
|
||||
pub network_owner_id: Option<&'a str>,
|
||||
pub sandbox_policy_cwd: &'a Path,
|
||||
#[cfg(target_os = "macos")]
|
||||
pub macos_seatbelt_profile_extensions: Option<&'a MacOsSeatbeltProfileExtensions>,
|
||||
@@ -592,6 +594,7 @@ impl SandboxManager {
|
||||
sandbox,
|
||||
enforce_managed_network,
|
||||
network,
|
||||
network_owner_id,
|
||||
sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
@@ -709,6 +712,7 @@ impl SandboxManager {
|
||||
cwd: spec.cwd,
|
||||
env,
|
||||
network: network.cloned(),
|
||||
network_owner_id: network_owner_id.map(ToString::to_string),
|
||||
expiration: spec.expiration,
|
||||
capture_policy: spec.capture_policy,
|
||||
sandbox,
|
||||
|
||||
@@ -171,6 +171,7 @@ fn transform_preserves_unrestricted_file_system_policy_for_restricted_network()
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -541,6 +542,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
@@ -615,6 +617,7 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
sandbox: SandboxType::None,
|
||||
enforce_managed_network: false,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_policy_cwd: cwd.as_path(),
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
||||
@@ -163,6 +163,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
Some(session.conversation_id),
|
||||
),
|
||||
network: turn_context.network.clone(),
|
||||
network_owner_id: None,
|
||||
// TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we
|
||||
// should use that instead of an "arbitrarily large" timeout here.
|
||||
expiration: USER_SHELL_TIMEOUT_MS.into(),
|
||||
|
||||
@@ -215,6 +215,7 @@ impl ToolHandler for ApplyPatchHandler {
|
||||
session: session.clone(),
|
||||
turn: turn.clone(),
|
||||
call_id: call_id.clone(),
|
||||
network_approval_owner_id: None,
|
||||
tool_name: tool_name.to_string(),
|
||||
};
|
||||
let out = orchestrator
|
||||
@@ -319,6 +320,7 @@ pub(crate) async fn intercept_apply_patch(
|
||||
session: session.clone(),
|
||||
turn: turn.clone(),
|
||||
call_id: call_id.to_string(),
|
||||
network_approval_owner_id: None,
|
||||
tool_name: tool_name.to_string(),
|
||||
};
|
||||
let out = orchestrator
|
||||
|
||||
@@ -74,6 +74,7 @@ impl ShellHandler {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env(&turn_context.shell_environment_policy, Some(thread_id)),
|
||||
network: turn_context.network.clone(),
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
@@ -129,6 +130,7 @@ impl ShellCommandHandler {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env(&turn_context.shell_environment_policy, Some(thread_id)),
|
||||
network: turn_context.network.clone(),
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
windows_sandbox_private_desktop: turn_context
|
||||
@@ -474,6 +476,7 @@ impl ShellHandler {
|
||||
session: session.clone(),
|
||||
turn: turn.clone(),
|
||||
call_id: call_id.clone(),
|
||||
network_approval_owner_id: None,
|
||||
tool_name,
|
||||
};
|
||||
let out = orchestrator
|
||||
|
||||
@@ -1067,6 +1067,7 @@ impl JsReplManager {
|
||||
sandbox: sandbox_type,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_policy_cwd: &turn.cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
||||
@@ -64,6 +64,10 @@ impl ActiveNetworkApproval {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub(crate) fn owner_id(&self) -> Option<&str> {
|
||||
self.registration_id.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn into_deferred(self) -> Option<DeferredNetworkApproval> {
|
||||
match (self.mode, self.registration_id) {
|
||||
(NetworkApprovalMode::Deferred, Some(registration_id)) => {
|
||||
@@ -213,6 +217,18 @@ impl NetworkApprovalService {
|
||||
call_outcomes.remove(registration_id);
|
||||
}
|
||||
|
||||
async fn resolve_owner_call(
|
||||
&self,
|
||||
network_owner_id: Option<&str>,
|
||||
) -> Option<Arc<ActiveNetworkApprovalCall>> {
|
||||
if let Some(network_owner_id) = network_owner_id {
|
||||
let active_calls = self.active_calls.lock().await;
|
||||
return active_calls.get(network_owner_id).cloned();
|
||||
}
|
||||
|
||||
self.resolve_single_active_call().await
|
||||
}
|
||||
|
||||
async fn resolve_single_active_call(&self) -> Option<Arc<ActiveNetworkApprovalCall>> {
|
||||
let active_calls = self.active_calls.lock().await;
|
||||
if active_calls.len() == 1 {
|
||||
@@ -236,8 +252,12 @@ impl NetworkApprovalService {
|
||||
(created, true)
|
||||
}
|
||||
|
||||
async fn record_outcome_for_single_active_call(&self, outcome: NetworkApprovalOutcome) {
|
||||
let Some(owner_call) = self.resolve_single_active_call().await else {
|
||||
async fn record_outcome_for_owner_call(
|
||||
&self,
|
||||
network_owner_id: Option<&str>,
|
||||
outcome: NetworkApprovalOutcome,
|
||||
) {
|
||||
let Some(owner_call) = self.resolve_owner_call(network_owner_id).await else {
|
||||
return;
|
||||
};
|
||||
self.record_call_outcome(&owner_call.registration_id, outcome)
|
||||
@@ -265,8 +285,11 @@ impl NetworkApprovalService {
|
||||
return;
|
||||
};
|
||||
|
||||
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(message))
|
||||
.await;
|
||||
self.record_outcome_for_owner_call(
|
||||
blocked.network_owner_id.as_deref(),
|
||||
NetworkApprovalOutcome::DeniedByPolicy(message),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn active_turn_context(session: &Session) -> Option<Arc<crate::codex::TurnContext>> {
|
||||
@@ -328,9 +351,10 @@ impl NetworkApprovalService {
|
||||
pending.set_decision(PendingApprovalDecision::Deny).await;
|
||||
let mut pending_approvals = self.pending_host_approvals.lock().await;
|
||||
pending_approvals.remove(&key);
|
||||
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(
|
||||
policy_denial_message,
|
||||
))
|
||||
self.record_outcome_for_owner_call(
|
||||
request.network_owner_id.as_deref(),
|
||||
NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message),
|
||||
)
|
||||
.await;
|
||||
return NetworkDecision::deny(REASON_NOT_ALLOWED);
|
||||
};
|
||||
@@ -338,9 +362,10 @@ impl NetworkApprovalService {
|
||||
pending.set_decision(PendingApprovalDecision::Deny).await;
|
||||
let mut pending_approvals = self.pending_host_approvals.lock().await;
|
||||
pending_approvals.remove(&key);
|
||||
self.record_outcome_for_single_active_call(NetworkApprovalOutcome::DeniedByPolicy(
|
||||
policy_denial_message,
|
||||
))
|
||||
self.record_outcome_for_owner_call(
|
||||
request.network_owner_id.as_deref(),
|
||||
NetworkApprovalOutcome::DeniedByPolicy(policy_denial_message),
|
||||
)
|
||||
.await;
|
||||
return NetworkDecision::deny(REASON_NOT_ALLOWED);
|
||||
}
|
||||
@@ -349,7 +374,9 @@ impl NetworkApprovalService {
|
||||
host: request.host.clone(),
|
||||
protocol,
|
||||
};
|
||||
let owner_call = self.resolve_single_active_call().await;
|
||||
let owner_call = self
|
||||
.resolve_owner_call(request.network_owner_id.as_deref())
|
||||
.await;
|
||||
let approval_decision = if routes_approval_to_guardian(&turn_context) {
|
||||
// TODO(ccunningham): Attach guardian network reviews to the reviewed tool item
|
||||
// lifecycle instead of this temporary standalone network approval id.
|
||||
|
||||
@@ -180,9 +180,14 @@ fn only_never_policy_disables_network_approval_flow() {
|
||||
}
|
||||
|
||||
fn denied_blocked_request(host: &str) -> BlockedRequest {
|
||||
denied_blocked_request_with_owner(host, None)
|
||||
}
|
||||
|
||||
fn denied_blocked_request_with_owner(host: &str, network_owner_id: Option<&str>) -> BlockedRequest {
|
||||
BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
network_owner_id: network_owner_id.map(ToString::to_string),
|
||||
client: None,
|
||||
method: None,
|
||||
mode: None,
|
||||
@@ -249,3 +254,65 @@ async fn record_blocked_request_ignores_ambiguous_unattributed_blocked_requests(
|
||||
assert_eq!(service.take_call_outcome("registration-1").await, None);
|
||||
assert_eq!(service.take_call_outcome("registration-2").await, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_owner_call_uses_network_owner_id_when_multiple_calls_are_active() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call(
|
||||
"registration-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
"command-1".to_string(),
|
||||
)
|
||||
.await;
|
||||
service
|
||||
.register_call(
|
||||
"registration-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
"command-2".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let owner_call = service
|
||||
.resolve_owner_call(Some("registration-2"))
|
||||
.await
|
||||
.expect("owner call should resolve");
|
||||
|
||||
assert_eq!(owner_call.registration_id, "registration-2");
|
||||
assert_eq!(owner_call.turn_id, "turn-2");
|
||||
assert_eq!(owner_call.parent_tool_item_id, "command-2");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn record_blocked_request_uses_network_owner_id_when_multiple_calls_are_active() {
|
||||
let service = NetworkApprovalService::default();
|
||||
service
|
||||
.register_call(
|
||||
"registration-1".to_string(),
|
||||
"turn-1".to_string(),
|
||||
"command-1".to_string(),
|
||||
)
|
||||
.await;
|
||||
service
|
||||
.register_call(
|
||||
"registration-2".to_string(),
|
||||
"turn-2".to_string(),
|
||||
"command-2".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
service
|
||||
.record_blocked_request(denied_blocked_request_with_owner(
|
||||
"example.com",
|
||||
Some("registration-2"),
|
||||
))
|
||||
.await;
|
||||
|
||||
assert_eq!(service.take_call_outcome("registration-1").await, None);
|
||||
assert_eq!(
|
||||
service.take_call_outcome("registration-2").await,
|
||||
Some(NetworkApprovalOutcome::DeniedByPolicy(
|
||||
"Network access to \"example.com\" was blocked: domain is not on the allowlist for the current sandbox mode.".to_string()
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::guardian::GUARDIAN_REJECTION_MESSAGE;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::network_policy_decision::network_approval_context_from_payload;
|
||||
use crate::sandboxing::SandboxManager;
|
||||
use crate::tools::network_approval::ActiveNetworkApproval;
|
||||
use crate::tools::network_approval::DeferredNetworkApproval;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
use crate::tools::network_approval::begin_network_approval;
|
||||
@@ -64,11 +65,16 @@ impl ToolOrchestrator {
|
||||
tool.network_approval_spec(req, tool_ctx),
|
||||
)
|
||||
.await;
|
||||
let network_approval_owner_id = network_approval
|
||||
.as_ref()
|
||||
.and_then(ActiveNetworkApproval::owner_id)
|
||||
.map(ToString::to_string);
|
||||
|
||||
let attempt_tool_ctx = ToolCtx {
|
||||
session: tool_ctx.session.clone(),
|
||||
turn: tool_ctx.turn.clone(),
|
||||
call_id: tool_ctx.call_id.clone(),
|
||||
network_approval_owner_id,
|
||||
tool_name: tool_ctx.tool_name.clone(),
|
||||
};
|
||||
let run_result = tool.run(req, attempt, &attempt_tool_ctx).await;
|
||||
|
||||
@@ -208,7 +208,7 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
|
||||
) -> Result<ExecToolCallOutput, ToolError> {
|
||||
let spec = Self::build_command_spec(req, &ctx.turn.config.codex_home)?;
|
||||
let env = attempt
|
||||
.env_for(spec, /*network*/ None)
|
||||
.env_for(spec, /*network*/ None, /*network_owner_id*/ None)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let out = execute_env(env, Self::stdout_stream(ctx))
|
||||
.await
|
||||
|
||||
@@ -253,7 +253,11 @@ impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||||
req.justification.clone(),
|
||||
)?;
|
||||
let env = attempt
|
||||
.env_for(spec, req.network.as_ref())
|
||||
.env_for(
|
||||
spec,
|
||||
req.network.as_ref(),
|
||||
ctx.network_approval_owner_id.as_deref(),
|
||||
)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let out = execute_env(env, Self::stdout_stream(ctx))
|
||||
.await
|
||||
|
||||
@@ -117,13 +117,18 @@ pub(super) async fn try_run_zsh_fork(
|
||||
req.justification.clone(),
|
||||
)?;
|
||||
let sandbox_exec_request = attempt
|
||||
.env_for(spec, req.network.as_ref())
|
||||
.env_for(
|
||||
spec,
|
||||
req.network.as_ref(),
|
||||
ctx.network_approval_owner_id.as_deref(),
|
||||
)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
let crate::sandboxing::ExecRequest {
|
||||
command,
|
||||
cwd: sandbox_cwd,
|
||||
env: sandbox_env,
|
||||
network: sandbox_network,
|
||||
network_owner_id,
|
||||
expiration: _sandbox_expiration,
|
||||
capture_policy: _capture_policy,
|
||||
sandbox,
|
||||
@@ -153,6 +158,7 @@ pub(super) async fn try_run_zsh_fork(
|
||||
sandbox,
|
||||
env: sandbox_env,
|
||||
network: sandbox_network,
|
||||
network_owner_id,
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions,
|
||||
justification,
|
||||
@@ -259,6 +265,7 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
sandbox: exec_request.sandbox,
|
||||
env: exec_request.env.clone(),
|
||||
network: exec_request.network.clone(),
|
||||
network_owner_id: exec_request.network_owner_id.clone(),
|
||||
windows_sandbox_level: exec_request.windows_sandbox_level,
|
||||
sandbox_permissions: exec_request.sandbox_permissions,
|
||||
justification: exec_request.justification.clone(),
|
||||
@@ -856,6 +863,7 @@ struct CoreShellCommandExecutor {
|
||||
sandbox: SandboxType,
|
||||
env: HashMap<String, String>,
|
||||
network: Option<codex_network_proxy::NetworkProxy>,
|
||||
network_owner_id: Option<String>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
justification: Option<String>,
|
||||
@@ -904,6 +912,7 @@ impl ShellCommandExecutor for CoreShellCommandExecutor {
|
||||
cwd: self.cwd.clone(),
|
||||
env: exec_env,
|
||||
network: self.network.clone(),
|
||||
network_owner_id: self.network_owner_id.clone(),
|
||||
expiration: ExecExpiration::Cancellation(cancel_rx),
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
sandbox: self.sandbox,
|
||||
@@ -1060,6 +1069,7 @@ impl CoreShellCommandExecutor {
|
||||
sandbox,
|
||||
enforce_managed_network: self.network.is_some(),
|
||||
network: self.network.as_ref(),
|
||||
network_owner_id: self.network_owner_id.as_deref(),
|
||||
sandbox_policy_cwd: &self.sandbox_policy_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions,
|
||||
@@ -1069,7 +1079,10 @@ impl CoreShellCommandExecutor {
|
||||
windows_sandbox_private_desktop: false,
|
||||
})?;
|
||||
if let Some(network) = exec_request.network.as_ref() {
|
||||
network.apply_to_env(&mut exec_request.env);
|
||||
network.apply_to_env_for_owner(
|
||||
&mut exec_request.env,
|
||||
exec_request.network_owner_id.as_deref(),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(PreparedExec {
|
||||
|
||||
@@ -655,6 +655,7 @@ async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
@@ -707,6 +708,7 @@ async fn prepare_escalated_exec_permissions_preserve_macos_seatbelt_extensions()
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
file_system_sandbox_policy: unrestricted_file_system_sandbox_policy(),
|
||||
@@ -782,6 +784,7 @@ async fn prepare_escalated_exec_permission_profile_unions_turn_and_requested_mac
|
||||
cwd: cwd.to_path_buf(),
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox: SandboxType::None,
|
||||
sandbox_policy: sandbox_policy.clone(),
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
|
||||
@@ -207,7 +207,7 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
|
||||
let mut env = req.env.clone();
|
||||
if let Some(network) = req.network.as_ref() {
|
||||
network.apply_to_env(&mut env);
|
||||
network.apply_to_env_for_owner(&mut env, ctx.network_approval_owner_id.as_deref());
|
||||
}
|
||||
if let UnifiedExecShellMode::ZshFork(zsh_fork_config) = &self.shell_mode {
|
||||
let spec = build_command_spec(
|
||||
@@ -221,7 +221,11 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
)
|
||||
.map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?;
|
||||
let exec_env = attempt
|
||||
.env_for(spec, req.network.as_ref())
|
||||
.env_for(
|
||||
spec,
|
||||
req.network.as_ref(),
|
||||
ctx.network_approval_owner_id.as_deref(),
|
||||
)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
match zsh_fork_backend::maybe_prepare_unified_exec(
|
||||
req,
|
||||
@@ -269,7 +273,11 @@ impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRunt
|
||||
)
|
||||
.map_err(|_| ToolError::Rejected("missing command line for PTY".to_string()))?;
|
||||
let exec_env = attempt
|
||||
.env_for(spec, req.network.as_ref())
|
||||
.env_for(
|
||||
spec,
|
||||
req.network.as_ref(),
|
||||
ctx.network_approval_owner_id.as_deref(),
|
||||
)
|
||||
.map_err(|err| ToolError::Codex(err.into()))?;
|
||||
self.manager
|
||||
.open_session_with_exec_env(&exec_env, req.tty, Box::new(NoopSpawnLifecycle))
|
||||
|
||||
@@ -300,6 +300,7 @@ pub(crate) struct ToolCtx {
|
||||
pub session: Arc<Session>,
|
||||
pub turn: Arc<TurnContext>,
|
||||
pub call_id: String,
|
||||
pub network_approval_owner_id: Option<String>,
|
||||
pub tool_name: String,
|
||||
}
|
||||
|
||||
@@ -341,6 +342,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
&self,
|
||||
spec: CommandSpec,
|
||||
network: Option<&NetworkProxy>,
|
||||
network_owner_id: Option<&str>,
|
||||
) -> Result<crate::sandboxing::ExecRequest, SandboxTransformError> {
|
||||
self.manager
|
||||
.transform(crate::sandboxing::SandboxTransformRequest {
|
||||
@@ -351,6 +353,7 @@ impl<'a> SandboxAttempt<'a> {
|
||||
sandbox: self.sandbox,
|
||||
enforce_managed_network: self.enforce_managed_network,
|
||||
network,
|
||||
network_owner_id,
|
||||
sandbox_policy_cwd: self.sandbox_cwd,
|
||||
#[cfg(target_os = "macos")]
|
||||
macos_seatbelt_profile_extensions: None,
|
||||
|
||||
@@ -627,6 +627,7 @@ impl UnifiedExecProcessManager {
|
||||
session: context.session.clone(),
|
||||
turn: context.turn.clone(),
|
||||
call_id: context.call_id.clone(),
|
||||
network_approval_owner_id: None,
|
||||
tool_name: "exec_command".to_string(),
|
||||
};
|
||||
orchestrator
|
||||
|
||||
@@ -41,6 +41,7 @@ async fn run_test_cmd(tmp: TempDir, cmd: Vec<&str>) -> Result<ExecToolCallOutput
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: HashMap::new(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -120,6 +120,7 @@ async fn run_cmd_result_with_policies(
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env_from_core_vars(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
@@ -380,6 +381,7 @@ async fn assert_network_blocked(cmd: &[&str]) {
|
||||
capture_policy: ExecCapturePolicy::ShellTool,
|
||||
env: create_env_from_core_vars(),
|
||||
network: None,
|
||||
network_owner_id: None,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
windows_sandbox_private_desktop: false,
|
||||
|
||||
@@ -50,6 +50,7 @@ use rama_http::StatusCode;
|
||||
use rama_http::header;
|
||||
use rama_http::headers::HeaderMapExt;
|
||||
use rama_http::headers::Host;
|
||||
use rama_http::headers::ProxyAuthorization;
|
||||
use rama_http::layer::remove_header::RemoveResponseHeaderLayer;
|
||||
use rama_http::matcher::MethodMatcher;
|
||||
use rama_http_backend::client::proxy::layer::HttpProxyConnector;
|
||||
@@ -65,6 +66,7 @@ use rama_net::proxy::ProxyRequest;
|
||||
use rama_net::proxy::ProxyTarget;
|
||||
use rama_net::proxy::StreamForwardService;
|
||||
use rama_net::stream::SocketInfo;
|
||||
use rama_net::user::Basic;
|
||||
use rama_tcp::client::Request as TcpRequest;
|
||||
use rama_tcp::client::service::TcpConnector;
|
||||
use rama_tcp::server::TcpListener;
|
||||
@@ -149,6 +151,12 @@ async fn run_http_proxy_with_listener(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn proxy_authorization_network_owner_id<T>(req: &Request<T>) -> Option<String> {
|
||||
req.headers()
|
||||
.typed_get::<ProxyAuthorization<Basic>>()
|
||||
.map(|header| header.0.username().to_string())
|
||||
}
|
||||
|
||||
async fn http_connect_accept(
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
mut req: Request,
|
||||
@@ -173,6 +181,7 @@ async fn http_connect_accept(
|
||||
}
|
||||
|
||||
let client = client_addr(&req);
|
||||
let network_owner_id = proxy_authorization_network_owner_id(&req);
|
||||
let enabled = app_state
|
||||
.enabled()
|
||||
.await
|
||||
@@ -185,6 +194,7 @@ async fn http_connect_accept(
|
||||
host,
|
||||
authority.port,
|
||||
client_addr(&req),
|
||||
network_owner_id,
|
||||
Some("CONNECT".to_string()),
|
||||
NetworkProtocol::HttpsConnect,
|
||||
/*audit_endpoint_override*/ None,
|
||||
@@ -196,6 +206,7 @@ async fn http_connect_accept(
|
||||
protocol: NetworkProtocol::HttpsConnect,
|
||||
host: host.clone(),
|
||||
port: authority.port,
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client_addr: client.clone(),
|
||||
method: Some("CONNECT".to_string()),
|
||||
command: None,
|
||||
@@ -220,6 +231,7 @@ async fn http_connect_accept(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: reason.clone(),
|
||||
network_owner_id,
|
||||
client: client.clone(),
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: None,
|
||||
@@ -283,6 +295,7 @@ async fn http_connect_accept(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_MITM_REQUIRED.to_string(),
|
||||
network_owner_id: None,
|
||||
client: client.clone(),
|
||||
method: Some("CONNECT".to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
@@ -432,6 +445,7 @@ async fn http_plain_proxy(
|
||||
}
|
||||
};
|
||||
let client = client_addr(&req);
|
||||
let network_owner_id = proxy_authorization_network_owner_id(&req);
|
||||
let method_allowed = match app_state
|
||||
.method_allowed(req.method().as_str())
|
||||
.await
|
||||
@@ -471,6 +485,7 @@ async fn http_plain_proxy(
|
||||
socket_path,
|
||||
/*port*/ 0,
|
||||
client_addr(&req),
|
||||
network_owner_id.clone(),
|
||||
Some(req.method().as_str().to_string()),
|
||||
NetworkProtocol::Http,
|
||||
Some(("unix-socket", 0)),
|
||||
@@ -616,6 +631,7 @@ async fn http_plain_proxy(
|
||||
host,
|
||||
port,
|
||||
client_addr(&req),
|
||||
network_owner_id.clone(),
|
||||
Some(req.method().as_str().to_string()),
|
||||
NetworkProtocol::Http,
|
||||
/*audit_endpoint_override*/ None,
|
||||
@@ -627,6 +643,7 @@ async fn http_plain_proxy(
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: host.clone(),
|
||||
port,
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client_addr: client.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
command: None,
|
||||
@@ -651,6 +668,7 @@ async fn http_plain_proxy(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: reason.clone(),
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client: client.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
mode: None,
|
||||
@@ -696,6 +714,7 @@ async fn http_plain_proxy(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
network_owner_id,
|
||||
client: client.clone(),
|
||||
method: Some(req.method().as_str().to_string()),
|
||||
mode: Some(NetworkMode::Limited),
|
||||
@@ -889,6 +908,7 @@ async fn proxy_disabled_response(
|
||||
host: String,
|
||||
port: u16,
|
||||
client: Option<String>,
|
||||
network_owner_id: Option<String>,
|
||||
method: Option<String>,
|
||||
protocol: NetworkProtocol,
|
||||
audit_endpoint_override: Option<(&str, u16)>,
|
||||
@@ -913,6 +933,7 @@ async fn proxy_disabled_response(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: blocked_host,
|
||||
reason: REASON_PROXY_DISABLED.to_string(),
|
||||
network_owner_id,
|
||||
client,
|
||||
method,
|
||||
mode: None,
|
||||
|
||||
@@ -5,6 +5,7 @@ mod config;
|
||||
mod http_proxy;
|
||||
mod mitm;
|
||||
mod network_policy;
|
||||
mod owner_identity;
|
||||
mod policy;
|
||||
mod proxy;
|
||||
mod reasons;
|
||||
|
||||
@@ -289,6 +289,7 @@ async fn mitm_blocking_response(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: policy.target_host.clone(),
|
||||
reason: reason.to_string(),
|
||||
network_owner_id: None,
|
||||
client: client.clone(),
|
||||
method: Some(method.clone()),
|
||||
mode: Some(policy.mode),
|
||||
@@ -311,6 +312,7 @@ async fn mitm_blocking_response(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: policy.target_host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
network_owner_id: None,
|
||||
client: client.clone(),
|
||||
method: Some(method.clone()),
|
||||
mode: Some(policy.mode),
|
||||
|
||||
@@ -79,6 +79,7 @@ pub struct NetworkPolicyRequest {
|
||||
pub protocol: NetworkProtocol,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub network_owner_id: Option<String>,
|
||||
pub client_addr: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub command: Option<String>,
|
||||
@@ -89,6 +90,7 @@ pub struct NetworkPolicyRequestArgs {
|
||||
pub protocol: NetworkProtocol,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub network_owner_id: Option<String>,
|
||||
pub client_addr: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub command: Option<String>,
|
||||
@@ -101,6 +103,7 @@ impl NetworkPolicyRequest {
|
||||
protocol,
|
||||
host,
|
||||
port,
|
||||
network_owner_id,
|
||||
client_addr,
|
||||
method,
|
||||
command,
|
||||
@@ -110,6 +113,7 @@ impl NetworkPolicyRequest {
|
||||
protocol,
|
||||
host,
|
||||
port,
|
||||
network_owner_id,
|
||||
client_addr,
|
||||
method,
|
||||
command,
|
||||
@@ -625,6 +629,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
network_owner_id: None,
|
||||
client_addr: None,
|
||||
method: None,
|
||||
command: None,
|
||||
@@ -685,6 +690,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "blocked.com".to_string(),
|
||||
port: 80,
|
||||
network_owner_id: None,
|
||||
client_addr: Some("127.0.0.1:1234".to_string()),
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
@@ -726,6 +732,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
network_owner_id: None,
|
||||
client_addr: None,
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
@@ -776,6 +783,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "example.com".to_string(),
|
||||
port: 80,
|
||||
network_owner_id: None,
|
||||
client_addr: None,
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
@@ -859,6 +867,7 @@ mod tests {
|
||||
protocol: NetworkProtocol::Http,
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 80,
|
||||
network_owner_id: None,
|
||||
client_addr: None,
|
||||
method: Some("GET".to_string()),
|
||||
command: None,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use rama_core::extensions::Extensions;
|
||||
use rama_core::extensions::ExtensionsRef;
|
||||
use rama_http::headers::authorization::AuthoritySync;
|
||||
use rama_net::user::Basic;
|
||||
use rama_net::user::UserId;
|
||||
use rama_net::user::authority::AuthorizeResult;
|
||||
use rama_net::user::authority::Authorizer;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub(crate) struct ProxyOwnerAuthorizer;
|
||||
|
||||
impl AuthoritySync<Basic, ()> for ProxyOwnerAuthorizer {
|
||||
fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool {
|
||||
ext.insert(UserId::Username(credentials.username().to_string()));
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl Authorizer<Basic> for ProxyOwnerAuthorizer {
|
||||
type Error = std::convert::Infallible;
|
||||
|
||||
async fn authorize(&self, credentials: Basic) -> AuthorizeResult<Basic, Self::Error> {
|
||||
let mut extensions = Extensions::new();
|
||||
extensions.insert(UserId::Username(credentials.username().to_string()));
|
||||
AuthorizeResult {
|
||||
credentials,
|
||||
result: Ok(Some(extensions)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn network_owner_id<T: ExtensionsRef>(input: &T) -> Option<String> {
|
||||
match input.extensions().get::<UserId>() {
|
||||
Some(UserId::Username(username)) => Some(username.clone()),
|
||||
Some(UserId::Token(token)) => String::from_utf8(token.clone()).ok(),
|
||||
Some(UserId::Anonymous) | None => None,
|
||||
}
|
||||
}
|
||||
@@ -305,15 +305,23 @@ fn set_env_keys(env: &mut HashMap<String, String>, keys: &[&str], value: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn proxy_url(scheme: &str, addr: SocketAddr, network_owner_id: Option<&str>) -> String {
|
||||
match network_owner_id {
|
||||
Some(network_owner_id) => format!("{scheme}://{network_owner_id}@{addr}"),
|
||||
None => format!("{scheme}://{addr}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_proxy_env_overrides(
|
||||
env: &mut HashMap<String, String>,
|
||||
http_addr: SocketAddr,
|
||||
socks_addr: SocketAddr,
|
||||
socks_enabled: bool,
|
||||
allow_local_binding: bool,
|
||||
network_owner_id: Option<&str>,
|
||||
) {
|
||||
let http_proxy_url = format!("http://{http_addr}");
|
||||
let socks_proxy_url = format!("socks5h://{socks_addr}");
|
||||
let http_proxy_url = proxy_url("http", http_addr, network_owner_id);
|
||||
let socks_proxy_url = proxy_url("socks5h", socks_addr, network_owner_id);
|
||||
env.insert(
|
||||
ALLOW_LOCAL_BINDING_ENV_KEY.to_string(),
|
||||
if allow_local_binding {
|
||||
@@ -414,6 +422,14 @@ impl NetworkProxy {
|
||||
}
|
||||
|
||||
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
|
||||
self.apply_to_env_for_owner(env, /*network_owner_id*/ None);
|
||||
}
|
||||
|
||||
pub fn apply_to_env_for_owner(
|
||||
&self,
|
||||
env: &mut HashMap<String, String>,
|
||||
network_owner_id: Option<&str>,
|
||||
) {
|
||||
// Enforce proxying for child processes. We intentionally override existing values so
|
||||
// command-level environment cannot bypass the managed proxy endpoint.
|
||||
apply_proxy_env_overrides(
|
||||
@@ -422,6 +438,7 @@ impl NetworkProxy {
|
||||
self.socks_addr,
|
||||
self.socks_enabled,
|
||||
self.allow_local_binding,
|
||||
network_owner_id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -696,6 +713,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
/*network_owner_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -746,6 +764,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
false,
|
||||
true,
|
||||
/*network_owner_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -764,6 +783,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
/*network_owner_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -809,6 +829,7 @@ mod tests {
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
/*network_owner_id*/ None,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -816,4 +837,26 @@ mod tests {
|
||||
Some(&"ssh -o ProxyCommand='tsh proxy ssh --cluster=dev %r@%h:%p'".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_proxy_env_overrides_includes_owner_credentials() {
|
||||
let mut env = HashMap::new();
|
||||
apply_proxy_env_overrides(
|
||||
&mut env,
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128),
|
||||
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
|
||||
true,
|
||||
false,
|
||||
Some("owner-1"),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
env.get("HTTP_PROXY"),
|
||||
Some(&"http://owner-1@127.0.0.1:3128".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
env.get("ALL_PROXY"),
|
||||
Some(&"socks5h://owner-1@127.0.0.1:8081".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,8 @@ pub enum HostBlockDecision {
|
||||
pub struct BlockedRequest {
|
||||
pub host: String,
|
||||
pub reason: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub network_owner_id: Option<String>,
|
||||
pub client: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub mode: Option<NetworkMode>,
|
||||
@@ -100,6 +102,7 @@ pub struct BlockedRequest {
|
||||
pub struct BlockedRequestArgs {
|
||||
pub host: String,
|
||||
pub reason: String,
|
||||
pub network_owner_id: Option<String>,
|
||||
pub client: Option<String>,
|
||||
pub method: Option<String>,
|
||||
pub mode: Option<NetworkMode>,
|
||||
@@ -114,6 +117,7 @@ impl BlockedRequest {
|
||||
let BlockedRequestArgs {
|
||||
host,
|
||||
reason,
|
||||
network_owner_id,
|
||||
client,
|
||||
method,
|
||||
mode,
|
||||
@@ -125,6 +129,7 @@ impl BlockedRequest {
|
||||
Self {
|
||||
host,
|
||||
reason,
|
||||
network_owner_id,
|
||||
client,
|
||||
method,
|
||||
mode,
|
||||
@@ -994,6 +999,7 @@ mod tests {
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: "google.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
network_owner_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
@@ -1034,6 +1040,7 @@ mod tests {
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: format!("example{idx}.com"),
|
||||
reason: "not_allowed".to_string(),
|
||||
network_owner_id: None,
|
||||
client: None,
|
||||
method: Some("GET".to_string()),
|
||||
mode: None,
|
||||
@@ -1056,6 +1063,7 @@ mod tests {
|
||||
let entry = BlockedRequest {
|
||||
host: "google.com".to_string(),
|
||||
reason: "not_allowed".to_string(),
|
||||
network_owner_id: None,
|
||||
client: Some("127.0.0.1".to_string()),
|
||||
method: Some("GET".to_string()),
|
||||
mode: Some(NetworkMode::Full),
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::network_policy::NetworkPolicyRequestArgs;
|
||||
use crate::network_policy::NetworkProtocol;
|
||||
use crate::network_policy::emit_block_decision_audit_event;
|
||||
use crate::network_policy::evaluate_host_policy;
|
||||
use crate::owner_identity::ProxyOwnerAuthorizer;
|
||||
use crate::owner_identity::network_owner_id as extract_network_owner_id;
|
||||
use crate::policy::normalize_host;
|
||||
use crate::reasons::REASON_METHOD_NOT_ALLOWED;
|
||||
use crate::reasons::REASON_PROXY_DISABLED;
|
||||
@@ -105,7 +107,11 @@ async fn run_socks5_with_listener(
|
||||
});
|
||||
|
||||
let socks_connector = DefaultConnector::default().with_connector(policy_tcp_connector);
|
||||
let base = Socks5Acceptor::new().with_connector(socks_connector);
|
||||
let mut base = Socks5Acceptor::new();
|
||||
base.set_auth_optional(true);
|
||||
let base = base
|
||||
.with_connector(socks_connector)
|
||||
.with_authorizer(ProxyOwnerAuthorizer);
|
||||
|
||||
if enable_socks5_udp {
|
||||
let udp_state = state.clone();
|
||||
@@ -150,6 +156,7 @@ async fn handle_socks5_tcp(
|
||||
.extensions()
|
||||
.get::<SocketInfo>()
|
||||
.map(|info| info.peer_addr().to_string());
|
||||
let network_owner_id = extract_network_owner_id(&req);
|
||||
|
||||
match app_state.enabled().await {
|
||||
Ok(true) => {}
|
||||
@@ -175,6 +182,7 @@ async fn handle_socks5_tcp(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_PROXY_DISABLED.to_string(),
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: None,
|
||||
@@ -217,6 +225,7 @@ async fn handle_socks5_tcp(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(NetworkMode::Limited),
|
||||
@@ -243,6 +252,7 @@ async fn handle_socks5_tcp(
|
||||
protocol: NetworkProtocol::Socks5Tcp,
|
||||
host: host.clone(),
|
||||
port,
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client_addr: client.clone(),
|
||||
method: None,
|
||||
command: None,
|
||||
@@ -267,6 +277,7 @@ async fn handle_socks5_tcp(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: reason.clone(),
|
||||
network_owner_id,
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: None,
|
||||
@@ -314,6 +325,7 @@ async fn inspect_socks5_udp(
|
||||
let client = extensions
|
||||
.get::<SocketInfo>()
|
||||
.map(|info| info.peer_addr().to_string());
|
||||
let network_owner_id = extract_network_owner_id(&extensions);
|
||||
|
||||
match state.enabled().await {
|
||||
Ok(true) => {}
|
||||
@@ -339,6 +351,7 @@ async fn inspect_socks5_udp(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_PROXY_DISABLED.to_string(),
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: None,
|
||||
@@ -381,6 +394,7 @@ async fn inspect_socks5_udp(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: REASON_METHOD_NOT_ALLOWED.to_string(),
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: Some(NetworkMode::Limited),
|
||||
@@ -403,6 +417,7 @@ async fn inspect_socks5_udp(
|
||||
protocol: NetworkProtocol::Socks5Udp,
|
||||
host: host.clone(),
|
||||
port,
|
||||
network_owner_id: network_owner_id.clone(),
|
||||
client_addr: client.clone(),
|
||||
method: None,
|
||||
command: None,
|
||||
@@ -427,6 +442,7 @@ async fn inspect_socks5_udp(
|
||||
.record_blocked(BlockedRequest::new(BlockedRequestArgs {
|
||||
host: host.clone(),
|
||||
reason: reason.clone(),
|
||||
network_owner_id,
|
||||
client: client.clone(),
|
||||
method: None,
|
||||
mode: None,
|
||||
|
||||
Reference in New Issue
Block a user