fix: preserve nested workdir delegation chains
This commit is contained in:
@@ -69,6 +69,42 @@ impl Drop for WorkdirDelegation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct AppliedWorkdirDelegation {
|
||||||
|
pub scoped_session: WorkdirSessionHandle,
|
||||||
|
_leases: Vec<WorkdirDelegation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for AppliedWorkdirDelegation {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("AppliedWorkdirDelegation")
|
||||||
|
.field("workdir", self.scoped_session.workdir())
|
||||||
|
.field("lease_count", &self._leases.len())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn apply_delegation_chain(
|
||||||
|
source: WorkdirSessionHandle,
|
||||||
|
requests: impl IntoIterator<Item = WorkdirDelegationRequest>,
|
||||||
|
) -> Result<AppliedWorkdirDelegation, WorkdirError> {
|
||||||
|
let mut current = source;
|
||||||
|
let mut leases = Vec::new();
|
||||||
|
for request in requests {
|
||||||
|
let authority = if current.is_delegation_capable() {
|
||||||
|
current.clone()
|
||||||
|
} else {
|
||||||
|
delegation_capable_session(current.clone())
|
||||||
|
};
|
||||||
|
let lease = authority.delegate(request).await?;
|
||||||
|
current = lease.scoped_session.clone();
|
||||||
|
leases.push(lease);
|
||||||
|
}
|
||||||
|
Ok(AppliedWorkdirDelegation {
|
||||||
|
scoped_session: current,
|
||||||
|
_leases: leases,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct SessionValidity {
|
struct SessionValidity {
|
||||||
active: AtomicBool,
|
active: AtomicBool,
|
||||||
@@ -835,6 +871,38 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn applied_chain_cannot_replace_outer_provider_attenuation() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
fs::create_dir_all(root.path().join("outer")).unwrap();
|
||||||
|
fs::create_dir_all(root.path().join("outside")).unwrap();
|
||||||
|
let result = apply_delegation_chain(
|
||||||
|
Arc::new(LocalWorkdirSession::materialized_bound(
|
||||||
|
Workdir::new("delegation-chain-test"),
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
root.path().to_path_buf(),
|
||||||
|
SharedScope::new(
|
||||||
|
Scope::from_config(&ScopeConfig {
|
||||||
|
allow: vec![ScopeRule {
|
||||||
|
target: root.path().to_path_buf(),
|
||||||
|
permission: Permission::Write,
|
||||||
|
recursive: true,
|
||||||
|
}],
|
||||||
|
deny: Vec::new(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
WorkdirSessionCapabilities::ALL,
|
||||||
|
)),
|
||||||
|
[
|
||||||
|
request("outer", WorkdirDelegationPermission::Read),
|
||||||
|
request("outside", WorkdirDelegationPermission::Read),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(matches!(result, Err(WorkdirError::Denied(_))));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn closing_parent_invalidates_delegated_sessions() {
|
async fn closing_parent_invalidates_delegated_sessions() {
|
||||||
let root = TempDir::new().unwrap();
|
let root = TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ pub enum WorkdirSessionOperation {
|
|||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkdirSessionOperationRequest {
|
pub struct WorkdirSessionOperationRequest {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub delegation: Option<crate::WorkdirDelegationRequest>,
|
pub delegations: Vec<crate::WorkdirDelegationRequest>,
|
||||||
pub operation: WorkdirSessionOperation,
|
pub operation: WorkdirSessionOperation,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +216,7 @@ mod client {
|
|||||||
workdir: Workdir,
|
workdir: Workdir,
|
||||||
session_id: WorkdirSessionId,
|
session_id: WorkdirSessionId,
|
||||||
capabilities: WorkdirSessionCapabilities,
|
capabilities: WorkdirSessionCapabilities,
|
||||||
delegation: Option<crate::WorkdirDelegationRequest>,
|
delegations: Vec<crate::WorkdirDelegationRequest>,
|
||||||
closed: AtomicBool,
|
closed: AtomicBool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +269,7 @@ mod client {
|
|||||||
workdir: Workdir::new(opened.workdir_id.as_str()),
|
workdir: Workdir::new(opened.workdir_id.as_str()),
|
||||||
session_id: opened.session_id,
|
session_id: opened.session_id,
|
||||||
capabilities: opened.capabilities,
|
capabilities: opened.capabilities,
|
||||||
delegation: None,
|
delegations: Vec::new(),
|
||||||
closed: AtomicBool::new(false),
|
closed: AtomicBool::new(false),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -297,7 +297,7 @@ mod client {
|
|||||||
],
|
],
|
||||||
)?;
|
)?;
|
||||||
let operation = WorkdirSessionOperationRequest {
|
let operation = WorkdirSessionOperationRequest {
|
||||||
delegation: self.delegation.clone(),
|
delegations: self.delegations.clone(),
|
||||||
operation,
|
operation,
|
||||||
};
|
};
|
||||||
let response = self
|
let response = self
|
||||||
@@ -335,6 +335,8 @@ mod client {
|
|||||||
if self.closed.load(Ordering::Acquire) {
|
if self.closed.load(Ordering::Acquire) {
|
||||||
return Err(WorkdirError::SessionClosed);
|
return Err(WorkdirError::SessionClosed);
|
||||||
}
|
}
|
||||||
|
let mut delegations = self.delegations.clone();
|
||||||
|
delegations.push(request.clone());
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
client: self.client.clone(),
|
client: self.client.clone(),
|
||||||
base_url: self.base_url.clone(),
|
base_url: self.base_url.clone(),
|
||||||
@@ -342,7 +344,7 @@ mod client {
|
|||||||
workdir: self.workdir.clone(),
|
workdir: self.workdir.clone(),
|
||||||
session_id: self.session_id.clone(),
|
session_id: self.session_id.clone(),
|
||||||
capabilities: self.capabilities,
|
capabilities: self.capabilities,
|
||||||
delegation: Some(request.clone()),
|
delegations,
|
||||||
closed: AtomicBool::new(false),
|
closed: AtomicBool::new(false),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ use async_trait::async_trait;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub use delegation::{
|
pub use delegation::{
|
||||||
ReadOnlyWorkdirSession, WorkdirDelegation, WorkdirDelegationPermission,
|
AppliedWorkdirDelegation, ReadOnlyWorkdirSession, WorkdirDelegation,
|
||||||
WorkdirDelegationRequest, WorkdirDelegationRule, delegation_capable_session,
|
WorkdirDelegationPermission, WorkdirDelegationRequest, WorkdirDelegationRule,
|
||||||
|
apply_delegation_chain, delegation_capable_session,
|
||||||
};
|
};
|
||||||
pub use fs_operation::{
|
pub use fs_operation::{
|
||||||
ContentHash, EditRequest, EditResult, EntryKind, FsPath as WorkdirPath, GlobRequest,
|
ContentHash, EditRequest, EditResult, EntryKind, FsPath as WorkdirPath, GlobRequest,
|
||||||
|
|||||||
@@ -320,8 +320,8 @@ mod tests {
|
|||||||
pub struct WorkspaceWorkdirSessionOperationRequest {
|
pub struct WorkspaceWorkdirSessionOperationRequest {
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub expected_session_fence: Option<String>,
|
pub expected_session_fence: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub delegation: Option<crate::WorkdirDelegationRequest>,
|
pub delegations: Vec<crate::WorkdirDelegationRequest>,
|
||||||
pub operation: crate::http::WorkdirSessionOperation,
|
pub operation: crate::http::WorkdirSessionOperation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -611,18 +611,8 @@ async fn run_workdir_session_operation(
|
|||||||
.ok_or_else(RuntimeHttpWorkdirError::not_found)?;
|
.ok_or_else(RuntimeHttpWorkdirError::not_found)?;
|
||||||
record.session.clone()
|
record.session.clone()
|
||||||
};
|
};
|
||||||
let delegation = if let Some(delegation) = request.delegation {
|
let applied = workdir::apply_delegation_chain(source, request.delegations).await?;
|
||||||
Some(
|
let session = applied.scoped_session.as_ref();
|
||||||
workdir::delegation_capable_session(source.clone())
|
|
||||||
.delegate(delegation)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let session = delegation.as_ref().map_or(source.as_ref(), |delegation| {
|
|
||||||
delegation.scoped_session.as_ref()
|
|
||||||
});
|
|
||||||
let operation = request.operation;
|
let operation = request.operation;
|
||||||
|
|
||||||
let result = match operation {
|
let result = match operation {
|
||||||
@@ -2279,7 +2269,7 @@ mod tests {
|
|||||||
expires_at: u64::MAX,
|
expires_at: u64::MAX,
|
||||||
};
|
};
|
||||||
let operation = WorkdirSessionOperationRequest {
|
let operation = WorkdirSessionOperationRequest {
|
||||||
delegation: None,
|
delegations: Vec::new(),
|
||||||
operation: WorkdirSessionOperation::Stat(StatRequest {
|
operation: WorkdirSessionOperation::Stat(StatRequest {
|
||||||
path: WorkdirPath::new("hello.txt").expect("logical path"),
|
path: WorkdirPath::new("hello.txt").expect("logical path"),
|
||||||
}),
|
}),
|
||||||
@@ -2298,14 +2288,14 @@ mod tests {
|
|||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
let delegated_read = WorkdirSessionOperationRequest {
|
let delegated_read = WorkdirSessionOperationRequest {
|
||||||
delegation: Some(workdir::WorkdirDelegationRequest {
|
delegations: vec![workdir::WorkdirDelegationRequest {
|
||||||
rules: vec![workdir::WorkdirDelegationRule {
|
rules: vec![workdir::WorkdirDelegationRule {
|
||||||
target: WorkdirPath::new("granted").unwrap(),
|
target: WorkdirPath::new("granted").unwrap(),
|
||||||
permission: workdir::WorkdirDelegationPermission::Read,
|
permission: workdir::WorkdirDelegationPermission::Read,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
}],
|
}],
|
||||||
cwd: WorkdirPath::new("granted").unwrap(),
|
cwd: WorkdirPath::new("granted").unwrap(),
|
||||||
}),
|
}],
|
||||||
operation: WorkdirSessionOperation::Read(ReadRequest {
|
operation: WorkdirSessionOperation::Read(ReadRequest {
|
||||||
path: WorkdirPath::new("link").unwrap(),
|
path: WorkdirPath::new("link").unwrap(),
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ pub struct WorkspaceAttachedWorkdirSession {
|
|||||||
client: Arc<dyn WorkspaceClient>,
|
client: Arc<dyn WorkspaceClient>,
|
||||||
workdir: Workdir,
|
workdir: Workdir,
|
||||||
expected_session_fence: Option<String>,
|
expected_session_fence: Option<String>,
|
||||||
delegation: Option<workdir::WorkdirDelegationRequest>,
|
delegations: Vec<workdir::WorkdirDelegationRequest>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WorkspaceAttachedWorkdirSession {
|
impl WorkspaceAttachedWorkdirSession {
|
||||||
@@ -164,7 +164,7 @@ impl WorkspaceAttachedWorkdirSession {
|
|||||||
client,
|
client,
|
||||||
workdir: Workdir::new("workspace-attachment"),
|
workdir: Workdir::new("workspace-attachment"),
|
||||||
expected_session_fence: None,
|
expected_session_fence: None,
|
||||||
delegation: None,
|
delegations: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,7 +183,7 @@ impl WorkspaceAttachedWorkdirSession {
|
|||||||
),
|
),
|
||||||
serde_json::to_string(&WorkspaceWorkdirSessionOperationRequest {
|
serde_json::to_string(&WorkspaceWorkdirSessionOperationRequest {
|
||||||
expected_session_fence: self.expected_session_fence.clone(),
|
expected_session_fence: self.expected_session_fence.clone(),
|
||||||
delegation: self.delegation.clone(),
|
delegations: self.delegations.clone(),
|
||||||
operation,
|
operation,
|
||||||
})
|
})
|
||||||
.map_err(|error| {
|
.map_err(|error| {
|
||||||
@@ -267,11 +267,13 @@ impl WorkdirSession for WorkspaceAttachedWorkdirSession {
|
|||||||
})?;
|
})?;
|
||||||
fence.value
|
fence.value
|
||||||
};
|
};
|
||||||
|
let mut delegations = self.delegations.clone();
|
||||||
|
delegations.push(request.clone());
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
client: self.client.clone(),
|
client: self.client.clone(),
|
||||||
workdir: self.workdir.clone(),
|
workdir: self.workdir.clone(),
|
||||||
expected_session_fence: Some(expected_session_fence),
|
expected_session_fence: Some(expected_session_fence),
|
||||||
delegation: Some(request.clone()),
|
delegations,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1100,7 +1102,60 @@ mod tests {
|
|||||||
serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap();
|
serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap();
|
||||||
assert_eq!(body["expected_session_fence"], "attachment-fence");
|
assert_eq!(body["expected_session_fence"], "attachment-fence");
|
||||||
assert_eq!(body["operation"]["operation"], "stat");
|
assert_eq!(body["operation"]["operation"], "stat");
|
||||||
assert_eq!(body["delegation"]["rules"][0]["target"], "");
|
assert_eq!(body["delegations"][0]["rules"][0]["target"], "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn nested_attached_session_preserves_full_delegation_chain() {
|
||||||
|
let client = Arc::new(RecordingWorkspaceClient::new(vec![
|
||||||
|
response(json!({"value": "attachment-fence"})),
|
||||||
|
response(json!({
|
||||||
|
"operation": "stat",
|
||||||
|
"result": {"path": "nested/file", "kind": "file", "size": 1}
|
||||||
|
})),
|
||||||
|
]));
|
||||||
|
let parent = workdir::delegation_capable_session(WorkspaceAttachedWorkdirSession::handle(
|
||||||
|
client.clone(),
|
||||||
|
));
|
||||||
|
let outer = parent
|
||||||
|
.delegate(workdir::WorkdirDelegationRequest {
|
||||||
|
rules: vec![workdir::WorkdirDelegationRule {
|
||||||
|
target: workdir::WorkdirPath::new("").unwrap(),
|
||||||
|
permission: workdir::WorkdirDelegationPermission::Read,
|
||||||
|
recursive: true,
|
||||||
|
}],
|
||||||
|
cwd: workdir::WorkdirPath::new("").unwrap(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
let nested = outer
|
||||||
|
.scoped_session
|
||||||
|
.delegate(workdir::WorkdirDelegationRequest {
|
||||||
|
rules: vec![workdir::WorkdirDelegationRule {
|
||||||
|
target: workdir::WorkdirPath::new("nested").unwrap(),
|
||||||
|
permission: workdir::WorkdirDelegationPermission::Read,
|
||||||
|
recursive: true,
|
||||||
|
}],
|
||||||
|
cwd: workdir::WorkdirPath::new("nested").unwrap(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
nested
|
||||||
|
.scoped_session
|
||||||
|
.stat(StatRequest {
|
||||||
|
path: workdir::WorkdirPath::new("file").unwrap(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let requests = client.requests();
|
||||||
|
assert_eq!(requests.len(), 2);
|
||||||
|
let body: serde_json::Value =
|
||||||
|
serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap();
|
||||||
|
assert_eq!(body["delegations"].as_array().unwrap().len(), 2);
|
||||||
|
assert_eq!(body["delegations"][0]["rules"][0]["target"], "");
|
||||||
|
assert_eq!(body["delegations"][1]["rules"][0]["target"], "nested");
|
||||||
|
assert_eq!(body["operation"]["request"]["path"], "nested/file");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -5139,24 +5139,14 @@ async fn scoped_execute_current_worker_workdir_operation(
|
|||||||
request.expected_session_fence.as_deref(),
|
request.expected_session_fence.as_deref(),
|
||||||
)?;
|
)?;
|
||||||
let source = open_current_worker_workdir_session_locked(&api, &worker, &link).await?;
|
let source = open_current_worker_workdir_session_locked(&api, &worker, &link).await?;
|
||||||
let delegation = if let Some(delegation) = request.delegation {
|
let applied = workdir::apply_delegation_chain(source, request.delegations)
|
||||||
Some(
|
|
||||||
workdir::delegation_capable_session(source.clone())
|
|
||||||
.delegate(delegation)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error::RuntimeOperationFailed {
|
.map_err(|error| Error::RuntimeOperationFailed {
|
||||||
runtime_id: worker.runtime_id.clone(),
|
runtime_id: worker.runtime_id.clone(),
|
||||||
code: "workdir_session_delegation_failed".to_string(),
|
code: "workdir_session_delegation_failed".to_string(),
|
||||||
message: error.to_string(),
|
message: error.to_string(),
|
||||||
})?,
|
})?;
|
||||||
)
|
let result = execute_workdir_session_operation(&applied.scoped_session, request.operation)
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let session = delegation
|
|
||||||
.as_ref()
|
|
||||||
.map_or(&source, |delegation| &delegation.scoped_session);
|
|
||||||
let result = execute_workdir_session_operation(session, request.operation)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|error| Error::RuntimeOperationFailed {
|
.map_err(|error| Error::RuntimeOperationFailed {
|
||||||
runtime_id: worker.runtime_id.clone(),
|
runtime_id: worker.runtime_id.clone(),
|
||||||
|
|||||||
Reference in New Issue
Block a user