Skip to content

Commit b4f6ced

Browse files
committed
fix(policy): reserve provider rule namespace
Closes #1982 Reject user-authored _provider_* network policy keys at gateway boundaries, strip provider-derived rules from sandbox sync, and document the round-trip workflow. Signed-off-by: John Myers <[email protected]>
1 parent 62b03f0 commit b4f6ced

9 files changed

Lines changed: 413 additions & 16 deletions

File tree

‎crates/openshell-policy/src/compose.rs‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,19 @@
55
66
use openshell_core::proto::{NetworkPolicyRule, SandboxPolicy};
77

8+
pub const PROVIDER_RULE_NAME_PREFIX: &str = "_provider_";
9+
810
#[derive(Debug, Clone, PartialEq)]
911
pub struct ProviderPolicyLayer {
1012
pub rule_name: String,
1113
pub rule: NetworkPolicyRule,
1214
}
1315

16+
#[must_use]
17+
pub fn is_provider_rule_name(rule_name: &str) -> bool {
18+
rule_name.starts_with(PROVIDER_RULE_NAME_PREFIX)
19+
}
20+
1421
#[must_use]
1522
pub fn provider_rule_name(provider_name: &str) -> String {
1623
let sanitized = provider_name
@@ -27,12 +34,20 @@ pub fn provider_rule_name(provider_name: &str) -> String {
2734
.to_string();
2835

2936
if sanitized.is_empty() {
30-
"_provider_unnamed".to_string()
37+
format!("{PROVIDER_RULE_NAME_PREFIX}unnamed")
3138
} else {
32-
format!("_provider_{sanitized}")
39+
format!("{PROVIDER_RULE_NAME_PREFIX}{sanitized}")
3340
}
3441
}
3542

43+
pub fn strip_provider_rule_names(policy: &mut SandboxPolicy) -> bool {
44+
let original_len = policy.network_policies.len();
45+
policy
46+
.network_policies
47+
.retain(|key, _| !is_provider_rule_name(key));
48+
policy.network_policies.len() != original_len
49+
}
50+
3651
/// Compose a normal sandbox policy from user-authored policy plus provider
3752
/// policy layers.
3853
///
@@ -76,7 +91,10 @@ fn unique_provider_rule_key(policy: &SandboxPolicy, preferred: &str) -> String {
7691

7792
#[cfg(test)]
7893
mod tests {
79-
use super::{ProviderPolicyLayer, compose_effective_policy, provider_rule_name};
94+
use super::{
95+
PROVIDER_RULE_NAME_PREFIX, ProviderPolicyLayer, compose_effective_policy,
96+
is_provider_rule_name, provider_rule_name, strip_provider_rule_names,
97+
};
8098
use openshell_core::proto::{NetworkEndpoint, NetworkPolicyRule, SandboxPolicy};
8199

82100
fn rule(name: &str, host: &str) -> NetworkPolicyRule {
@@ -107,6 +125,38 @@ mod tests {
107125
assert_eq!(provider_rule_name("..."), "_provider_unnamed");
108126
}
109127

128+
#[test]
129+
fn provider_rule_name_prefix_identifies_reserved_keys() {
130+
assert_eq!(PROVIDER_RULE_NAME_PREFIX, "_provider_");
131+
assert!(is_provider_rule_name("_provider_work_github"));
132+
assert!(is_provider_rule_name("_provider_work_github_2"));
133+
assert!(is_provider_rule_name("_provider_"));
134+
assert!(!is_provider_rule_name("provider_work_github"));
135+
assert!(!is_provider_rule_name("custom_provider_work_github"));
136+
}
137+
138+
#[test]
139+
fn strip_provider_rule_names_removes_only_reserved_keys() {
140+
let mut policy = SandboxPolicy::default();
141+
policy.network_policies.insert(
142+
"_provider_work_github".to_string(),
143+
rule("_provider_work_github", "api.github.com"),
144+
);
145+
policy.network_policies.insert(
146+
"sandbox_only".to_string(),
147+
rule("sandbox_only", "sandbox.example.com"),
148+
);
149+
150+
assert!(strip_provider_rule_names(&mut policy));
151+
assert!(
152+
!policy
153+
.network_policies
154+
.contains_key("_provider_work_github")
155+
);
156+
assert!(policy.network_policies.contains_key("sandbox_only"));
157+
assert!(!strip_provider_rule_names(&mut policy));
158+
}
159+
110160
#[test]
111161
fn compose_concatenates_provider_rules_without_overwriting_user_rules() {
112162
let mut source = SandboxPolicy::default();

‎crates/openshell-policy/src/lib.rs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ use openshell_core::proto::{
2424
};
2525
use serde::{Deserialize, Serialize};
2626

27-
pub use compose::{ProviderPolicyLayer, compose_effective_policy, provider_rule_name};
27+
pub use compose::{
28+
PROVIDER_RULE_NAME_PREFIX, ProviderPolicyLayer, compose_effective_policy,
29+
is_provider_rule_name, provider_rule_name, strip_provider_rule_names,
30+
};
2831
pub use merge::{
2932
PolicyMergeError, PolicyMergeOp, PolicyMergeResult, PolicyMergeWarning, generated_rule_name,
3033
merge_policy, policy_covers_rule,

‎crates/openshell-policy/src/merge.rs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ use openshell_core::proto::{
77
L7Allow, L7DenyRule, L7Rule, NetworkBinary, NetworkEndpoint, NetworkPolicyRule, SandboxPolicy,
88
};
99

10+
use crate::is_provider_rule_name;
11+
1012
#[derive(Debug, Clone, PartialEq)]
1113
pub enum PolicyMergeOp {
1214
AddRule {
@@ -413,7 +415,7 @@ fn add_rule(
413415
let mut keys: Vec<_> = policy.network_policies.keys().cloned().collect();
414416
keys.sort();
415417
keys.into_iter()
416-
.filter(|k| !k.starts_with("_provider_"))
418+
.filter(|k| !is_provider_rule_name(k))
417419
.find(|key| {
418420
policy
419421
.network_policies
@@ -653,7 +655,7 @@ fn find_endpoint_mut<'a>(
653655
keys.sort();
654656
let target_key = keys
655657
.into_iter()
656-
.filter(|k| !k.starts_with("_provider_"))
658+
.filter(|k| !is_provider_rule_name(k))
657659
.find(|key| {
658660
policy.network_policies.get(key).is_some_and(|rule| {
659661
rule.endpoints

‎crates/openshell-sandbox/src/lib.rs‎

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -886,6 +886,10 @@ fn enrich_proto_baseline_paths(proto: &mut openshell_core::proto::SandboxPolicy)
886886
modified
887887
}
888888

889+
fn strip_proto_provider_policy_entries(proto: &mut openshell_core::proto::SandboxPolicy) -> bool {
890+
openshell_policy::strip_provider_rule_names(proto)
891+
}
892+
889893
/// Ensure a `SandboxPolicy` (Rust type) includes the baseline filesystem
890894
/// paths required by proxy-mode sandboxes and GPU runtimes. Used for the
891895
/// local-file code path where no proto is available.
@@ -1050,6 +1054,69 @@ mod baseline_tests {
10501054
);
10511055
}
10521056

1057+
#[test]
1058+
fn proto_strip_provider_policy_entries_removes_only_reserved_entries() {
1059+
let mut policy = openshell_policy::restrictive_default_policy();
1060+
policy.network_policies.insert(
1061+
"_provider_work_github".to_string(),
1062+
openshell_core::proto::NetworkPolicyRule {
1063+
name: "_provider_work_github".to_string(),
1064+
..Default::default()
1065+
},
1066+
);
1067+
policy.network_policies.insert(
1068+
"sandbox_only".to_string(),
1069+
openshell_core::proto::NetworkPolicyRule {
1070+
name: "sandbox_only".to_string(),
1071+
..Default::default()
1072+
},
1073+
);
1074+
1075+
assert!(strip_proto_provider_policy_entries(&mut policy));
1076+
assert!(
1077+
!policy
1078+
.network_policies
1079+
.contains_key("_provider_work_github")
1080+
);
1081+
assert!(policy.network_policies.contains_key("sandbox_only"));
1082+
assert!(!strip_proto_provider_policy_entries(&mut policy));
1083+
}
1084+
1085+
#[test]
1086+
fn proto_sync_payload_strips_provider_entries_without_mutating_runtime_policy() {
1087+
let mut runtime_policy = openshell_policy::restrictive_default_policy();
1088+
runtime_policy.network_policies.insert(
1089+
"_provider_work_github".to_string(),
1090+
openshell_core::proto::NetworkPolicyRule {
1091+
name: "_provider_work_github".to_string(),
1092+
..Default::default()
1093+
},
1094+
);
1095+
runtime_policy.network_policies.insert(
1096+
"sandbox_only".to_string(),
1097+
openshell_core::proto::NetworkPolicyRule {
1098+
name: "sandbox_only".to_string(),
1099+
..Default::default()
1100+
},
1101+
);
1102+
1103+
let mut sync_policy = runtime_policy.clone();
1104+
assert!(strip_proto_provider_policy_entries(&mut sync_policy));
1105+
1106+
assert!(
1107+
runtime_policy
1108+
.network_policies
1109+
.contains_key("_provider_work_github"),
1110+
"runtime policy must retain provider-derived rules for OPA input"
1111+
);
1112+
assert!(
1113+
!sync_policy
1114+
.network_policies
1115+
.contains_key("_provider_work_github")
1116+
);
1117+
assert!(sync_policy.network_policies.contains_key("sandbox_only"));
1118+
}
1119+
10531120
#[test]
10541121
fn proto_gpu_enrichment_promotes_proc_without_network_policy() {
10551122
let mut policy = openshell_policy::restrictive_default_policy();
@@ -1298,6 +1365,7 @@ async fn load_policy(
12981365
// Enrich before syncing so the gateway baseline includes
12991366
// baseline paths from the start.
13001367
enrich_proto_baseline_paths(&mut discovered);
1368+
strip_proto_provider_policy_entries(&mut discovered);
13011369
let sandbox = sandbox.as_deref().ok_or_else(|| {
13021370
miette::miette!(
13031371
"Cannot sync discovered policy: sandbox not available.\n\
@@ -1322,11 +1390,12 @@ async fn load_policy(
13221390
// sandboxes. If the policy was enriched, sync the updated version
13231391
// back to the gateway so users can see the effective policy.
13241392
let enriched = enrich_proto_baseline_paths(&mut proto_policy);
1325-
if enriched
1393+
let mut sync_policy = proto_policy.clone();
1394+
let stripped_provider_entries = strip_proto_provider_policy_entries(&mut sync_policy);
1395+
if (enriched || stripped_provider_entries)
13261396
&& let Some(sandbox_name) = sandbox.as_deref()
13271397
&& let Err(e) =
1328-
openshell_core::grpc_client::sync_policy(endpoint, sandbox_name, &proto_policy)
1329-
.await
1398+
openshell_core::grpc_client::sync_policy(endpoint, sandbox_name, &sync_policy).await
13301399
{
13311400
warn!(
13321401
error = %e,

0 commit comments

Comments
 (0)