fix: resolve target-qualified ancestor interfaces through live publications
Add a provider-owned routing hint without granting authority. Fetch checks provider ownership and re-resolves one coherent target/scope publication; default self/shared scope behavior remains unchanged. Validation: 7 ancestor regression tests, 901 Worker unit and 110 integration tests, root cargo check, formatting and diff checks passed.
This commit is contained in:
@@ -288,6 +288,13 @@ pub trait WipSubtreeProvider: Send + Sync {
|
|||||||
/// looking up a mutable ancestor after resolving the target. Self-scoped
|
/// looking up a mutable ancestor after resolving the target. Self-scoped
|
||||||
/// publishers can use `WipPublication::self_scoped` on their pinned snapshot.
|
/// publishers can use `WipPublication::self_scoped` on their pinned snapshot.
|
||||||
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError>;
|
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError>;
|
||||||
|
/// Routing hint for target-qualified local Interface names in an ancestor
|
||||||
|
/// scope. This grants no authority: Host re-resolves one coherent current
|
||||||
|
/// publication, checks the exact reference, and forbids escaping this owner.
|
||||||
|
/// Self/shared-scope providers retain the ordinary scope Object route.
|
||||||
|
fn interface_target(&self, reference: &InterfaceReference) -> Option<String> {
|
||||||
|
Some(reference.scope.clone())
|
||||||
|
}
|
||||||
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError>;
|
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1248,8 +1255,29 @@ impl WipHost {
|
|||||||
reference
|
reference
|
||||||
.validate()
|
.validate()
|
||||||
.map_err(|e| protocol_error(ProtocolErrorCode::InvalidRequest, e.to_string()))?;
|
.map_err(|e| protocol_error(ProtocolErrorCode::InvalidRequest, e.to_string()))?;
|
||||||
|
let target = if let Some(mount) = self.subtree(&reference.scope) {
|
||||||
|
let target = mount.provider.interface_target(reference).ok_or_else(|| {
|
||||||
|
protocol_error(
|
||||||
|
ProtocolErrorCode::InterfaceNotFound,
|
||||||
|
"interface is not published",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if wip_protocol::validate_path(&target).is_err()
|
||||||
|
|| !self
|
||||||
|
.subtree(&target)
|
||||||
|
.is_some_and(|owner| owner.root == mount.root)
|
||||||
|
{
|
||||||
|
return Err(protocol_error(
|
||||||
|
ProtocolErrorCode::InterfaceNotFound,
|
||||||
|
"interface target is outside its provider",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
target
|
||||||
|
} else {
|
||||||
|
reference.scope.clone()
|
||||||
|
};
|
||||||
if let Some(publication) = self
|
if let Some(publication) = self
|
||||||
.publication_for_interface_live(&reference.scope, reference)
|
.publication_for_interface_live(&target, reference)
|
||||||
.await?
|
.await?
|
||||||
&& publication.projection.interface == *reference
|
&& publication.projection.interface == *reference
|
||||||
&& let Some(scope) = publication.scope
|
&& let Some(scope) = publication.scope
|
||||||
|
|||||||
@@ -538,3 +538,84 @@ async fn same_name_scope_replacement_or_loss_of_ref_rejects_old_metadata_without
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TargetQualifiedProvider {
|
||||||
|
inner: Arc<AncestorProvider>,
|
||||||
|
target_hint: Mutex<Option<String>>,
|
||||||
|
}
|
||||||
|
#[async_trait]
|
||||||
|
impl WipSubtreeProvider for TargetQualifiedProvider {
|
||||||
|
fn interface_target(&self, _: &InterfaceReference) -> Option<String> {
|
||||||
|
self.target_hint.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
async fn publication(&self, path: &str) -> Result<Option<WipPublication>, ProtocolError> {
|
||||||
|
self.inner.publication(path).await
|
||||||
|
}
|
||||||
|
async fn children(&self, path: &str) -> Result<Vec<String>, ProtocolError> {
|
||||||
|
self.inner.children(path).await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn ancestor_interface_target_hint_fetches_one_current_publication_and_never_grants_authority()
|
||||||
|
{
|
||||||
|
let (_, inner) = fixture("/github", true, false);
|
||||||
|
let provider = Arc::new(TargetQualifiedProvider {
|
||||||
|
inner: inner.clone(),
|
||||||
|
target_hint: Mutex::new(Some(inner.target_path.clone())),
|
||||||
|
});
|
||||||
|
let mut mounts = WipMountRegistry::new();
|
||||||
|
mounts.allocate_namespace("scope", "github").unwrap();
|
||||||
|
mounts
|
||||||
|
.mount(projection(
|
||||||
|
"/github",
|
||||||
|
"/github",
|
||||||
|
"read",
|
||||||
|
inner.calls.clone(),
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
mounts
|
||||||
|
.mount_subtree(WipSubtreeMount {
|
||||||
|
root: "/github".into(),
|
||||||
|
provider: provider.clone(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let host = WipHost::new(mounts);
|
||||||
|
let selected = reference("/github", "read");
|
||||||
|
let fetched = host.fetch_interface_live(&selected).await.unwrap();
|
||||||
|
assert_eq!(fetched.scope_ref, Some("live-scope".into()));
|
||||||
|
assert_eq!(
|
||||||
|
inner.state.lock().unwrap().lookups,
|
||||||
|
[inner.target_path.clone()]
|
||||||
|
);
|
||||||
|
// A hint must not fabricate a Descriptor or restore a deleted scope lifetime.
|
||||||
|
inner.state.lock().unwrap().visible = false;
|
||||||
|
assert_eq!(
|
||||||
|
host.fetch_interface_live(&selected).await.unwrap_err().code,
|
||||||
|
ProtocolErrorCode::InterfaceNotFound
|
||||||
|
);
|
||||||
|
inner.state.lock().unwrap().visible = true;
|
||||||
|
let forged = reference("/github", "not-published");
|
||||||
|
assert_eq!(
|
||||||
|
host.fetch_interface_live(&forged).await.unwrap_err().code,
|
||||||
|
ProtocolErrorCode::InterfaceNotFound
|
||||||
|
);
|
||||||
|
for hint in [
|
||||||
|
None,
|
||||||
|
Some("/outside"),
|
||||||
|
Some("/github-extra/item"),
|
||||||
|
Some("/github/../item"),
|
||||||
|
Some("/github/missing"),
|
||||||
|
] {
|
||||||
|
*provider.target_hint.lock().unwrap() = hint.map(str::to_owned);
|
||||||
|
inner.state.lock().unwrap().lookups.clear();
|
||||||
|
assert_eq!(
|
||||||
|
host.fetch_interface_live(&selected).await.unwrap_err().code,
|
||||||
|
ProtocolErrorCode::InterfaceNotFound
|
||||||
|
);
|
||||||
|
if hint != Some("/github/missing") {
|
||||||
|
assert!(inner.state.lock().unwrap().lookups.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(inner.calls.load(Ordering::SeqCst), 0);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user