fix: validate spawn delegation authority
This commit is contained in:
parent
ee508f707f
commit
c04b1ca289
|
|
@ -210,6 +210,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// A different top-level Pod trying to register /src/core/x
|
// A different top-level Pod trying to register /src/core/x
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use manifest::ScopeRule;
|
use manifest::{ScopeError, ScopeRule};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
||||||
/// Errors raised by the mutating pod-registry operations.
|
/// Errors raised by the mutating pod-registry operations.
|
||||||
|
|
@ -20,10 +20,12 @@ pub enum ScopeLockError {
|
||||||
competitor_rule: ScopeRule,
|
competitor_rule: ScopeRule,
|
||||||
},
|
},
|
||||||
#[error(
|
#[error(
|
||||||
"requested scope `{}` is not within spawner `{spawner}`'s effective scope",
|
"requested scope `{}` is not within spawner `{spawner}`'s delegation scope",
|
||||||
.rule.target.display()
|
.rule.target.display()
|
||||||
)]
|
)]
|
||||||
NotSubset { spawner: String, rule: ScopeRule },
|
NotSubset { spawner: String, rule: ScopeRule },
|
||||||
|
#[error("invalid delegation scope: {source}")]
|
||||||
|
InvalidScope { source: ScopeError },
|
||||||
#[error("pod `{0}` is not registered")]
|
#[error("pod `{0}` is not registered")]
|
||||||
UnknownPod(String),
|
UnknownPod(String),
|
||||||
#[error(
|
#[error(
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use manifest::{Permission, ScopeRule};
|
use manifest::{DelegationScope, Permission, ScopeRule};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
||||||
use crate::conflict::{find_conflict_owner, find_conflict_owners, is_within_effective_write};
|
use crate::conflict::{find_conflict_owner, find_conflict_owners};
|
||||||
use crate::error::ScopeLockError;
|
use crate::error::ScopeLockError;
|
||||||
use crate::table::{Allocation, LockFileGuard};
|
use crate::table::{Allocation, LockFileGuard};
|
||||||
|
|
||||||
|
|
@ -106,8 +106,8 @@ pub fn register_pod_with_deny(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a spawned Pod whose scope is delegated from `spawner`.
|
/// Register a spawned Pod whose scope is delegated from `spawner`.
|
||||||
/// The requested scope must be within `spawner`'s effective write
|
/// The requested scope must be within the spawner's delegation authority;
|
||||||
/// scope; overlap with any Pod other than `spawner` is a conflict.
|
/// overlap with any Pod other than `spawner` is a conflict.
|
||||||
pub fn delegate_scope(
|
pub fn delegate_scope(
|
||||||
guard: &mut LockFileGuard,
|
guard: &mut LockFileGuard,
|
||||||
spawner: &str,
|
spawner: &str,
|
||||||
|
|
@ -115,6 +115,7 @@ pub fn delegate_scope(
|
||||||
pid: u32,
|
pid: u32,
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
scope_allow: Vec<ScopeRule>,
|
scope_allow: Vec<ScopeRule>,
|
||||||
|
delegation_scope: &DelegationScope,
|
||||||
) -> Result<(), ScopeLockError> {
|
) -> Result<(), ScopeLockError> {
|
||||||
reclaim_stale(guard);
|
reclaim_stale(guard);
|
||||||
if guard.data().find(&spawned).is_some() {
|
if guard.data().find(&spawned).is_some() {
|
||||||
|
|
@ -124,7 +125,10 @@ pub fn delegate_scope(
|
||||||
return Err(ScopeLockError::UnknownPod(spawner.into()));
|
return Err(ScopeLockError::UnknownPod(spawner.into()));
|
||||||
}
|
}
|
||||||
for rule in &scope_allow {
|
for rule in &scope_allow {
|
||||||
if !is_within_effective_write(guard.data(), spawner, rule) {
|
let allowed = delegation_scope
|
||||||
|
.allows_rule(rule)
|
||||||
|
.map_err(|source| ScopeLockError::InvalidScope { source })?;
|
||||||
|
if !allowed {
|
||||||
return Err(ScopeLockError::NotSubset {
|
return Err(ScopeLockError::NotSubset {
|
||||||
spawner: spawner.into(),
|
spawner: spawner.into(),
|
||||||
rule: rule.clone(),
|
rule: rule.clone(),
|
||||||
|
|
@ -363,11 +367,42 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/docs", true)],
|
vec![write_rule("/docs", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
assert!(matches!(err, ScopeLockError::NotSubset { .. }));
|
assert!(matches!(err, ScopeLockError::NotSubset { .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn delegate_uses_delegation_scope_not_direct_effective_write() {
|
||||||
|
let dir = TempDir::new().unwrap();
|
||||||
|
let path = dir.path().join("pods.json");
|
||||||
|
let mut g = open_empty(&path);
|
||||||
|
register_pod(
|
||||||
|
&mut g,
|
||||||
|
"orchestrator".into(),
|
||||||
|
std::process::id(),
|
||||||
|
sock("orchestrator"),
|
||||||
|
vec![read_rule("/workspace", true)],
|
||||||
|
sid(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
delegate_scope(
|
||||||
|
&mut g,
|
||||||
|
"orchestrator",
|
||||||
|
"coder".into(),
|
||||||
|
std::process::id(),
|
||||||
|
sock("coder"),
|
||||||
|
vec![write_rule("/workspace/.worktree/task", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/workspace", true)]),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let coder = g.data().find("coder").expect("coder allocation");
|
||||||
|
assert_eq!(coder.delegated_from.as_deref(), Some("orchestrator"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delegate_succeeds_within_parent_scope() {
|
fn delegate_succeeds_within_parent_scope() {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
|
|
@ -389,6 +424,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(g.data().allocations.len(), 2);
|
assert_eq!(g.data().allocations.len(), 2);
|
||||||
|
|
@ -427,6 +463,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Sibling C from A tries to take /src/core/sub — already under B's scope.
|
// Sibling C from A tries to take /src/core/sub — already under B's scope.
|
||||||
|
|
@ -437,10 +474,13 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("c"),
|
sock("c"),
|
||||||
vec![write_rule("/src/core/sub", true)],
|
vec![write_rule("/src/core/sub", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap_err();
|
.unwrap_err();
|
||||||
// NotSubset fires first because /src/core is no longer in A's effective.
|
match err {
|
||||||
assert!(matches!(err, ScopeLockError::NotSubset { .. }));
|
ScopeLockError::WriteConflict { competitor, .. } => assert_eq!(competitor, "b"),
|
||||||
|
other => panic!("expected WriteConflict, got {other:?}"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -464,6 +504,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
delegate_scope(
|
delegate_scope(
|
||||||
|
|
@ -473,6 +514,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("d"),
|
sock("d"),
|
||||||
vec![write_rule("/src/core/x", true)],
|
vec![write_rule("/src/core/x", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src/core", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
release_pod(&mut g, "b").unwrap();
|
release_pod(&mut g, "b").unwrap();
|
||||||
|
|
@ -571,6 +613,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
delegate_scope(
|
delegate_scope(
|
||||||
|
|
@ -580,6 +623,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("d"),
|
sock("d"),
|
||||||
vec![write_rule("/src/core/x", true)],
|
vec![write_rule("/src/core/x", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src/core", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
// Simulate B crashing by rewriting its pid to one the probe
|
// Simulate B crashing by rewriting its pid to one the probe
|
||||||
|
|
@ -670,6 +714,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("b"),
|
sock("b"),
|
||||||
vec![write_rule("/src/core", true)],
|
vec![write_rule("/src/core", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/src", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(!is_within_effective_write(
|
assert!(!is_within_effective_write(
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,7 @@ mod tests {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
sock("child"),
|
sock("child"),
|
||||||
vec![write_rule("/p/sub", true)],
|
vec![write_rule("/p/sub", true)],
|
||||||
|
&delegation_scope(vec![write_rule("/p", true)]),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::{LazyLock, Mutex, MutexGuard};
|
use std::sync::{LazyLock, Mutex, MutexGuard};
|
||||||
|
|
||||||
use manifest::{Permission, ScopeRule};
|
use manifest::{DelegationScope, Permission, ScopeConfig, ScopeRule};
|
||||||
use session_store::SegmentId;
|
use session_store::SegmentId;
|
||||||
|
|
||||||
use crate::table::LockFileGuard;
|
use crate::table::LockFileGuard;
|
||||||
|
|
@ -88,6 +88,14 @@ pub(crate) fn read_rule(path: &str, recursive: bool) -> ScopeRule {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn delegation_scope(rules: Vec<ScopeRule>) -> DelegationScope {
|
||||||
|
DelegationScope::from_config(&ScopeConfig {
|
||||||
|
allow: rules,
|
||||||
|
deny: Vec::new(),
|
||||||
|
})
|
||||||
|
.expect("test delegation scope")
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn sock(name: &str) -> PathBuf {
|
pub(crate) fn sock(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("/tmp/{name}.sock"))
|
PathBuf::from(format!("/tmp/{name}.sock"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,7 @@ impl Tool for SpawnPodTool {
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
predicted_socket.clone(),
|
predicted_socket.clone(),
|
||||||
scope_allow.clone(),
|
scope_allow.clone(),
|
||||||
|
&self.delegation_scope,
|
||||||
)
|
)
|
||||||
.map_err(pod_registry_err_to_tool)?;
|
.map_err(pod_registry_err_to_tool)?;
|
||||||
}
|
}
|
||||||
|
|
@ -868,6 +869,7 @@ fn pod_registry_err_to_tool(e: ScopeLockError) -> ToolError {
|
||||||
| ScopeLockError::WriteConflict { .. }
|
| ScopeLockError::WriteConflict { .. }
|
||||||
| ScopeLockError::DuplicatePodName(_)
|
| ScopeLockError::DuplicatePodName(_)
|
||||||
| ScopeLockError::UnknownPod(_)
|
| ScopeLockError::UnknownPod(_)
|
||||||
|
| ScopeLockError::InvalidScope { .. }
|
||||||
| ScopeLockError::SegmentConflict { .. } => ToolError::InvalidArgument(e.to_string()),
|
| ScopeLockError::SegmentConflict { .. } => ToolError::InvalidArgument(e.to_string()),
|
||||||
ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()),
|
ScopeLockError::Io(_) => ToolError::ExecutionFailed(e.to_string()),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user