## Summary When network requests were blocked, downstream code often had to infer ask vs deny from free-form response text. That was brittle and led to incorrect approval behavior. This PR fixes the proxy side so blocked decisions are structured and request metadata survives reliably. ## Description - Blocked proxy responses now carry consistent structured policy decision data. - Request attempt metadata is preserved across proxy env paths (including ALL_PROXY flows). - Header stripping was tightened so we still remove unsafe forwarding headers, but keep metadata needed for policy handling. - Block messages were clarified (for example, allowlist miss vs explicit deny). - Added unified violation log entries so policy failures can be inspected in one place. - Added/updated tests for these behaviors. --------- Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
114 lines
3.7 KiB
Rust
114 lines
3.7 KiB
Rust
use crate::network_policy::NetworkDecisionSource;
|
|
use crate::network_policy::NetworkPolicyDecision;
|
|
use crate::network_policy::NetworkProtocol;
|
|
use crate::reasons::REASON_DENIED;
|
|
use crate::reasons::REASON_METHOD_NOT_ALLOWED;
|
|
use crate::reasons::REASON_NOT_ALLOWED;
|
|
use crate::reasons::REASON_NOT_ALLOWED_LOCAL;
|
|
use rama_http::Body;
|
|
use rama_http::Response;
|
|
use rama_http::StatusCode;
|
|
use serde::Serialize;
|
|
use tracing::error;
|
|
|
|
pub struct PolicyDecisionDetails<'a> {
|
|
pub decision: NetworkPolicyDecision,
|
|
pub reason: &'a str,
|
|
pub source: NetworkDecisionSource,
|
|
pub protocol: NetworkProtocol,
|
|
pub host: &'a str,
|
|
pub port: u16,
|
|
}
|
|
|
|
pub fn text_response(status: StatusCode, body: &str) -> Response {
|
|
Response::builder()
|
|
.status(status)
|
|
.header("content-type", "text/plain")
|
|
.body(Body::from(body.to_string()))
|
|
.unwrap_or_else(|_| Response::new(Body::from(body.to_string())))
|
|
}
|
|
|
|
pub fn json_response<T: Serialize>(value: &T) -> Response {
|
|
let body = match serde_json::to_string(value) {
|
|
Ok(body) => body,
|
|
Err(err) => {
|
|
error!("failed to serialize JSON response: {err}");
|
|
"{}".to_string()
|
|
}
|
|
};
|
|
Response::builder()
|
|
.status(StatusCode::OK)
|
|
.header("content-type", "application/json")
|
|
.body(Body::from(body))
|
|
.unwrap_or_else(|err| {
|
|
error!("failed to build JSON response: {err}");
|
|
Response::new(Body::from("{}"))
|
|
})
|
|
}
|
|
|
|
pub fn blocked_header_value(reason: &str) -> &'static str {
|
|
match reason {
|
|
REASON_NOT_ALLOWED | REASON_NOT_ALLOWED_LOCAL => "blocked-by-allowlist",
|
|
REASON_DENIED => "blocked-by-denylist",
|
|
REASON_METHOD_NOT_ALLOWED => "blocked-by-method-policy",
|
|
_ => "blocked-by-policy",
|
|
}
|
|
}
|
|
|
|
pub fn blocked_message(reason: &str) -> &'static str {
|
|
match reason {
|
|
REASON_NOT_ALLOWED => {
|
|
"Codex blocked this request: domain not in allowlist (this is not a denylist block)."
|
|
}
|
|
REASON_NOT_ALLOWED_LOCAL => {
|
|
"Codex blocked this request: local/private addresses not allowed."
|
|
}
|
|
REASON_DENIED => "Codex blocked this request: domain denied by policy.",
|
|
REASON_METHOD_NOT_ALLOWED => {
|
|
"Codex blocked this request: method not allowed in limited mode."
|
|
}
|
|
_ => "Codex blocked this request by network policy.",
|
|
}
|
|
}
|
|
|
|
pub fn blocked_message_with_policy(reason: &str, details: &PolicyDecisionDetails<'_>) -> String {
|
|
let _ = (details.reason, details.host);
|
|
blocked_message(reason).to_string()
|
|
}
|
|
|
|
pub fn blocked_text_response_with_policy(
|
|
reason: &str,
|
|
details: &PolicyDecisionDetails<'_>,
|
|
) -> Response {
|
|
Response::builder()
|
|
.status(StatusCode::FORBIDDEN)
|
|
.header("content-type", "text/plain")
|
|
.header("x-proxy-error", blocked_header_value(reason))
|
|
.body(Body::from(blocked_message_with_policy(reason, details)))
|
|
.unwrap_or_else(|_| Response::new(Body::from("blocked")))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::reasons::REASON_NOT_ALLOWED;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
#[test]
|
|
fn blocked_message_with_policy_returns_human_message() {
|
|
let details = PolicyDecisionDetails {
|
|
decision: NetworkPolicyDecision::Ask,
|
|
reason: REASON_NOT_ALLOWED,
|
|
source: NetworkDecisionSource::Decider,
|
|
protocol: NetworkProtocol::HttpsConnect,
|
|
host: "api.example.com",
|
|
port: 443,
|
|
};
|
|
|
|
let message = blocked_message_with_policy(REASON_NOT_ALLOWED, &details);
|
|
assert_eq!(
|
|
message,
|
|
"Codex blocked this request: domain not in allowlist (this is not a denylist block)."
|
|
);
|
|
}
|
|
}
|