mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-30 02:49:39 +02:00
fix(server): align stored-query MCP discovery gates
This commit is contained in:
parent
c06343362a
commit
916dc46c0e
13 changed files with 392 additions and 80 deletions
|
|
@ -13,9 +13,9 @@ use omnigraph_server::queries::{QueryRegistry, RegistrySpec};
|
|||
use omnigraph_server::{AppState, build_app};
|
||||
use serde_json::{Value, json};
|
||||
use support::{
|
||||
FIND_PERSON_GQ, INVOKE_POLICY_YAML, app_for_loaded_graph_with_auth_tokens,
|
||||
app_for_loaded_graph_with_auth_tokens_and_policy, app_with_stored_queries, g, graph_path,
|
||||
init_loaded_graph, json_response,
|
||||
FIND_PERSON_GQ, INVOKE_POLICY_YAML, POLICY_PROTECTED_READ_YAML,
|
||||
app_for_loaded_graph_with_auth_tokens, app_for_loaded_graph_with_auth_tokens_and_policy,
|
||||
app_with_stored_queries, g, graph_path, init_loaded_graph, json_response,
|
||||
};
|
||||
|
||||
/// Build a JSON-RPC POST to `/graphs/default/mcp`. Sets the `Accept` (both
|
||||
|
|
@ -577,6 +577,40 @@ async fn stored_query_tool_folds_docs_and_honors_mcp_annotation() {
|
|||
assert_ne!(v["result"]["isError"], json!(true), "renamed tool not callable: {v}");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn list_gate_matches_call_for_fixed_branchless_reads() {
|
||||
// A protected-only reader. Branch-arg reads (graph_query) relax and show
|
||||
// (callable on a protected branch). Fixed branchless reads (schema_get) use
|
||||
// the faithful read(None) gate — which a protected-scope rule denies — so
|
||||
// schema_get is hidden, matching the omnigraph://schema resource (same
|
||||
// branchless read). Tool and resource agree by construction.
|
||||
let (_t, app) = app_for_loaded_graph_with_auth_tokens_and_policy(
|
||||
&[("act-bruno", "tok")],
|
||||
POLICY_PROTECTED_READ_YAML,
|
||||
)
|
||||
.await;
|
||||
|
||||
let (_s, list) =
|
||||
json_response(&app, mcp_request(Some("tok"), rpc(1, "tools/list", json!({})))).await;
|
||||
let names = tool_names(&list);
|
||||
assert!(
|
||||
names.contains(&"graph_query".to_string()),
|
||||
"branch-arg read relaxes and shows under protected-only read: {names:?}"
|
||||
);
|
||||
assert!(
|
||||
!names.contains(&"schema_get".to_string()),
|
||||
"fixed branchless read uses read(None), denied under protected-only → hidden: {names:?}"
|
||||
);
|
||||
|
||||
// resources/list uses the same read(None) gate → empty, matching schema_get.
|
||||
let (_s, res) =
|
||||
json_response(&app, mcp_request(Some("tok"), rpc(2, "resources/list", json!({})))).await;
|
||||
assert!(
|
||||
res["result"]["resources"].as_array().unwrap().is_empty(),
|
||||
"resources hidden under protected-only read, consistent with schema_get: {res}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn per_query_mode_does_not_expose_meta_tools() {
|
||||
// Below the auto threshold the projection is per-query, so the discovery +
|
||||
|
|
|
|||
|
|
@ -345,28 +345,29 @@ async fn list_queries_returns_only_exposed_with_typed_params() {
|
|||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn list_queries_is_read_gated_so_a_non_invoker_can_list() {
|
||||
// The catalog is read-gated (not invoke_query-gated), so a reader who
|
||||
// lacks invoke_query still enumerates the exposed queries — the
|
||||
// documented probe-oracle gap until per-query Cedar filtering lands.
|
||||
async fn list_queries_requires_invoke_query() {
|
||||
// The catalog is invoke_query-gated (same authority as invocation and the
|
||||
// MCP `tools/list` surface): a reader who lacks invoke_query is denied
|
||||
// listing, while an invoke_query holder lists it.
|
||||
let (_temp, app) = app_with_stored_queries(
|
||||
&[("find_person", FIND_PERSON_GQ, true)],
|
||||
&[("act-noinvoke", "t-noinvoke")],
|
||||
&[("act-noinvoke", "t-noinvoke"), ("act-invoke", "t-invoke")],
|
||||
INVOKE_POLICY_YAML,
|
||||
)
|
||||
.await;
|
||||
let (status, body) = json_response(&app, get_request(&g("/queries"), "t-noinvoke")).await;
|
||||
assert_eq!(status, StatusCode::OK, "read-gated catalog; body: {body}");
|
||||
// read-only, no invoke_query → 403.
|
||||
let (status, _body) = json_response(&app, get_request(&g("/queries"), "t-noinvoke")).await;
|
||||
assert_eq!(status, StatusCode::FORBIDDEN, "catalog listing requires invoke_query");
|
||||
// invoke_query holder → 200 with the exposed query.
|
||||
let (status, body) = json_response(&app, get_request(&g("/queries"), "t-invoke")).await;
|
||||
assert_eq!(status, StatusCode::OK, "invoker lists the catalog; body: {body}");
|
||||
let names: Vec<&str> = body["queries"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|q| q["name"].as_str().unwrap())
|
||||
.collect();
|
||||
assert!(
|
||||
names.contains(&"find_person"),
|
||||
"a reader lists the catalog despite lacking invoke_query: {names:?}"
|
||||
);
|
||||
assert!(names.contains(&"find_person"), "invoker sees the exposed query: {names:?}");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
|
|
|||
|
|
@ -362,6 +362,10 @@ rules:
|
|||
actors: {{ group: permitted }}
|
||||
actions: [schema_apply, branch_create, branch_delete, branch_merge]
|
||||
target_branch_scope: any
|
||||
- id: permit-invoke
|
||||
allow:
|
||||
actors: {{ group: permitted }}
|
||||
actions: [invoke_query]
|
||||
"#
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue