Skip to content

Commit dd3f3cd

Browse files
committed
fix(policy): carry L7 rule params through the proto load path
proto_to_opa_data_json dropped the per-rule params matcher map for both allow and deny rules, so MCP tools/call rules narrowed by params.name degraded to allow-any-tool on the production proto load path (the YAML path enforced correctly). Emit params alongside query for both rule kinds and add a from_proto regression test. Signed-off-by: Kris Hicks <khicks@nvidia.com>
1 parent 91c5767 commit dd3f3cd

1 file changed

Lines changed: 101 additions & 0 deletions

File tree

  • crates/openshell-supervisor-network/src

crates/openshell-supervisor-network/src/opa.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,6 +1200,12 @@ fn proto_to_opa_data_json(proto: &ProtoSandboxPolicy, entrypoint_pid: u32) -> St
12001200
if !query.is_empty() {
12011201
allow["query"] = query.into();
12021202
}
1203+
let params = a.map_or_else(serde_json::Map::new, |allow| {
1204+
l7_matchers_to_json(&allow.params)
1205+
});
1206+
if !params.is_empty() {
1207+
allow["params"] = params.into();
1208+
}
12031209
serde_json::json!({ "allow": allow })
12041210
})
12051211
.collect();
@@ -1239,6 +1245,10 @@ fn proto_to_opa_data_json(proto: &ProtoSandboxPolicy, entrypoint_pid: u32) -> St
12391245
if !query.is_empty() {
12401246
deny["query"] = query.into();
12411247
}
1248+
let params = l7_matchers_to_json(&d.params);
1249+
if !params.is_empty() {
1250+
deny["params"] = params.into();
1251+
}
12421252
deny
12431253
})
12441254
.collect();
@@ -2843,6 +2853,97 @@ network_policies:
28432853
assert!(!eval_l7(&engine, &deny_input));
28442854
}
28452855

2856+
#[test]
2857+
fn l7_mcp_tool_params_from_proto_are_enforced() {
2858+
// Regression: the proto load path (from_proto) must carry the rule
2859+
// `params` matcher map. If it is dropped, a tools/call allow rule
2860+
// narrowed to one tool degrades to allow-any-tool in production, even
2861+
// though the YAML/add_data_json path enforces it correctly.
2862+
let mut params = std::collections::HashMap::new();
2863+
params.insert(
2864+
"name".to_string(),
2865+
L7QueryMatcher {
2866+
glob: String::new(),
2867+
any: vec!["read_status".to_string(), "submit_*".to_string()],
2868+
},
2869+
);
2870+
2871+
let mut network_policies = std::collections::HashMap::new();
2872+
network_policies.insert(
2873+
"mcp_proto".to_string(),
2874+
NetworkPolicyRule {
2875+
name: "mcp_proto".to_string(),
2876+
endpoints: vec![NetworkEndpoint {
2877+
host: "mcp.proto.com".to_string(),
2878+
port: 8000,
2879+
path: "/mcp".to_string(),
2880+
protocol: "mcp".to_string(),
2881+
enforcement: "enforce".to_string(),
2882+
rules: vec![L7Rule {
2883+
allow: Some(L7Allow {
2884+
method: "tools/call".to_string(),
2885+
path: String::new(),
2886+
command: String::new(),
2887+
query: std::collections::HashMap::new(),
2888+
operation_type: String::new(),
2889+
operation_name: String::new(),
2890+
fields: Vec::new(),
2891+
params,
2892+
}),
2893+
}],
2894+
..Default::default()
2895+
}],
2896+
binaries: vec![NetworkBinary {
2897+
path: "/usr/bin/curl".to_string(),
2898+
..Default::default()
2899+
}],
2900+
},
2901+
);
2902+
2903+
let proto = ProtoSandboxPolicy {
2904+
version: 1,
2905+
filesystem: Some(ProtoFs {
2906+
include_workdir: true,
2907+
read_only: vec![],
2908+
read_write: vec![],
2909+
}),
2910+
landlock: Some(openshell_core::proto::LandlockPolicy {
2911+
compatibility: "best_effort".to_string(),
2912+
}),
2913+
process: Some(ProtoProc {
2914+
run_as_user: "sandbox".to_string(),
2915+
run_as_group: "sandbox".to_string(),
2916+
}),
2917+
network_policies,
2918+
};
2919+
2920+
let engine = OpaEngine::from_proto(&proto).expect("engine from proto");
2921+
2922+
let allowed_tool = l7_jsonrpc_input_with_params(
2923+
"mcp.proto.com",
2924+
8000,
2925+
"/mcp",
2926+
"tools/call",
2927+
serde_json::json!({ "name": "read_status" }),
2928+
);
2929+
assert!(
2930+
eval_l7(&engine, &allowed_tool),
2931+
"tools/call for an allowed tool should be permitted"
2932+
);
2933+
2934+
let blocked_tool = l7_jsonrpc_input_with_params(
2935+
"mcp.proto.com",
2936+
8000,
2937+
"/mcp",
2938+
"tools/call",
2939+
serde_json::json!({ "name": "blocked_action" }),
2940+
);
2941+
assert!(
2942+
!eval_l7(&engine, &blocked_tool),
2943+
"tools/call for a non-matching tool must be denied (params matcher must survive the proto load path)"
2944+
);
2945+
}
2946+
28462947
#[test]
28472948
fn l7_jsonrpc_endpoint_ignores_rest_shaped_allow_rules() {
28482949
let data = serde_json::json!({

0 commit comments

Comments
 (0)