fix: align delegated paths with provider resolution

This commit is contained in:
2026-08-19 11:22:56 +09:00
parent af3decce51
commit 5f2798458e
7 changed files with 144 additions and 16 deletions
+69 -14
View File
@@ -360,6 +360,10 @@ impl WorkdirSession for DelegatingWorkdirSession {
true
}
fn transports_delegation_context(&self) -> bool {
self.source.transports_delegation_context()
}
async fn capture_delegation_source(
&self,
request: &WorkdirDelegationRequest,
@@ -431,44 +435,65 @@ impl WorkdirSession for DelegatingWorkdirSession {
}
async fn stat(&self, mut request: StatRequest) -> Result<StatResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_read(&request.path, WorkdirSessionCapability::Read)?;
let path = self.resolve_path(&request.path)?;
self.ensure_read(&path, WorkdirSessionCapability::Read)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.stat(request).await
}
async fn read(&self, mut request: ReadRequest) -> Result<ReadResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_read(&request.path, WorkdirSessionCapability::Read)?;
let path = self.resolve_path(&request.path)?;
self.ensure_read(&path, WorkdirSessionCapability::Read)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.read(request).await
}
async fn write(&self, mut request: WriteRequest) -> Result<WriteResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_write(&request.path, WorkdirSessionCapability::Write)?;
let path = self.resolve_path(&request.path)?;
self.ensure_write(&path, WorkdirSessionCapability::Write)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.write(request).await
}
async fn edit(&self, mut request: EditRequest) -> Result<EditResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_write(&request.path, WorkdirSessionCapability::Edit)?;
let path = self.resolve_path(&request.path)?;
self.ensure_write(&path, WorkdirSessionCapability::Edit)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.edit(request).await
}
async fn list(&self, mut request: ListRequest) -> Result<ListResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_read(&request.path, WorkdirSessionCapability::Read)?;
let path = self.resolve_path(&request.path)?;
self.ensure_read(&path, WorkdirSessionCapability::Read)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.list(request).await
}
async fn glob(&self, mut request: GlobRequest) -> Result<GlobResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_read(&request.path, WorkdirSessionCapability::Glob)?;
let path = self.resolve_path(&request.path)?;
self.ensure_read(&path, WorkdirSessionCapability::Glob)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.glob(request).await
}
async fn grep(&self, mut request: GrepRequest) -> Result<GrepResult, WorkdirError> {
request.path = self.resolve_path(&request.path)?;
self.ensure_read(&request.path, WorkdirSessionCapability::Grep)?;
let path = self.resolve_path(&request.path)?;
self.ensure_read(&path, WorkdirSessionCapability::Grep)?;
if !self.source.transports_delegation_context() {
request.path = path;
}
self.source.grep(request).await
}
@@ -531,6 +556,10 @@ impl WorkdirSession for ReadOnlyWorkdirSession {
true
}
fn transports_delegation_context(&self) -> bool {
self.inner.transports_delegation_context()
}
async fn delegate(
&self,
request: WorkdirDelegationRequest,
@@ -798,6 +827,32 @@ mod tests {
assert!(!root.path().join("secret/new").exists());
}
#[cfg(unix)]
#[tokio::test]
async fn write_delegation_rejects_symlink_target_before_lease() {
use std::os::unix::fs::symlink;
let root = TempDir::new().unwrap();
fs::create_dir_all(root.path().join("granted")).unwrap();
fs::create_dir_all(root.path().join("secret")).unwrap();
symlink("../secret", root.path().join("granted/outside")).unwrap();
let parent = session(root.path());
assert!(matches!(
parent
.delegate(request(
"granted/outside",
WorkdirDelegationPermission::Write
))
.await,
Err(WorkdirError::Denied(_))
));
parent
.write(write("secret/parent", "still-authoritative"))
.await
.unwrap();
}
#[tokio::test]
async fn write_lease_blocks_parent_region_until_release() {
let root = TempDir::new().unwrap();
+4
View File
@@ -328,6 +328,10 @@ mod client {
self.capabilities
}
fn transports_delegation_context(&self) -> bool {
true
}
async fn capture_delegation_source(
&self,
request: &crate::WorkdirDelegationRequest,
+6
View File
@@ -149,6 +149,12 @@ pub trait WorkdirSession: std::fmt::Debug + Send + Sync {
false
}
/// Whether this session transports the delegation chain to another
/// provider boundary that will apply logical cwd/path resolution there.
fn transports_delegation_context(&self) -> bool {
false
}
/// Capture a provider-specific source for a delegated child session.
/// Remote providers use this boundary to pin attachment identity without
/// exposing transport handles or host paths.
+22 -1
View File
@@ -388,6 +388,18 @@ impl WorkdirSession for LocalWorkdirSession {
recursive: rule.recursive,
})
.collect::<Vec<_>>();
for (logical, host) in request.rules.iter().zip(&host_rules) {
if logical.permission == WorkdirDelegationPermission::Write {
let resolved = Scope::resolved_target(host)
.map_err(|error| WorkdirError::Denied(error.to_string()))?;
if resolved != host.target {
return Err(WorkdirError::Denied(format!(
"write delegation target `{}` traverses a symlink",
logical.target
)));
}
}
}
let parent_scope = self.inner.scope.snapshot();
for rule in &host_rules {
if !parent_scope
@@ -405,10 +417,19 @@ impl WorkdirSession for LocalWorkdirSession {
deny: Vec::new(),
})
.map_err(|error| WorkdirError::Denied(error.to_string()))?;
let child_cwd = self.inner.root.join(request.cwd.as_str());
if !child_scope.is_readable(&child_cwd)
|| !std::fs::metadata(&child_cwd).is_ok_and(|metadata| metadata.is_dir())
{
return Err(WorkdirError::Denied(format!(
"delegated cwd `{}` is not a readable Workdir directory",
request.cwd
)));
}
Ok(Arc::new(LocalWorkdirSession::materialized_bound(
self.inner.workdir.clone(),
self.inner.root.clone(),
self.inner.cwd.clone(),
self.inner.root.clone(),
SharedScope::new(child_scope),
self.inner.capabilities,
)))