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,
|
context: &ExecutionContext,
|
||||||
request: CallRequest,
|
request: CallRequest,
|
||||||
authorize: impl FnOnce(&T, &InterfaceRef, &str) -> bool,
|
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 {
|
) -> OperationAttempt {
|
||||||
let pre_dispatch_failure =
|
let pre_dispatch_failure =
|
||||||
|kind, message: &str| OperationAttempt::NotStarted(CallError::new(kind, message));
|
|kind, message: &str| OperationAttempt::NotStarted(CallError::new(kind, message));
|
||||||
let current_object_validator = self.implementation.validator();
|
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)
|
&& current_object_validator.map(Validator::as_bytes) != Some(supplied)
|
||||||
{
|
{
|
||||||
return pre_dispatch_failure(
|
return pre_dispatch_failure(
|
||||||
@@ -548,9 +573,11 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(bound) = self.interfaces.iter().find(|bound| {
|
let Some(bound) = self
|
||||||
bound.artifact.reference.as_str() == request.interface.reference.as_str()
|
.interfaces
|
||||||
}) else {
|
.iter()
|
||||||
|
.find(|bound| bound.artifact.reference.as_str() == interface.reference.as_str())
|
||||||
|
else {
|
||||||
return pre_dispatch_failure(
|
return pre_dispatch_failure(
|
||||||
CallErrorKind::InterfaceMismatch,
|
CallErrorKind::InterfaceMismatch,
|
||||||
"interface is not a member of the current object publication",
|
"interface is not a member of the current object publication",
|
||||||
@@ -559,7 +586,7 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
|
|
||||||
match (
|
match (
|
||||||
bound.artifact.validator.as_ref(),
|
bound.artifact.validator.as_ref(),
|
||||||
request.interface.validator.as_deref(),
|
interface.validator.as_deref(),
|
||||||
) {
|
) {
|
||||||
(Some(_), None) => {
|
(Some(_), None) => {
|
||||||
return pre_dispatch_failure(
|
return pre_dispatch_failure(
|
||||||
@@ -581,18 +608,18 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
.descriptor
|
.descriptor
|
||||||
.operations
|
.operations
|
||||||
.iter()
|
.iter()
|
||||||
.any(|operation| operation.name == request.operation)
|
.any(|declared| declared.name == operation)
|
||||||
{
|
{
|
||||||
return pre_dispatch_failure(
|
return pre_dispatch_failure(
|
||||||
CallErrorKind::OperationNotFound,
|
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
|
&& self
|
||||||
.implementation
|
.implementation
|
||||||
.requires_validator(&bound.artifact.reference, &request.operation)
|
.requires_validator(&bound.artifact.reference, &operation)
|
||||||
{
|
{
|
||||||
return pre_dispatch_failure(
|
return pre_dispatch_failure(
|
||||||
CallErrorKind::ValidatorRequired,
|
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
|
if let Err(error) = bound
|
||||||
.artifact
|
.artifact
|
||||||
.descriptor
|
.descriptor
|
||||||
.validate_arguments(&request.operation, &request.arguments)
|
.validate_arguments(&operation, &arguments)
|
||||||
{
|
{
|
||||||
return pre_dispatch_failure(CallErrorKind::InvalidArguments, &error.to_string());
|
return pre_dispatch_failure(CallErrorKind::InvalidArguments, &error.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authorize(
|
if !authorize(&self.implementation, &bound.artifact.reference, &operation) {
|
||||||
&self.implementation,
|
|
||||||
&bound.artifact.reference,
|
|
||||||
&request.operation,
|
|
||||||
) {
|
|
||||||
return pre_dispatch_failure(
|
return pre_dispatch_failure(
|
||||||
CallErrorKind::PermissionDenied,
|
CallErrorKind::PermissionDenied,
|
||||||
"operation is not permitted for this caller",
|
"operation is not permitted for this caller",
|
||||||
@@ -623,8 +655,7 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
return OperationAttempt::NotStarted(error);
|
return OperationAttempt::NotStarted(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
let value =
|
let value = match (bound.dispatch)(&self.implementation, &operation, arguments) {
|
||||||
match (bound.dispatch)(&self.implementation, &request.operation, request.arguments) {
|
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(
|
Err(
|
||||||
error @ (DispatchError::UnknownOperation(_)
|
error @ (DispatchError::UnknownOperation(_)
|
||||||
@@ -639,7 +670,7 @@ impl<T: WipObject> PublishedObject<T> {
|
|||||||
if bound
|
if bound
|
||||||
.artifact
|
.artifact
|
||||||
.descriptor
|
.descriptor
|
||||||
.validate_result(&request.operation, &value)
|
.validate_result(&operation, &value)
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
return OperationAttempt::Started(Err(OperationFailure::internal(
|
return OperationAttempt::Started(Err(OperationFailure::internal(
|
||||||
@@ -922,6 +953,62 @@ pub trait ObjectProvider {
|
|||||||
ProviderResult::Failure(error) => Err(CallError::from_host_failure(error)),
|
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.
|
/// 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);
|
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]
|
#[test]
|
||||||
fn immutable_interface_rejects_a_supplied_validator() {
|
fn immutable_interface_rejects_a_supplied_validator() {
|
||||||
let (_, provider, _) = registry_and_provider();
|
let (_, provider, _) = registry_and_provider();
|
||||||
|
|||||||
Reference in New Issue
Block a user