From 9cc82c63041aa8ee8327229f7e1299e42edbebf8 Mon Sep 17 00:00:00 2001 From: Shiju Date: Sat, 5 Sep 2026 15:40:51 +0530 Subject: [PATCH] fix(supervisor-network): omit absent L7 selectors from policy data Build protobuf-derived allow objects from selectors that are actually present. This keeps protobuf and YAML policy validation aligned for protocols such as GraphQL over WebSocket. Signed-off-by: Shiju --- .../openshell-supervisor-network/src/opa.rs | 123 ++++++++++++++++-- 1 file changed, 110 insertions(+), 13 deletions(-) diff --git a/crates/openshell-supervisor-network/src/opa.rs b/crates/openshell-supervisor-network/src/opa.rs index 8d77da76e9..3303aa89b5 100644 --- a/crates/openshell-supervisor-network/src/opa.rs +++ b/crates/openshell-supervisor-network/src/opa.rs @@ -1936,29 +1936,48 @@ fn proto_to_opa_data_json(proto: &ProtoSandboxPolicy, entrypoint_pid: u32) -> St .iter() .map(|r| { let a = r.allow.as_ref(); - let mut allow = serde_json::json!({ - "method": a.map_or("", |a| &a.method), - "path": a.map_or("", |a| &a.path), - "command": a.map_or("", |a| &a.command), - "operation_type": a.map_or("", |a| &a.operation_type), - "operation_name": a.map_or("", |a| &a.operation_name), - }); - if let Some(a) = a - && !a.fields.is_empty() - { - allow["fields"] = a.fields.clone().into(); + let mut allow = serde_json::Map::new(); + if let Some(a) = a { + // Proto3 represents absent scalar selectors as empty + // strings. Omit them so protobuf and YAML rules expose + // the same selector families to runtime validation. + if !a.method.is_empty() { + allow.insert("method".to_string(), a.method.clone().into()); + } + if !a.path.is_empty() { + allow.insert("path".to_string(), a.path.clone().into()); + } + if !a.command.is_empty() { + allow + .insert("command".to_string(), a.command.clone().into()); + } + if !a.operation_type.is_empty() { + allow.insert( + "operation_type".to_string(), + a.operation_type.clone().into(), + ); + } + if !a.operation_name.is_empty() { + allow.insert( + "operation_name".to_string(), + a.operation_name.clone().into(), + ); + } + if !a.fields.is_empty() { + allow.insert("fields".to_string(), a.fields.clone().into()); + } } let query = a.map_or_else(serde_json::Map::new, |allow| { l7_matchers_to_json(&allow.query) }); if !query.is_empty() { - allow["query"] = query.into(); + allow.insert("query".to_string(), query.into()); } let params = a.map_or_else(serde_json::Map::new, |allow| { l7_matchers_to_json(&allow.params) }); if !params.is_empty() { - allow["params"] = params.into(); + allow.insert("params".to_string(), params.into()); } serde_json::json!({ "allow": allow }) }) @@ -2294,6 +2313,84 @@ mod tests { policy } + fn projected_allow( + protocol: &str, + allow: L7Allow, + ) -> serde_json::Map { + let proto = ProtoSandboxPolicy { + version: 1, + network_policies: std::collections::HashMap::from([( + "selector_projection".to_string(), + NetworkPolicyRule { + name: "selector_projection".to_string(), + endpoints: vec![NetworkEndpoint { + host: "api.example.com".to_string(), + port: 443, + protocol: protocol.to_string(), + rules: vec![L7Rule { allow: Some(allow) }], + ..Default::default() + }], + binaries: vec![], + }, + )]), + ..Default::default() + }; + let projected: serde_json::Value = serde_json::from_str(&proto_to_opa_data_json(&proto, 0)) + .expect("protobuf policy projection must produce JSON"); + projected["network_policies"]["selector_projection"]["endpoints"][0]["rules"][0]["allow"] + .as_object() + .expect("projected allow rule must be an object") + .clone() + } + + #[test] + fn proto_projection_omits_absent_allow_selectors() { + let allow = projected_allow( + "websocket", + L7Allow { + operation_type: "subscription".to_string(), + operation_name: "NewMessages".to_string(), + fields: vec!["messageAdded".to_string()], + ..Default::default() + }, + ); + + assert_eq!( + allow, + serde_json::json!({ + "operation_type": "subscription", + "operation_name": "NewMessages", + "fields": ["messageAdded"], + }) + .as_object() + .expect("expected object") + .clone() + ); + } + + #[test] + fn proto_projection_preserves_nonempty_allow_selectors() { + let allow = projected_allow( + "rest", + L7Allow { + method: "POST".to_string(), + path: "/repos/**".to_string(), + ..Default::default() + }, + ); + + assert_eq!( + allow, + serde_json::json!({ + "method": "POST", + "path": "/repos/**", + }) + .as_object() + .expect("expected object") + .clone() + ); + } + const POLICY_DNS_SNAPSHOT_DATA: &str = r#" network_policies: dns_transport: