fix: validate remote workdir grants before leasing

This commit is contained in:
2026-08-19 11:42:49 +09:00
parent 5f2798458e
commit 84f1b229ba
2 changed files with 70 additions and 8 deletions
+8 -2
View File
@@ -341,7 +341,7 @@ mod client {
} }
let mut delegations = self.delegations.clone(); let mut delegations = self.delegations.clone();
delegations.push(request.clone()); delegations.push(request.clone());
Ok(Arc::new(Self { let candidate = Arc::new(Self {
client: self.client.clone(), client: self.client.clone(),
base_url: self.base_url.clone(), base_url: self.base_url.clone(),
authorization: self.authorization.clone(), authorization: self.authorization.clone(),
@@ -350,7 +350,13 @@ mod client {
capabilities: self.capabilities, capabilities: self.capabilities,
delegations, delegations,
closed: AtomicBool::new(false), closed: AtomicBool::new(false),
})) });
candidate
.stat(StatRequest {
path: fs_operation::FsPath::new("").expect("empty Workdir path is valid"),
})
.await?;
Ok(candidate)
} }
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> { async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
@@ -273,12 +273,18 @@ impl WorkdirSession for WorkspaceAttachedWorkdirSession {
}; };
let mut delegations = self.delegations.clone(); let mut delegations = self.delegations.clone();
delegations.push(request.clone()); delegations.push(request.clone());
Ok(Arc::new(Self { let candidate = 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),
delegations, delegations,
})) });
candidate
.stat(StatRequest {
path: workdir::WorkdirPath::new("").expect("empty Workdir path is valid"),
})
.await?;
Ok(candidate)
} }
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> { async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
@@ -1069,6 +1075,10 @@ mod tests {
async fn delegated_attached_session_carries_captured_fence_on_operations() { async fn delegated_attached_session_carries_captured_fence_on_operations() {
let client = Arc::new(RecordingWorkspaceClient::new(vec![ let client = Arc::new(RecordingWorkspaceClient::new(vec![
response(json!({"value": "attachment-fence"})), response(json!({"value": "attachment-fence"})),
response(json!({
"operation": "stat",
"result": {"path": "", "kind": "directory", "size": 0}
})),
response(json!({ response(json!({
"operation": "stat", "operation": "stat",
"result": {"path": "visible.txt", "kind": "file", "size": 8} "result": {"path": "visible.txt", "kind": "file", "size": 8}
@@ -1097,22 +1107,59 @@ mod tests {
.unwrap(); .unwrap();
let requests = client.requests(); let requests = client.requests();
assert_eq!(requests.len(), 2); assert_eq!(requests.len(), 3);
assert_eq!( assert_eq!(
requests[0].path, requests[0].path,
"/api/w/workspace%2Ftest/workers/self/workdir-session/fence" "/api/w/workspace%2Ftest/workers/self/workdir-session/fence"
); );
let body: serde_json::Value = let body: serde_json::Value =
serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap(); serde_json::from_str(requests[2].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["delegations"][0]["rules"][0]["target"], ""); assert_eq!(body["delegations"][0]["rules"][0]["target"], "");
} }
#[tokio::test]
async fn attached_provider_rejection_happens_before_delegation_is_returned() {
let client = Arc::new(RecordingWorkspaceClient::new(vec![
response(json!({"value": "attachment-fence"})),
response(json!({"error": "provider rejected delegated write target"})),
]));
let parent = workdir::delegation_capable_session(WorkspaceAttachedWorkdirSession::handle(
client.clone(),
));
let result = parent
.delegate(workdir::WorkdirDelegationRequest {
rules: vec![workdir::WorkdirDelegationRule {
target: workdir::WorkdirPath::new("linked-target").unwrap(),
permission: workdir::WorkdirDelegationPermission::Write,
recursive: true,
}],
cwd: workdir::WorkdirPath::new("linked-target").unwrap(),
})
.await;
assert!(result.is_err(), "provider rejection must fail before lease");
let requests = client.requests();
assert_eq!(requests.len(), 2);
let validation: serde_json::Value =
serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap();
assert_eq!(validation["operation"]["operation"], "stat");
assert_eq!(validation["delegations"].as_array().unwrap().len(), 1);
}
#[tokio::test] #[tokio::test]
async fn nested_attached_session_preserves_full_delegation_chain() { async fn nested_attached_session_preserves_full_delegation_chain() {
let client = Arc::new(RecordingWorkspaceClient::new(vec![ let client = Arc::new(RecordingWorkspaceClient::new(vec![
response(json!({"value": "attachment-fence"})), response(json!({"value": "attachment-fence"})),
response(json!({
"operation": "stat",
"result": {"path": "", "kind": "directory", "size": 0}
})),
response(json!({
"operation": "stat",
"result": {"path": "nested", "kind": "directory", "size": 0}
})),
response(json!({ response(json!({
"operation": "stat", "operation": "stat",
"result": {"path": "nested/file", "kind": "file", "size": 1} "result": {"path": "nested/file", "kind": "file", "size": 1}
@@ -1153,9 +1200,18 @@ mod tests {
.unwrap(); .unwrap();
let requests = client.requests(); let requests = client.requests();
assert_eq!(requests.len(), 2); assert_eq!(requests.len(), 4);
let body: serde_json::Value = let outer_validation: serde_json::Value =
serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap(); serde_json::from_str(requests[1].body.as_deref().unwrap()).unwrap();
let nested_validation: serde_json::Value =
serde_json::from_str(requests[2].body.as_deref().unwrap()).unwrap();
assert_eq!(outer_validation["delegations"].as_array().unwrap().len(), 1);
assert_eq!(
nested_validation["delegations"].as_array().unwrap().len(),
2
);
let body: serde_json::Value =
serde_json::from_str(requests[3].body.as_deref().unwrap()).unwrap();
assert_eq!(body["delegations"].as_array().unwrap().len(), 2); assert_eq!(body["delegations"].as_array().unwrap().len(), 2);
assert_eq!(body["delegations"][0]["rules"][0]["target"], ""); assert_eq!(body["delegations"][0]["rules"][0]["target"], "");
assert_eq!(body["delegations"][1]["rules"][0]["target"], "nested"); assert_eq!(body["delegations"][1]["rules"][0]["target"], "nested");