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
This commit is contained in:
1 parent
9899b37a82
commit
1fda0d8587
1 file changed
+75
-22
+75
-22
@@ -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<CallError> {
|
||||
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<dyn OwnershipProvider>)
|
||||
.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();
|
||||
|
||||
Reference in new issue
Block a user