fix: fence resolved Workdir lease aliases
This commit is contained in:
@@ -11,7 +11,8 @@ use crate::{
|
|||||||
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
||||||
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
||||||
ReadRequest, ReadResult, StatRequest, StatResult, WorkdirError, WorkdirId,
|
ReadRequest, ReadResult, StatRequest, StatResult, WorkdirError, WorkdirId,
|
||||||
WorkdirScopeAuthorizationRequest, WorkdirSessionCapabilities, WriteRequest, WriteResult,
|
WorkdirScopeAuthorizationRequest, WorkdirScopeOverlapRequest, WorkdirSessionCapabilities,
|
||||||
|
WriteRequest, WriteResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Opaque Runtime-owned identifier for one ephemeral Workdir session.
|
/// Opaque Runtime-owned identifier for one ephemeral Workdir session.
|
||||||
@@ -56,6 +57,7 @@ pub struct OpenWorkdirSessionResponse {
|
|||||||
#[serde(tag = "operation", content = "request", rename_all = "snake_case")]
|
#[serde(tag = "operation", content = "request", rename_all = "snake_case")]
|
||||||
pub enum WorkdirSessionOperation {
|
pub enum WorkdirSessionOperation {
|
||||||
AuthorizeScope(WorkdirScopeAuthorizationRequest),
|
AuthorizeScope(WorkdirScopeAuthorizationRequest),
|
||||||
|
ScopeRulesOverlap(WorkdirScopeOverlapRequest),
|
||||||
Stat(StatRequest),
|
Stat(StatRequest),
|
||||||
Read(ReadRequest),
|
Read(ReadRequest),
|
||||||
Write(WriteRequest),
|
Write(WriteRequest),
|
||||||
@@ -81,6 +83,7 @@ pub struct WorkdirSessionOperationRequest {
|
|||||||
#[serde(tag = "operation", content = "result", rename_all = "snake_case")]
|
#[serde(tag = "operation", content = "result", rename_all = "snake_case")]
|
||||||
pub enum WorkdirSessionOperationResult {
|
pub enum WorkdirSessionOperationResult {
|
||||||
AuthorizeScope,
|
AuthorizeScope,
|
||||||
|
ScopeRulesOverlap { overlaps: bool },
|
||||||
Stat(StatResult),
|
Stat(StatResult),
|
||||||
Read(ReadResult),
|
Read(ReadResult),
|
||||||
Write(WriteResult),
|
Write(WriteResult),
|
||||||
@@ -462,6 +465,19 @@ mod client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scope_rules_overlap(
|
||||||
|
&self,
|
||||||
|
request: WorkdirScopeOverlapRequest,
|
||||||
|
) -> Result<bool, WorkdirError> {
|
||||||
|
match self
|
||||||
|
.operate(WorkdirSessionOperation::ScopeRulesOverlap(request))
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
WorkdirSessionOperationResult::ScopeRulesOverlap { overlaps } => Ok(overlaps),
|
||||||
|
_ => Err(Self::mismatch("scope_rules_overlap")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
||||||
match self.operate(WorkdirSessionOperation::Stat(request)).await? {
|
match self.operate(WorkdirSessionOperation::Stat(request)).await? {
|
||||||
WorkdirSessionOperationResult::Stat(result) => Ok(result),
|
WorkdirSessionOperationResult::Stat(result) => Ok(result),
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ pub use local::{
|
|||||||
};
|
};
|
||||||
pub use operation::*;
|
pub use operation::*;
|
||||||
pub use scope::{
|
pub use scope::{
|
||||||
ReadOnlyWorkdirSession, WorkdirScopeAuthorizationRequest, WorkdirScopeLease, WorkdirToolBroker,
|
ReadOnlyWorkdirSession, WorkdirScopeAuthorizationRequest, WorkdirScopeLease,
|
||||||
WorkdirToolScope, WorkdirToolScopePermission, WorkdirToolScopeRule,
|
WorkdirScopeOverlapRequest, WorkdirToolBroker, WorkdirToolScope, WorkdirToolScopePermission,
|
||||||
|
WorkdirToolScopeRule,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Persistent, opaque identity of one materialized Workdir.
|
/// Persistent, opaque identity of one materialized Workdir.
|
||||||
@@ -166,6 +167,15 @@ pub trait WorkdirSession: std::fmt::Debug + Send + Sync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scope_rules_overlap(
|
||||||
|
&self,
|
||||||
|
_request: WorkdirScopeOverlapRequest,
|
||||||
|
) -> Result<bool, WorkdirError> {
|
||||||
|
Err(WorkdirError::Denied(
|
||||||
|
"Workdir provider cannot compare resolved scope authority".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError>;
|
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError>;
|
||||||
async fn read(&self, request: ReadRequest) -> Result<ReadResult, WorkdirError>;
|
async fn read(&self, request: ReadRequest) -> Result<ReadResult, WorkdirError>;
|
||||||
async fn write(&self, request: WriteRequest) -> Result<WriteResult, WorkdirError>;
|
async fn write(&self, request: WriteRequest) -> Result<WriteResult, WorkdirError>;
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ use crate::{
|
|||||||
CommandSnapshot, CommandStatus, CommandStream, CommandStreamSlice, EditRequest, EditResult,
|
CommandSnapshot, CommandStatus, CommandStream, CommandStreamSlice, EditRequest, EditResult,
|
||||||
GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult, ReadRequest,
|
GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult, ReadRequest,
|
||||||
ReadResult, StatRequest, StatResult, Workdir, WorkdirError, WorkdirPath,
|
ReadResult, StatRequest, StatResult, Workdir, WorkdirError, WorkdirPath,
|
||||||
WorkdirScopeAuthorizationRequest, WorkdirSession, WorkdirSessionCapabilities,
|
WorkdirScopeAuthorizationRequest, WorkdirScopeOverlapRequest, WorkdirSession,
|
||||||
WorkdirSessionCapability, WorkdirToolScopePermission, WriteRequest, WriteResult,
|
WorkdirSessionCapabilities, WorkdirSessionCapability, WorkdirToolScopePermission, WriteRequest,
|
||||||
|
WriteResult,
|
||||||
};
|
};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use crate::{EntryKind, WriteOutcome};
|
use crate::{EntryKind, WriteOutcome};
|
||||||
@@ -225,6 +226,41 @@ impl fs_operation::FsAccessPolicy for ScopeAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn path_sets_overlap(
|
||||||
|
left: &Path,
|
||||||
|
left_recursive: bool,
|
||||||
|
right: &Path,
|
||||||
|
right_recursive: bool,
|
||||||
|
) -> bool {
|
||||||
|
match (left_recursive, right_recursive) {
|
||||||
|
(true, true) => left.starts_with(right) || right.starts_with(left),
|
||||||
|
(true, false) => {
|
||||||
|
right.starts_with(left)
|
||||||
|
|| left == right
|
||||||
|
|| left.parent().is_some_and(|parent| parent == right)
|
||||||
|
}
|
||||||
|
(false, true) => {
|
||||||
|
left.starts_with(right)
|
||||||
|
|| left == right
|
||||||
|
|| right.parent().is_some_and(|parent| parent == left)
|
||||||
|
}
|
||||||
|
(false, false) => {
|
||||||
|
left == right
|
||||||
|
|| left.parent().is_some_and(|parent| parent == right)
|
||||||
|
|| right.parent().is_some_and(|parent| parent == left)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rule_targets(
|
||||||
|
root: &Path,
|
||||||
|
rule: &crate::WorkdirToolScopeRule,
|
||||||
|
) -> std::io::Result<(PathBuf, PathBuf)> {
|
||||||
|
let logical = root.join(rule.target.as_str());
|
||||||
|
let resolved = fs_operation::resolve_access_path(&logical)?;
|
||||||
|
Ok((logical, resolved))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct LocalWorkdirSessionInner {
|
struct LocalWorkdirSessionInner {
|
||||||
workdir: Workdir,
|
workdir: Workdir,
|
||||||
@@ -626,6 +662,28 @@ impl WorkdirSession for LocalWorkdirSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scope_rules_overlap(
|
||||||
|
&self,
|
||||||
|
request: WorkdirScopeOverlapRequest,
|
||||||
|
) -> Result<bool, WorkdirError> {
|
||||||
|
self.ensure_open()?;
|
||||||
|
let (left_logical, left_resolved) = rule_targets(&self.inner.root, &request.left)
|
||||||
|
.map_err(|error| WorkdirError::io(&self.inner.root, error))?;
|
||||||
|
let (right_logical, right_resolved) = rule_targets(&self.inner.root, &request.right)
|
||||||
|
.map_err(|error| WorkdirError::io(&self.inner.root, error))?;
|
||||||
|
Ok(path_sets_overlap(
|
||||||
|
&left_logical,
|
||||||
|
request.left.recursive,
|
||||||
|
&right_logical,
|
||||||
|
request.right.recursive,
|
||||||
|
) || path_sets_overlap(
|
||||||
|
&left_resolved,
|
||||||
|
request.left.recursive,
|
||||||
|
&right_resolved,
|
||||||
|
request.right.recursive,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
||||||
self.ensure_capability(WorkdirSessionCapability::Read)?;
|
self.ensure_capability(WorkdirSessionCapability::Read)?;
|
||||||
let logical = request.path.clone();
|
let logical = request.path.clone();
|
||||||
|
|||||||
+155
-79
@@ -45,6 +45,14 @@ pub struct WorkdirScopeAuthorizationRequest {
|
|||||||
pub permission: WorkdirToolScopePermission,
|
pub permission: WorkdirToolScopePermission,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Provider-side overlap comparison that keeps resolved host paths private.
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct WorkdirScopeOverlapRequest {
|
||||||
|
pub left: WorkdirToolScopeRule,
|
||||||
|
pub right: WorkdirToolScopeRule,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
pub struct WorkdirToolScope {
|
pub struct WorkdirToolScope {
|
||||||
@@ -82,6 +90,7 @@ impl WorkdirToolBroker {
|
|||||||
capabilities,
|
capabilities,
|
||||||
validity: SessionValidity::root(),
|
validity: SessionValidity::root(),
|
||||||
child_write_leases: Mutex::new(HashMap::new()),
|
child_write_leases: Mutex::new(HashMap::new()),
|
||||||
|
scope_lock: tokio::sync::Mutex::new(()),
|
||||||
next_lease_id: AtomicU64::new(1),
|
next_lease_id: AtomicU64::new(1),
|
||||||
close_lock: Arc::new(tokio::sync::Mutex::new(())),
|
close_lock: Arc::new(tokio::sync::Mutex::new(())),
|
||||||
owned_commands: Arc::new(Mutex::new(HashSet::new())),
|
owned_commands: Arc::new(Mutex::new(HashSet::new())),
|
||||||
@@ -322,6 +331,7 @@ struct ScopedWorkdirSession {
|
|||||||
capabilities: WorkdirSessionCapabilities,
|
capabilities: WorkdirSessionCapabilities,
|
||||||
validity: Arc<SessionValidity>,
|
validity: Arc<SessionValidity>,
|
||||||
child_write_leases: Mutex<HashMap<u64, ActiveWriteLease>>,
|
child_write_leases: Mutex<HashMap<u64, ActiveWriteLease>>,
|
||||||
|
scope_lock: tokio::sync::Mutex<()>,
|
||||||
next_lease_id: AtomicU64,
|
next_lease_id: AtomicU64,
|
||||||
close_lock: Arc<tokio::sync::Mutex<()>>,
|
close_lock: Arc<tokio::sync::Mutex<()>>,
|
||||||
owned_commands: Arc<Mutex<HashSet<String>>>,
|
owned_commands: Arc<Mutex<HashSet<String>>>,
|
||||||
@@ -386,9 +396,6 @@ impl ScopedWorkdirSession {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if permission == WorkdirToolScopePermission::Write {
|
|
||||||
self.ensure_parent_write_available(path)?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,33 +482,48 @@ impl ScopedWorkdirSession {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_parent_write_available(&self, path: &FsPath) -> Result<(), WorkdirError> {
|
async fn ensure_parent_write_available(&self, path: &FsPath) -> Result<(), WorkdirError> {
|
||||||
let mut leases = self
|
let active_write_rules = {
|
||||||
.child_write_leases
|
let mut leases = self
|
||||||
.lock()
|
.child_write_leases
|
||||||
.expect("Workdir tool scope lease mutex poisoned");
|
.lock()
|
||||||
leases.retain(|_, lease| {
|
.expect("Workdir tool scope lease mutex poisoned");
|
||||||
lease
|
leases.retain(|_, lease| {
|
||||||
.validity
|
lease
|
||||||
.upgrade()
|
.validity
|
||||||
.is_some_and(|validity| validity.is_active())
|
|
||||||
|| lease
|
|
||||||
.cleanup_pending
|
|
||||||
.upgrade()
|
.upgrade()
|
||||||
.is_some_and(|pending| pending.load(Ordering::Acquire))
|
.is_some_and(|validity| validity.is_active())
|
||||||
});
|
|| lease
|
||||||
if leases.values().any(|lease| {
|
.cleanup_pending
|
||||||
lease.rules.iter().any(|rule| {
|
.upgrade()
|
||||||
rule.permission == WorkdirToolScopePermission::Write
|
.is_some_and(|pending| pending.load(Ordering::Acquire))
|
||||||
&& rule_allows_path(rule, path, WorkdirToolScopePermission::Write)
|
});
|
||||||
})
|
leases
|
||||||
}) {
|
.values()
|
||||||
Err(WorkdirError::Denied(format!(
|
.flat_map(|lease| lease.rules.iter().cloned())
|
||||||
"logical workdir path `{path}` is leased to child Workdir tools"
|
.collect::<Vec<_>>()
|
||||||
)))
|
};
|
||||||
} else {
|
let requested = WorkdirToolScopeRule {
|
||||||
Ok(())
|
target: path.clone(),
|
||||||
|
permission: WorkdirToolScopePermission::Write,
|
||||||
|
recursive: false,
|
||||||
|
symlink_policy: SymlinkPolicy::Resolved,
|
||||||
|
};
|
||||||
|
for active in active_write_rules {
|
||||||
|
if self
|
||||||
|
.source
|
||||||
|
.scope_rules_overlap(WorkdirScopeOverlapRequest {
|
||||||
|
left: active,
|
||||||
|
right: requested.clone(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(WorkdirError::Denied(format!(
|
||||||
|
"path `{path}` is leased to child Workdir tools"
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn ensure_scope_targets_are_authorized(
|
async fn ensure_scope_targets_are_authorized(
|
||||||
@@ -527,6 +549,9 @@ impl ScopedWorkdirSession {
|
|||||||
) -> Result<FsPath, WorkdirError> {
|
) -> Result<FsPath, WorkdirError> {
|
||||||
self.ensure_active()?;
|
self.ensure_active()?;
|
||||||
let resolved = self.resolve_path(path)?;
|
let resolved = self.resolve_path(path)?;
|
||||||
|
if permission == WorkdirToolScopePermission::Write {
|
||||||
|
self.ensure_parent_write_available(&resolved).await?;
|
||||||
|
}
|
||||||
if let Some(rules) = self.scope.as_ref() {
|
if let Some(rules) = self.scope.as_ref() {
|
||||||
self.source
|
self.source
|
||||||
.authorize_scope_path(WorkdirScopeAuthorizationRequest {
|
.authorize_scope_path(WorkdirScopeAuthorizationRequest {
|
||||||
@@ -613,6 +638,7 @@ impl ScopedWorkdirSession {
|
|||||||
self: &Arc<Self>,
|
self: &Arc<Self>,
|
||||||
request: WorkdirToolScope,
|
request: WorkdirToolScope,
|
||||||
) -> Result<WorkdirScopeLease, WorkdirError> {
|
) -> Result<WorkdirScopeLease, WorkdirError> {
|
||||||
|
let _scope_guard = self.scope_lock.lock().await;
|
||||||
let capabilities = self.validate_scope(&request.rules, request.command)?;
|
let capabilities = self.validate_scope(&request.rules, request.command)?;
|
||||||
if !request
|
if !request
|
||||||
.rules
|
.rules
|
||||||
@@ -629,50 +655,61 @@ impl ScopedWorkdirSession {
|
|||||||
let validity = SessionValidity::child(self.validity.clone());
|
let validity = SessionValidity::child(self.validity.clone());
|
||||||
let cleanup_pending = Arc::new(AtomicBool::new(true));
|
let cleanup_pending = Arc::new(AtomicBool::new(true));
|
||||||
let id = self.next_lease_id.fetch_add(1, Ordering::Relaxed);
|
let id = self.next_lease_id.fetch_add(1, Ordering::Relaxed);
|
||||||
if request
|
let write_rules = request
|
||||||
.rules
|
.rules
|
||||||
.iter()
|
.iter()
|
||||||
.any(|rule| rule.permission == WorkdirToolScopePermission::Write)
|
.filter(|rule| rule.permission == WorkdirToolScopePermission::Write)
|
||||||
{
|
.cloned()
|
||||||
let mut leases = self
|
.collect::<Vec<_>>();
|
||||||
.child_write_leases
|
if !write_rules.is_empty() {
|
||||||
.lock()
|
let active_write_rules = {
|
||||||
.expect("Workdir tool scope lease mutex poisoned");
|
let mut leases = self
|
||||||
leases.retain(|_, lease| {
|
.child_write_leases
|
||||||
lease
|
.lock()
|
||||||
.validity
|
.expect("Workdir tool scope lease mutex poisoned");
|
||||||
.upgrade()
|
leases.retain(|_, lease| {
|
||||||
.is_some_and(|validity| validity.is_active())
|
|
||||||
|| lease
|
|
||||||
.cleanup_pending
|
|
||||||
.upgrade()
|
|
||||||
.is_some_and(|pending| pending.load(Ordering::Acquire))
|
|
||||||
});
|
|
||||||
let requested_write_rules = request
|
|
||||||
.rules
|
|
||||||
.iter()
|
|
||||||
.filter(|rule| rule.permission == WorkdirToolScopePermission::Write);
|
|
||||||
for requested in requested_write_rules {
|
|
||||||
if leases.values().any(|lease| {
|
|
||||||
lease
|
lease
|
||||||
.rules
|
.validity
|
||||||
.iter()
|
.upgrade()
|
||||||
.any(|active| rules_overlap(active, requested))
|
.is_some_and(|validity| validity.is_active())
|
||||||
}) {
|
|| lease
|
||||||
return Err(WorkdirError::Denied(format!(
|
.cleanup_pending
|
||||||
"scoped write path `{}` overlaps an active child scope",
|
.upgrade()
|
||||||
requested.target
|
.is_some_and(|pending| pending.load(Ordering::Acquire))
|
||||||
)));
|
});
|
||||||
|
leases
|
||||||
|
.values()
|
||||||
|
.flat_map(|lease| lease.rules.iter().cloned())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
for requested in &write_rules {
|
||||||
|
for active in &active_write_rules {
|
||||||
|
if self
|
||||||
|
.source
|
||||||
|
.scope_rules_overlap(WorkdirScopeOverlapRequest {
|
||||||
|
left: active.clone(),
|
||||||
|
right: requested.clone(),
|
||||||
|
})
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(WorkdirError::Denied(format!(
|
||||||
|
"scoped write path `{}` overlaps an active child scope after provider resolution",
|
||||||
|
requested.target
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
leases.insert(
|
self.child_write_leases
|
||||||
id,
|
.lock()
|
||||||
ActiveWriteLease {
|
.expect("Workdir tool scope lease mutex poisoned")
|
||||||
validity: Arc::downgrade(&validity),
|
.insert(
|
||||||
cleanup_pending: Arc::downgrade(&cleanup_pending),
|
id,
|
||||||
rules: request.rules.clone(),
|
ActiveWriteLease {
|
||||||
},
|
validity: Arc::downgrade(&validity),
|
||||||
);
|
cleanup_pending: Arc::downgrade(&cleanup_pending),
|
||||||
|
rules: request.rules.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let owned_commands = Arc::new(Mutex::new(HashSet::new()));
|
let owned_commands = Arc::new(Mutex::new(HashSet::new()));
|
||||||
let pending_command_events = Arc::new(Mutex::new(HashMap::new()));
|
let pending_command_events = Arc::new(Mutex::new(HashMap::new()));
|
||||||
@@ -698,6 +735,7 @@ impl ScopedWorkdirSession {
|
|||||||
capabilities,
|
capabilities,
|
||||||
validity: validity.clone(),
|
validity: validity.clone(),
|
||||||
child_write_leases: Mutex::new(HashMap::new()),
|
child_write_leases: Mutex::new(HashMap::new()),
|
||||||
|
scope_lock: tokio::sync::Mutex::new(()),
|
||||||
next_lease_id: AtomicU64::new(1),
|
next_lease_id: AtomicU64::new(1),
|
||||||
close_lock: close_lock.clone(),
|
close_lock: close_lock.clone(),
|
||||||
owned_commands,
|
owned_commands,
|
||||||
@@ -1011,6 +1049,13 @@ impl WorkdirSession for ReadOnlyWorkdirSession {
|
|||||||
self.inner.authorize_scope_path(request).await
|
self.inner.authorize_scope_path(request).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scope_rules_overlap(
|
||||||
|
&self,
|
||||||
|
request: WorkdirScopeOverlapRequest,
|
||||||
|
) -> Result<bool, WorkdirError> {
|
||||||
|
self.inner.scope_rules_overlap(request).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
||||||
self.inner.stat(request).await
|
self.inner.stat(request).await
|
||||||
}
|
}
|
||||||
@@ -1167,13 +1212,6 @@ fn unix_timestamp_ms() -> u64 {
|
|||||||
.min(u128::from(u64::MAX)) as u64
|
.min(u128::from(u64::MAX)) as u64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rules_overlap(left: &WorkdirToolScopeRule, right: &WorkdirToolScopeRule) -> bool {
|
|
||||||
left.permission == WorkdirToolScopePermission::Write
|
|
||||||
&& right.permission == WorkdirToolScopePermission::Write
|
|
||||||
&& (rule_allows_path(left, &right.target, WorkdirToolScopePermission::Write)
|
|
||||||
|| rule_allows_path(right, &left.target, WorkdirToolScopePermission::Write))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn rule_allows_path(
|
pub(crate) fn rule_allows_path(
|
||||||
rule: &WorkdirToolScopeRule,
|
rule: &WorkdirToolScopeRule,
|
||||||
path: &FsPath,
|
path: &FsPath,
|
||||||
@@ -1578,6 +1616,41 @@ mod tests {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn sibling_write_scopes_reject_distinct_aliases_to_same_resolved_target() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
fs::create_dir_all(root.path().join("target")).unwrap();
|
||||||
|
symlink("target", root.path().join("alias-a")).unwrap();
|
||||||
|
symlink("target", root.path().join("alias-b")).unwrap();
|
||||||
|
let parent = session(root.path());
|
||||||
|
let _first = parent
|
||||||
|
.scope(request("alias-a", WorkdirToolScopePermission::Write))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
parent
|
||||||
|
.scope(request("alias-b", WorkdirToolScopePermission::Write))
|
||||||
|
.await,
|
||||||
|
Err(WorkdirError::Denied(message))
|
||||||
|
if message.contains("overlaps an active child scope after provider resolution")
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
parent
|
||||||
|
.write(WriteRequest {
|
||||||
|
path: FsPath::new("target/from-parent").unwrap(),
|
||||||
|
content: b"blocked".to_vec(),
|
||||||
|
expected_hash: None,
|
||||||
|
})
|
||||||
|
.await,
|
||||||
|
Err(WorkdirError::Denied(message))
|
||||||
|
if message.contains("leased to child Workdir tools")
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn nested_scope_cannot_expand_resolved_policy_to_logical() {
|
async fn nested_scope_cannot_expand_resolved_policy_to_logical() {
|
||||||
let root = TempDir::new().unwrap();
|
let root = TempDir::new().unwrap();
|
||||||
@@ -1641,7 +1714,7 @@ mod tests {
|
|||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn write_delegation_leases_the_logical_symlink_path() {
|
async fn write_delegation_leases_logical_alias_and_resolved_target() {
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
let root = TempDir::new().unwrap();
|
let root = TempDir::new().unwrap();
|
||||||
@@ -1657,10 +1730,13 @@ mod tests {
|
|||||||
.write(write("from-child", "child-authoritative"))
|
.write(write("from-child", "child-authoritative"))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
parent
|
assert!(matches!(
|
||||||
.write(write("secret/parent", "still-authoritative"))
|
parent
|
||||||
.await
|
.write(write("secret/parent", "must-be-blocked"))
|
||||||
.unwrap();
|
.await,
|
||||||
|
Err(WorkdirError::Denied(message))
|
||||||
|
if message.contains("leased to child Workdir tools")
|
||||||
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
fs::read_to_string(root.path().join("secret/from-child")).unwrap(),
|
fs::read_to_string(root.path().join("secret/from-child")).unwrap(),
|
||||||
"child-authoritative"
|
"child-authoritative"
|
||||||
|
|||||||
@@ -1016,6 +1016,10 @@ async fn run_workdir_session_operation(
|
|||||||
session.authorize_scope_path(request).await?;
|
session.authorize_scope_path(request).await?;
|
||||||
WorkdirSessionOperationResult::AuthorizeScope
|
WorkdirSessionOperationResult::AuthorizeScope
|
||||||
}
|
}
|
||||||
|
WorkdirSessionOperation::ScopeRulesOverlap(request) => {
|
||||||
|
let overlaps = session.scope_rules_overlap(request).await?;
|
||||||
|
WorkdirSessionOperationResult::ScopeRulesOverlap { overlaps }
|
||||||
|
}
|
||||||
WorkdirSessionOperation::Stat(request) => {
|
WorkdirSessionOperation::Stat(request) => {
|
||||||
WorkdirSessionOperationResult::Stat(session.stat(request).await?)
|
WorkdirSessionOperationResult::Stat(session.stat(request).await?)
|
||||||
}
|
}
|
||||||
@@ -3014,6 +3018,33 @@ mod tests {
|
|||||||
WorkdirSessionOperationResult::AuthorizeScope
|
WorkdirSessionOperationResult::AuthorizeScope
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let overlap_rule = workdir::WorkdirToolScopeRule {
|
||||||
|
target: WorkdirPath::new("hello.txt").unwrap(),
|
||||||
|
permission: workdir::WorkdirToolScopePermission::Write,
|
||||||
|
recursive: false,
|
||||||
|
symlink_policy: Default::default(),
|
||||||
|
};
|
||||||
|
let overlap = WorkdirSessionOperationRequest {
|
||||||
|
operation: WorkdirSessionOperation::ScopeRulesOverlap(
|
||||||
|
workdir::WorkdirScopeOverlapRequest {
|
||||||
|
left: overlap_rule.clone(),
|
||||||
|
right: overlap_rule,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let Json(result) = run_workdir_session_operation(
|
||||||
|
State(state.clone()),
|
||||||
|
Path("session-1".to_string()),
|
||||||
|
Some(Extension(auth.clone())),
|
||||||
|
Ok(Json(overlap)),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("provider-side resolved overlap check");
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
WorkdirSessionOperationResult::ScopeRulesOverlap { overlaps: true }
|
||||||
|
));
|
||||||
|
|
||||||
let grep = WorkdirSessionOperationRequest {
|
let grep = WorkdirSessionOperationRequest {
|
||||||
operation: WorkdirSessionOperation::Grep(GrepRequest {
|
operation: WorkdirSessionOperation::Grep(GrepRequest {
|
||||||
pattern: "hello".into(),
|
pattern: "hello".into(),
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ use workdir::{
|
|||||||
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
||||||
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
||||||
ReadRequest, ReadResult, StatRequest, StatResult, Workdir, WorkdirError,
|
ReadRequest, ReadResult, StatRequest, StatResult, Workdir, WorkdirError,
|
||||||
WorkdirScopeAuthorizationRequest, WorkdirSession, WorkdirSessionCapabilities,
|
WorkdirScopeAuthorizationRequest, WorkdirScopeOverlapRequest, WorkdirSession,
|
||||||
WorkdirSessionHandle, WriteRequest, WriteResult,
|
WorkdirSessionCapabilities, WorkdirSessionHandle, WriteRequest, WriteResult,
|
||||||
};
|
};
|
||||||
|
|
||||||
use workspace_api::{
|
use workspace_api::{
|
||||||
@@ -294,6 +294,16 @@ impl WorkdirSession for WorkspaceAttachedWorkdirSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn scope_rules_overlap(
|
||||||
|
&self,
|
||||||
|
request: WorkdirScopeOverlapRequest,
|
||||||
|
) -> Result<bool, WorkdirError> {
|
||||||
|
match self.operate(WorkdirSessionOperation::ScopeRulesOverlap(request))? {
|
||||||
|
WorkdirSessionOperationResult::ScopeRulesOverlap { overlaps } => Ok(overlaps),
|
||||||
|
_ => Err(Self::mismatch("scope_rules_overlap")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
||||||
match self.operate(WorkdirSessionOperation::Stat(request))? {
|
match self.operate(WorkdirSessionOperation::Stat(request))? {
|
||||||
WorkdirSessionOperationResult::Stat(result) => Ok(result),
|
WorkdirSessionOperationResult::Stat(result) => Ok(result),
|
||||||
|
|||||||
@@ -8434,6 +8434,7 @@ async fn scoped_execute_current_worker_workdir_operation(
|
|||||||
.map_err(|error| current_worker_workdir_operation_error(&worker, error))?
|
.map_err(|error| current_worker_workdir_operation_error(&worker, error))?
|
||||||
}
|
}
|
||||||
operation @ (WorkdirSessionOperation::AuthorizeScope(_)
|
operation @ (WorkdirSessionOperation::AuthorizeScope(_)
|
||||||
|
| WorkdirSessionOperation::ScopeRulesOverlap(_)
|
||||||
| WorkdirSessionOperation::Stat(_)
|
| WorkdirSessionOperation::Stat(_)
|
||||||
| WorkdirSessionOperation::Read(_)
|
| WorkdirSessionOperation::Read(_)
|
||||||
| WorkdirSessionOperation::Write(_)
|
| WorkdirSessionOperation::Write(_)
|
||||||
@@ -8489,6 +8490,10 @@ async fn execute_workdir_session_operation(
|
|||||||
.authorize_scope_path(request)
|
.authorize_scope_path(request)
|
||||||
.await
|
.await
|
||||||
.map(|()| WorkdirSessionOperationResult::AuthorizeScope),
|
.map(|()| WorkdirSessionOperationResult::AuthorizeScope),
|
||||||
|
WorkdirSessionOperation::ScopeRulesOverlap(request) => session
|
||||||
|
.scope_rules_overlap(request)
|
||||||
|
.await
|
||||||
|
.map(|overlaps| WorkdirSessionOperationResult::ScopeRulesOverlap { overlaps }),
|
||||||
WorkdirSessionOperation::Stat(request) => session
|
WorkdirSessionOperation::Stat(request) => session
|
||||||
.stat(request)
|
.stat(request)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user