Skip to content

Commit d43582b

Browse files
committed
test(mcp): cover relay deny by tool
Signed-off-by: ddurst <267424412+ddurst-nvidia@users.noreply.github.com>
1 parent ea2d942 commit d43582b

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

  • crates/openshell-supervisor-network/src/l7

crates/openshell-supervisor-network/src/l7/relay.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2285,6 +2285,82 @@ network_policies:
22852285
assert!(!message.contains("purge_cache"));
22862286
}
22872287

2288+
#[test]
2289+
fn mcp_tool_deny_rule_blocks_tools_call() {
2290+
let data = r#"
2291+
network_policies:
2292+
mcp_api:
2293+
name: mcp_api
2294+
endpoints:
2295+
- host: api.example.test
2296+
port: 443
2297+
path: "/mcp"
2298+
protocol: mcp
2299+
enforcement: enforce
2300+
mcp:
2301+
max_body_bytes: 131072
2302+
rules:
2303+
deny:
2304+
- mcp_method: tools/call
2305+
tool: delete_resource
2306+
allow:
2307+
- mcp_method: initialize
2308+
- mcp_method: tools/list
2309+
- mcp_method: tools/call
2310+
tool: read_status
2311+
binaries:
2312+
- { path: /usr/bin/node }
2313+
"#;
2314+
let engine = OpaEngine::from_strings(TEST_POLICY, data).unwrap();
2315+
let tunnel_engine = engine
2316+
.clone_engine_for_tunnel(engine.current_generation())
2317+
.unwrap();
2318+
let ctx = L7EvalContext {
2319+
host: "api.example.test".into(),
2320+
port: 443,
2321+
policy_name: "mcp_api".into(),
2322+
binary_path: "/usr/bin/node".into(),
2323+
ancestors: vec![],
2324+
cmdline_paths: vec![],
2325+
secret_resolver: None,
2326+
activity_tx: None,
2327+
dynamic_credentials: None,
2328+
token_grant_resolver: None,
2329+
};
2330+
let mut request = L7RequestInfo {
2331+
action: "POST".into(),
2332+
target: "/mcp".into(),
2333+
query_params: std::collections::HashMap::new(),
2334+
graphql: None,
2335+
jsonrpc: Some(crate::l7::jsonrpc::parse_mcp_body(
2336+
br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"read_status","arguments":{}}}"#,
2337+
)),
2338+
};
2339+
2340+
let (allowed, reason) = evaluate_l7_request(&tunnel_engine, &ctx, &request).unwrap();
2341+
assert!(allowed, "{reason}");
2342+
2343+
request.jsonrpc = Some(crate::l7::jsonrpc::parse_mcp_body(
2344+
br#"{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_resource","arguments":{"scope":"workspace/main"}}}"#,
2345+
));
2346+
let parsed = request.jsonrpc.as_ref().expect("parsed MCP request");
2347+
assert!(
2348+
parsed.error.is_none(),
2349+
"MCP request should parse: {parsed:?}"
2350+
);
2351+
assert_eq!(
2352+
parsed.calls.first().and_then(|call| call.tool.as_deref()),
2353+
Some("delete_resource")
2354+
);
2355+
2356+
let (allowed, reason) = evaluate_l7_request(&tunnel_engine, &ctx, &request).unwrap();
2357+
assert!(!allowed, "delete_resource must match the MCP deny rule");
2358+
assert!(
2359+
reason.contains("deny rule"),
2360+
"deny reason should identify policy denial: {reason}"
2361+
);
2362+
}
2363+
22882364
#[test]
22892365
fn jsonrpc_log_records_digest_not_args() {
22902366
let info = crate::l7::jsonrpc::parse_jsonrpc_body(

0 commit comments

Comments
 (0)