feat(host): decode operation arguments in dispatch boundary
This commit is contained in:
+105
-18
@@ -535,11 +535,36 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
context: &ExecutionContext,
|
||||
request: CallRequest,
|
||||
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
|
||||
) -> OperationAttempt {
|
||||
let CallRequest {
|
||||
target,
|
||||
interface,
|
||||
operation,
|
||||
arguments,
|
||||
} = request;
|
||||
self.dispatch_resolved_with_decoder(
|
||||
context,
|
||||
target,
|
||||
interface,
|
||||
operation,
|
||||
|_| Ok::<_, std::convert::Infallible>(arguments),
|
||||
authorize,
|
||||
)
|
||||
}
|
||||
|
||||
fn dispatch_resolved_with_decoder<E>(
|
||||
&self,
|
||||
context: &ExecutionContext,
|
||||
target: Target,
|
||||
interface: InterfaceTarget,
|
||||
operation: String,
|
||||
decode_arguments: impl FnOnce(&InterfaceDescriptor) -> Result<BTreeMap<String, Value>, E>,
|
||||
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
|
||||
) -> OperationAttempt {
|
||||
let pre_dispatch_failure =
|
||||
|kind, message: &str| OperationAttempt::NotStarted(CallError::new(kind, message));
|
||||
let current_object_validator = self.implementation.validator();
|
||||
if let Some(supplied) = request.target.validator.as_deref()
|
||||
if let Some(supplied) = target.validator.as_deref()
|
||||
&& current_object_validator.map(Validator::as_bytes) != Some(supplied)
|
||||
{
|
||||
return pre_dispatch_failure(
|
||||
@@ -548,9 +573,11 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
);
|
||||
}
|
||||
|
||||
let Some(bound) = self.interfaces.iter().find(|bound| {
|
||||
bound.artifact.reference.as_str() == request.interface.reference.as_str()
|
||||
}) else {
|
||||
let Some(bound) = self
|
||||
.interfaces
|
||||
.iter()
|
||||
.find(|bound| bound.artifact.reference.as_str() == interface.reference.as_str())
|
||||
else {
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::InterfaceMismatch,
|
||||
"interface is not a member of the current object publication",
|
||||
@@ -559,7 +586,7 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
|
||||
match (
|
||||
bound.artifact.validator.as_ref(),
|
||||
request.interface.validator.as_deref(),
|
||||
interface.validator.as_deref(),
|
||||
) {
|
||||
(Some(_), None) => {
|
||||
return pre_dispatch_failure(
|
||||
@@ -581,18 +608,18 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
.descriptor
|
||||
.operations
|
||||
.iter()
|
||||
.any(|operation| operation.name == request.operation)
|
||||
.any(|declared| declared.name == operation)
|
||||
{
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::OperationNotFound,
|
||||
&format!("operation `{}` is not declared", request.operation),
|
||||
&format!("operation `{operation}` is not declared"),
|
||||
);
|
||||
}
|
||||
|
||||
if request.target.validator.is_none()
|
||||
if target.validator.is_none()
|
||||
&& self
|
||||
.implementation
|
||||
.requires_validator(&bound.artifact.reference, &request.operation)
|
||||
.requires_validator(&bound.artifact.reference, &operation)
|
||||
{
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::ValidatorRequired,
|
||||
@@ -600,19 +627,24 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
);
|
||||
}
|
||||
|
||||
let arguments = match decode_arguments(&bound.artifact.descriptor) {
|
||||
Ok(arguments) => arguments,
|
||||
Err(_) => {
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::InvalidArguments,
|
||||
"operation arguments could not be decoded",
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Err(error) = bound
|
||||
.artifact
|
||||
.descriptor
|
||||
.validate_arguments(&request.operation, &request.arguments)
|
||||
.validate_arguments(&operation, &arguments)
|
||||
{
|
||||
return pre_dispatch_failure(CallErrorKind::InvalidArguments, &error.to_string());
|
||||
}
|
||||
|
||||
if !authorize(
|
||||
&self.implementation,
|
||||
&bound.artifact.reference,
|
||||
&request.operation,
|
||||
) {
|
||||
if !authorize(&self.implementation, &bound.artifact.reference, &operation) {
|
||||
return pre_dispatch_failure(
|
||||
CallErrorKind::PermissionDenied,
|
||||
"operation is not permitted for this caller",
|
||||
@@ -623,8 +655,7 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
return OperationAttempt::NotStarted(error);
|
||||
}
|
||||
|
||||
let value =
|
||||
match (bound.dispatch)(&self.implementation, &request.operation, request.arguments) {
|
||||
let value = match (bound.dispatch)(&self.implementation, &operation, arguments) {
|
||||
Ok(value) => value,
|
||||
Err(
|
||||
error @ (DispatchError::UnknownOperation(_)
|
||||
@@ -639,7 +670,7 @@ impl<T: WipObject> PublishedObject<T> {
|
||||
if bound
|
||||
.artifact
|
||||
.descriptor
|
||||
.validate_result(&request.operation, &value)
|
||||
.validate_result(&operation, &value)
|
||||
.is_err()
|
||||
{
|
||||
return OperationAttempt::Started(Err(OperationFailure::internal(
|
||||
@@ -922,6 +953,62 @@ pub trait ObjectProvider {
|
||||
ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves one operation target and decodes its arguments only after all
|
||||
/// descriptor-independent canonical preconditions have succeeded.
|
||||
///
|
||||
/// Transport bindings whose value representation depends on the selected
|
||||
/// interface descriptor should use this method instead of resolving a
|
||||
/// descriptor before [`ObjectProvider::call_operation_with_context`]. The
|
||||
/// callback executes inside the same exact-once provider boundary after
|
||||
/// object/interface validators, membership, operation lookup, and required
|
||||
/// object-validator checks. A callback error becomes `InvalidArguments` and
|
||||
/// typed implementation code is not invoked.
|
||||
fn call_operation_with_context_and_decoder<E>(
|
||||
&self,
|
||||
context: &ExecutionContext,
|
||||
target: Target,
|
||||
interface: InterfaceTarget,
|
||||
operation: String,
|
||||
decode_arguments: impl FnOnce(&InterfaceDescriptor) -> Result<BTreeMap<String, Value>, E>,
|
||||
) -> Result<CallResult, CallError> {
|
||||
wip_protocol::validate_path(&target.path)
|
||||
.map_err(|error| CallError::new(CallErrorKind::InvalidRequest, error.to_string()))?;
|
||||
let path = target.path.clone();
|
||||
let execution = catch_unwind(AssertUnwindSafe(|| {
|
||||
self.execute_operation(context, &path, |publication| {
|
||||
publication.dispatch_resolved_with_decoder(
|
||||
context,
|
||||
target,
|
||||
interface,
|
||||
operation,
|
||||
decode_arguments,
|
||||
|object, interface, operation| {
|
||||
self.authorize(&path, object, interface, operation)
|
||||
},
|
||||
)
|
||||
})
|
||||
}));
|
||||
|
||||
let execution = execution.map_err(|_| {
|
||||
CallError::from_operation_failure(OperationFailure::internal(
|
||||
EffectGuarantee::Unknown,
|
||||
"provider execution panicked",
|
||||
))
|
||||
})?;
|
||||
match execution {
|
||||
ProviderResult::Found(OperationAttempt::NotStarted(error)) => Err(error),
|
||||
ProviderResult::Found(OperationAttempt::Started(Ok(result))) => Ok(result),
|
||||
ProviderResult::Found(OperationAttempt::Started(Err(error))) => {
|
||||
Err(CallError::from_operation_failure(error))
|
||||
}
|
||||
ProviderResult::NotFound => Err(CallError::new(
|
||||
CallErrorKind::NotFound,
|
||||
"target path is not published",
|
||||
)),
|
||||
ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Failure to construct an explicit object publication.
|
||||
|
||||
@@ -741,6 +741,89 @@ fn call_failures_follow_canonical_precedence_without_execution() {
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn descriptor_guided_decoder_runs_inside_canonical_resolution_boundary() {
|
||||
let (_, provider, hidden) = registry_and_provider();
|
||||
let decodes = Cell::new(0_u32);
|
||||
let decode_failure = || {
|
||||
decodes.set(decodes.get() + 1);
|
||||
Err::<BTreeMap<String, Value>, _>(())
|
||||
};
|
||||
|
||||
let missing = provider
|
||||
.call_operation_with_context_and_decoder(
|
||||
&ExecutionContext::new(),
|
||||
Target {
|
||||
path: "/missing".into(),
|
||||
validator: Some(b"stale".to_vec()),
|
||||
},
|
||||
InterfaceTarget {
|
||||
reference: hidden.reference().as_str().into(),
|
||||
validator: None,
|
||||
},
|
||||
"missing".into(),
|
||||
|_| decode_failure(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(missing.kind(), CallErrorKind::NotFound);
|
||||
assert_eq!(decodes.get(), 0);
|
||||
|
||||
let stale = provider
|
||||
.call_operation_with_context_and_decoder(
|
||||
&ExecutionContext::new(),
|
||||
Target {
|
||||
path: "/document".into(),
|
||||
validator: Some(b"stale".to_vec()),
|
||||
},
|
||||
InterfaceTarget {
|
||||
reference: hidden.reference().as_str().into(),
|
||||
validator: None,
|
||||
},
|
||||
"hidden".into(),
|
||||
|_| decode_failure(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(stale.kind(), CallErrorKind::ValidatorMismatch);
|
||||
assert_eq!(decodes.get(), 0);
|
||||
|
||||
let mismatch = provider
|
||||
.call_operation_with_context_and_decoder(
|
||||
&ExecutionContext::new(),
|
||||
Target {
|
||||
path: "/document".into(),
|
||||
validator: Some(b"object-v1".to_vec()),
|
||||
},
|
||||
InterfaceTarget {
|
||||
reference: hidden.reference().as_str().into(),
|
||||
validator: None,
|
||||
},
|
||||
"hidden".into(),
|
||||
|_| decode_failure(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(mismatch.kind(), CallErrorKind::InterfaceMismatch);
|
||||
assert_eq!(decodes.get(), 0);
|
||||
|
||||
let invalid_arguments = provider
|
||||
.call_operation_with_context_and_decoder(
|
||||
&ExecutionContext::new(),
|
||||
Target {
|
||||
path: "/document".into(),
|
||||
validator: Some(b"object-v1".to_vec()),
|
||||
},
|
||||
InterfaceTarget {
|
||||
reference: provider.reader.reference().as_str().into(),
|
||||
validator: Some(b"schema-v1".to_vec()),
|
||||
},
|
||||
"add".into(),
|
||||
|_| decode_failure(),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert_eq!(invalid_arguments.kind(), CallErrorKind::InvalidArguments);
|
||||
assert_eq!(decodes.get(), 1);
|
||||
assert_eq!(provider.calls.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn immutable_interface_rejects_a_supplied_validator() {
|
||||
let (_, provider, _) = registry_and_provider();
|
||||
|
||||
Reference in New Issue
Block a user