mirror of
https://github.com/ModernRelay/omnigraph.git
synced 2026-06-30 02:49:39 +02:00
Scope the stored-query 404-hiding claim to non-invoke_query callers
Review found the deny==404 catalog-hiding was overstated as a contract: it holds only at the outer invoke_query gate. A caller that HOLDS invoke_query but lacks read/change gets the inner gate's 403 for an existing query vs 404 for an unknown one — so existence is visible to grant-holders by design (the intended double-gate). The handler docstring, OpenAPI 404 description, and server.md all claimed the 404 was airtight against any denied actor. Correct the wording in all three (no behavior change) and add the missing symmetric test (invoke_query but no read -> 403 for an existing query, 404 for unknown) so the actual contract is pinned. Also document that in default-deny mode (tokens, no policy) every invocation 404s until an invoke_query rule is configured. Nits: the from_specs collision comment said "first declared wins" but it is lexicographically-first by name (BTreeMap); the effective_tool_name docstring overclaimed the CLI display routes through it (it resolves the rule on its own output DTO).
This commit is contained in:
parent
566e9b7651
commit
f4c38bb75a
5 changed files with 58 additions and 15 deletions
|
|
@ -239,12 +239,14 @@ async fn app_with_stored_queries(
|
|||
/// - `act-invoke`: invoke_query + read (stored reads, not mutations)
|
||||
/// - `act-full`: invoke_query + read + change (stored mutations)
|
||||
/// - `act-noinvoke`: read only, no invoke_query (boundary-denied)
|
||||
/// - `act-invokeonly`: invoke_query only, no read (clears the boundary, inner read denies)
|
||||
const INVOKE_POLICY_YAML: &str = r#"
|
||||
version: 1
|
||||
groups:
|
||||
invokers: ["act-invoke"]
|
||||
full: ["act-full"]
|
||||
readers: ["act-noinvoke"]
|
||||
invoke_only: ["act-invokeonly"]
|
||||
protected_branches: [main]
|
||||
rules:
|
||||
- id: invokers-invoke-and-read
|
||||
|
|
@ -262,6 +264,11 @@ rules:
|
|||
actors: { group: readers }
|
||||
actions: [read]
|
||||
branch_scope: any
|
||||
- id: invoke-only-no-read
|
||||
allow:
|
||||
actors: { group: invoke_only }
|
||||
actions: [invoke_query]
|
||||
branch_scope: any
|
||||
"#;
|
||||
|
||||
const FIND_PERSON_GQ: &str =
|
||||
|
|
@ -380,6 +387,34 @@ async fn invoke_unknown_query_and_denied_actor_return_identical_404() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn invoke_query_holder_without_read_sees_403_not_404() {
|
||||
// The 404-hiding is for callers WITHOUT invoke_query. An actor that
|
||||
// HOLDS invoke_query but lacks `read` clears the boundary gate, then the
|
||||
// inner read gate denies → 403 for an EXISTING read query, vs 404 for an
|
||||
// unknown one. Existence is visible to grant-holders by design (the
|
||||
// documented double-gate); this pins that actual contract.
|
||||
let (_temp, app) = app_with_stored_queries(
|
||||
&[("find_person", FIND_PERSON_GQ, false)],
|
||||
&[("act-invokeonly", "t-invokeonly")],
|
||||
INVOKE_POLICY_YAML,
|
||||
)
|
||||
.await;
|
||||
let (exists_status, _) = json_response(
|
||||
&app,
|
||||
invoke_request("find_person", "t-invokeonly", json!({ "params": { "name": "Alice" } })),
|
||||
)
|
||||
.await;
|
||||
let (absent_status, _) =
|
||||
json_response(&app, invoke_request("does_not_exist", "t-invokeonly", json!({}))).await;
|
||||
assert_eq!(
|
||||
exists_status,
|
||||
StatusCode::FORBIDDEN,
|
||||
"an existing read query the holder can't read → inner-gate 403"
|
||||
);
|
||||
assert_eq!(absent_status, StatusCode::NOT_FOUND, "unknown query still 404s");
|
||||
}
|
||||
|
||||
fn drifted_test_schema() -> String {
|
||||
fs::read_to_string(fixture("test.pg"))
|
||||
.unwrap()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue