From 1fda0d858742099ba363301893966b4f6e7e6876 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 17 Oct 2026 14:17:06 +0000 Subject: [PATCH] fix(gateway): review-012 regression-pass cleanup on the loud ownership default - deduplicate the thrice-repeated refusal block into GatewayDispatch::ownership_wiring_refusal(&self, op) - drop the dead unwrap_or("") branch in the refusal predicate (let-else: the internal-visibility exemption and the resource_type predicate are now separate lets) - pin the predicate refinement review 012 ruled on: a list-case op (resource_type set, resource_id_path None) is refused on an unwired spine and served through owns_any when a provider is wired Verification: cargo test (697), cargo test --all-features (721), clippy all-targets both configs, fmt --check --- src/gateway/dispatch.rs | 97 +++++++++++++++++++++++++++++++---------- 1 file changed, 75 insertions(+), 22 deletions(-) diff --git a/src/gateway/dispatch.rs b/src/gateway/dispatch.rs index 2f92ab2..10e7ceb 100644 --- a/src/gateway/dispatch.rs +++ b/src/gateway/dispatch.rs @@ -151,12 +151,7 @@ impl GatewayDispatch { return ResponseEnvelope::error(uuid::Uuid::new_v4().to_string(), error); } let request_id = uuid::Uuid::new_v4().to_string(); - if let Some(error) = ownership_wiring_refusal( - &self.registry, - &operation_name, - self.ownership_provider.as_ref(), - self.static_ownership_fallback, - ) { + if let Some(error) = self.ownership_wiring_refusal(&operation_name) { return ResponseEnvelope::error(request_id, error); } let context = self.build_root_context(&request_id, &operation_name, identity); @@ -194,12 +189,7 @@ impl GatewayDispatch { })); } let request_id = uuid::Uuid::new_v4().to_string(); - if let Some(error) = ownership_wiring_refusal( - &self.registry, - &operation_name, - self.ownership_provider.as_ref(), - self.static_ownership_fallback, - ) { + if let Some(error) = self.ownership_wiring_refusal(&operation_name) { return Box::pin(futures::stream::once(async move { ResponseEnvelope::error(request_id, error) })); @@ -224,12 +214,7 @@ impl GatewayDispatch { ) -> ResponseEnvelope { let operation_name = strip_leading_slash(op).to_string(); let request_id = uuid::Uuid::new_v4().to_string(); - if let Some(error) = ownership_wiring_refusal( - &self.registry, - &operation_name, - self.ownership_provider.as_ref(), - self.static_ownership_fallback, - ) { + if let Some(error) = self.ownership_wiring_refusal(&operation_name) { return ResponseEnvelope::error(request_id, error); } let context = self.build_root_context_sink(&request_id, &operation_name, identity); @@ -250,6 +235,15 @@ impl GatewayDispatch { } } + fn ownership_wiring_refusal(&self, operation: &str) -> Option { + ownership_wiring_refusal( + &self.registry, + operation, + self.ownership_provider.as_ref(), + self.static_ownership_fallback, + ) + } + fn build_root_context_sink( &self, request_id: &str, @@ -368,12 +362,11 @@ fn ownership_wiring_refusal( if ownership_provider.is_some() || static_ownership_fallback { return None; } - let registration = registry.registration(operation)?; - let spec = ®istration.spec; - if spec.visibility == Visibility::Internal || spec.access_control.resource_type.is_none() { + let spec = ®istry.registration(operation)?.spec; + if spec.visibility == Visibility::Internal { return None; } - let resource_type = spec.access_control.resource_type.as_deref().unwrap_or(""); + let resource_type = spec.access_control.resource_type.as_deref()?; Some(CallError::forbidden(format!( "operation '{operation}' gates on resource ownership ({resource_type}), but no ownership \ provider is wired on this dispatch spine; the deployment must wire \ @@ -1146,6 +1139,66 @@ mod tests { } } + #[tokio::test] + async fn gateway_list_case_resource_gated_ops_are_refused_unwired_and_served_via_owns_any() { + let registry = OperationRegistry::new(); + let mut list_spec = spec("ns/list", Visibility::External, OperationType::Query); + list_spec.access_control = namespace_acl(); + registry + .register(HandlerRegistration::new( + list_spec, + HandlerKind::Once(make_handler(|input, ctx| async move { + ResponseEnvelope::ok(ctx.request_id, input) + })), + OperationProvenance::Local, + None, + None, + Capabilities::new(), + )) + .unwrap(); + let registry = Arc::new(registry); + + let envelope = GatewayDispatch::new(Arc::clone(®istry)) + .invoke( + Some(identity_with_static_claim("token-carrier", true)), + "ns/list", + serde_json::json!({}), + ) + .await; + match envelope.result { + Err(error) => assert_eq!( + error.code, "FORBIDDEN", + "the list case (resource_id_path None) is refused unwired too, got {error:?}" + ), + Ok(v) => panic!( + "a token-carried static claim must not admit the list case on an unwired spine, \ + got {v:?}" + ), + } + + let store = Arc::new(InMemoryOwnershipStore::new()); + store + .record( + &identity_with_static_claim("tenant-a", false), + "blob-namespace", + "ns-9", + ) + .await + .unwrap(); + let envelope = GatewayDispatch::new(Arc::clone(®istry)) + .with_ownership_provider(Arc::clone(&store) as Arc) + .invoke( + Some(identity_with_static_claim("tenant-a", false)), + "ns/list", + serde_json::json!({}), + ) + .await; + assert!( + envelope.result.is_ok(), + "the wired spine serves the list case through owns_any, got {envelope:?}" + ); + } + #[tokio::test] async fn gateway_static_fallback_opt_out_serves_resource_gated_ops() { let dispatch = GatewayDispatch::new(namespace_registry()).with_static_ownership_fallback();