mirror of
https://github.com/openai/codex.git
synced 2026-02-02 06:57:03 +00:00
Compare commits
1 Commits
dev/cc/new
...
pr-network
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fcb37e01b |
@@ -22,6 +22,12 @@ pub enum AmendError {
|
||||
},
|
||||
#[error("failed to format prefix tokens: {source}")]
|
||||
SerializePrefix { source: serde_json::Error },
|
||||
#[error("network rule host cannot be empty")]
|
||||
EmptyNetworkHost,
|
||||
#[error("network rule protocol must be http or https")]
|
||||
InvalidNetworkProtocol,
|
||||
#[error("network rule decision must be allow, deny, or ask")]
|
||||
InvalidNetworkDecision,
|
||||
#[error("failed to open policy file {path}: {source}")]
|
||||
OpenPolicyFile {
|
||||
path: PathBuf,
|
||||
@@ -90,6 +96,53 @@ pub fn blocking_append_allow_prefix_rule(
|
||||
append_locked_line(policy_path, &rule)
|
||||
}
|
||||
|
||||
/// Append a `network_rule(...)` line to the policy file.
|
||||
pub fn blocking_append_network_rule(
|
||||
policy_path: &Path,
|
||||
host: &str,
|
||||
protocol: &str,
|
||||
decision: &str,
|
||||
justification: Option<&str>,
|
||||
) -> Result<(), AmendError> {
|
||||
let host = host.trim();
|
||||
if host.is_empty() {
|
||||
return Err(AmendError::EmptyNetworkHost);
|
||||
}
|
||||
if !matches!(protocol, "http" | "https") {
|
||||
return Err(AmendError::InvalidNetworkProtocol);
|
||||
}
|
||||
if !matches!(decision, "allow" | "deny" | "ask") {
|
||||
return Err(AmendError::InvalidNetworkDecision);
|
||||
}
|
||||
let host =
|
||||
serde_json::to_string(host).map_err(|source| AmendError::SerializePrefix { source })?;
|
||||
let mut rule =
|
||||
format!(r#"network_rule(host={host}, protocol="{protocol}", decision="{decision}""#);
|
||||
if let Some(justification) = justification {
|
||||
let justification = serde_json::to_string(justification)
|
||||
.map_err(|source| AmendError::SerializePrefix { source })?;
|
||||
rule.push_str(&format!(", justification={justification}"));
|
||||
}
|
||||
rule.push(')');
|
||||
|
||||
let dir = policy_path
|
||||
.parent()
|
||||
.ok_or_else(|| AmendError::MissingParent {
|
||||
path: policy_path.to_path_buf(),
|
||||
})?;
|
||||
match std::fs::create_dir(dir) {
|
||||
Ok(()) => {}
|
||||
Err(ref source) if source.kind() == std::io::ErrorKind::AlreadyExists => {}
|
||||
Err(source) => {
|
||||
return Err(AmendError::CreatePolicyDir {
|
||||
dir: dir.to_path_buf(),
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
append_locked_line(policy_path, &rule)
|
||||
}
|
||||
|
||||
fn append_locked_line(policy_path: &Path, line: &str) -> Result<(), AmendError> {
|
||||
let mut file = OpenOptions::new()
|
||||
.create(true)
|
||||
|
||||
@@ -8,6 +8,7 @@ pub mod rule;
|
||||
|
||||
pub use amend::AmendError;
|
||||
pub use amend::blocking_append_allow_prefix_rule;
|
||||
pub use amend::blocking_append_network_rule;
|
||||
pub use decision::Decision;
|
||||
pub use error::Error;
|
||||
pub use error::ErrorLocation;
|
||||
@@ -18,6 +19,9 @@ pub use execpolicycheck::ExecPolicyCheckCommand;
|
||||
pub use parser::PolicyParser;
|
||||
pub use policy::Evaluation;
|
||||
pub use policy::Policy;
|
||||
pub use rule::NetworkRule;
|
||||
pub use rule::NetworkRuleDecision;
|
||||
pub use rule::NetworkRuleProtocol;
|
||||
pub use rule::Rule;
|
||||
pub use rule::RuleMatch;
|
||||
pub use rule::RuleRef;
|
||||
|
||||
@@ -18,6 +18,9 @@ use std::sync::Arc;
|
||||
use crate::decision::Decision;
|
||||
use crate::error::Error;
|
||||
use crate::error::Result;
|
||||
use crate::rule::NetworkRule;
|
||||
use crate::rule::NetworkRuleDecision;
|
||||
use crate::rule::NetworkRuleProtocol;
|
||||
use crate::rule::PatternToken;
|
||||
use crate::rule::PrefixPattern;
|
||||
use crate::rule::PrefixRule;
|
||||
@@ -71,12 +74,14 @@ impl PolicyParser {
|
||||
#[derive(Debug, ProvidesStaticType)]
|
||||
struct PolicyBuilder {
|
||||
rules_by_program: MultiMap<String, RuleRef>,
|
||||
network_rules: Vec<NetworkRule>,
|
||||
}
|
||||
|
||||
impl PolicyBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
rules_by_program: MultiMap::new(),
|
||||
network_rules: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,8 +90,12 @@ impl PolicyBuilder {
|
||||
.insert(rule.program().to_string(), rule);
|
||||
}
|
||||
|
||||
fn add_network_rule(&mut self, rule: NetworkRule) {
|
||||
self.network_rules.push(rule);
|
||||
}
|
||||
|
||||
fn build(self) -> crate::policy::Policy {
|
||||
crate::policy::Policy::new(self.rules_by_program)
|
||||
crate::policy::Policy::new(self.rules_by_program, self.network_rules)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,4 +275,35 @@ fn policy_builtins(builder: &mut GlobalsBuilder) {
|
||||
rules.into_iter().for_each(|rule| builder.add_rule(rule));
|
||||
Ok(NoneType)
|
||||
}
|
||||
|
||||
fn network_rule<'v>(
|
||||
host: &'v str,
|
||||
protocol: &'v str,
|
||||
decision: &'v str,
|
||||
justification: Option<&'v str>,
|
||||
eval: &mut Evaluator<'v, '_, '_>,
|
||||
) -> anyhow::Result<NoneType> {
|
||||
let host = host.trim();
|
||||
if host.is_empty() {
|
||||
return Err(Error::InvalidRule("host cannot be empty".to_string()).into());
|
||||
}
|
||||
|
||||
let justification = match justification {
|
||||
Some(raw) if raw.trim().is_empty() => {
|
||||
return Err(Error::InvalidRule("justification cannot be empty".to_string()).into());
|
||||
}
|
||||
Some(raw) => Some(raw.to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let rule = NetworkRule {
|
||||
host: host.to_string(),
|
||||
protocol: NetworkRuleProtocol::parse(protocol)?,
|
||||
decision: NetworkRuleDecision::parse(decision)?,
|
||||
justification,
|
||||
};
|
||||
|
||||
policy_builder(eval).add_network_rule(rule);
|
||||
Ok(NoneType)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::decision::Decision;
|
||||
use crate::error::Error;
|
||||
use crate::error::Result;
|
||||
use crate::rule::NetworkRule;
|
||||
use crate::rule::PatternToken;
|
||||
use crate::rule::PrefixPattern;
|
||||
use crate::rule::PrefixRule;
|
||||
@@ -16,21 +17,32 @@ type HeuristicsFallback<'a> = Option<&'a dyn Fn(&[String]) -> Decision>;
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Policy {
|
||||
rules_by_program: MultiMap<String, RuleRef>,
|
||||
network_rules: Vec<NetworkRule>,
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
pub fn new(rules_by_program: MultiMap<String, RuleRef>) -> Self {
|
||||
Self { rules_by_program }
|
||||
pub fn new(
|
||||
rules_by_program: MultiMap<String, RuleRef>,
|
||||
network_rules: Vec<NetworkRule>,
|
||||
) -> Self {
|
||||
Self {
|
||||
rules_by_program,
|
||||
network_rules,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::new(MultiMap::new())
|
||||
Self::new(MultiMap::new(), Vec::new())
|
||||
}
|
||||
|
||||
pub fn rules(&self) -> &MultiMap<String, RuleRef> {
|
||||
&self.rules_by_program
|
||||
}
|
||||
|
||||
pub fn network_rules(&self) -> &[NetworkRule] {
|
||||
&self.network_rules
|
||||
}
|
||||
|
||||
pub fn get_allowed_prefixes(&self) -> Vec<Vec<String>> {
|
||||
let mut prefixes = Vec::new();
|
||||
|
||||
|
||||
@@ -92,6 +92,55 @@ pub struct PrefixRule {
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkRuleProtocol {
|
||||
Http,
|
||||
Https,
|
||||
}
|
||||
|
||||
impl NetworkRuleProtocol {
|
||||
pub fn parse(raw: &str) -> Result<Self> {
|
||||
match raw {
|
||||
"http" => Ok(Self::Http),
|
||||
"https" => Ok(Self::Https),
|
||||
other => Err(Error::InvalidRule(format!(
|
||||
"invalid network protocol: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NetworkRuleDecision {
|
||||
Allow,
|
||||
Deny,
|
||||
Ask,
|
||||
}
|
||||
|
||||
impl NetworkRuleDecision {
|
||||
pub fn parse(raw: &str) -> Result<Self> {
|
||||
match raw {
|
||||
"allow" => Ok(Self::Allow),
|
||||
"deny" => Ok(Self::Deny),
|
||||
"ask" => Ok(Self::Ask),
|
||||
other => Err(Error::InvalidRule(format!(
|
||||
"invalid network decision: {other}"
|
||||
))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NetworkRule {
|
||||
pub host: String,
|
||||
pub protocol: NetworkRuleProtocol,
|
||||
pub decision: NetworkRuleDecision,
|
||||
pub justification: Option<String>,
|
||||
}
|
||||
|
||||
pub trait Rule: Any + Debug + Send + Sync {
|
||||
fn program(&self) -> &str;
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ use anyhow::Result;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Error;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::NetworkRule;
|
||||
use codex_execpolicy::NetworkRuleDecision;
|
||||
use codex_execpolicy::NetworkRuleProtocol;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
@@ -72,6 +75,33 @@ prefix_rule(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_network_rule() -> Result<()> {
|
||||
let policy_src = r#"
|
||||
network_rule(
|
||||
host = "api.example.com",
|
||||
protocol = "https",
|
||||
decision = "allow",
|
||||
justification = "Allow API calls",
|
||||
)
|
||||
"#;
|
||||
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.rules", policy_src)?;
|
||||
let policy = parser.build();
|
||||
|
||||
assert_eq!(
|
||||
policy.network_rules(),
|
||||
&[NetworkRule {
|
||||
host: "api.example.com".to_string(),
|
||||
protocol: NetworkRuleProtocol::Https,
|
||||
decision: NetworkRuleDecision::Allow,
|
||||
justification: Some("Allow API calls".to_string()),
|
||||
}]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn justification_is_attached_to_forbidden_matches() -> Result<()> {
|
||||
let policy_src = r#"
|
||||
|
||||
Reference in New Issue
Block a user