fix: properly resolve Windows-based local vault password file locations and bootstrap them into the sandbox when possible
This commit is contained in:
+187
-8
@@ -381,15 +381,11 @@ fn copy_host_files(name: &str) -> Result<()> {
|
|||||||
|
|
||||||
match resolve_vault_password_file() {
|
match resolve_vault_password_file() {
|
||||||
Some(password_file) if password_file.exists() => {
|
Some(password_file) if password_file.exists() => {
|
||||||
let dest_path = match password_file.strip_prefix(&home_dir) {
|
let dest_path = host_to_sandbox_path(&password_file, &home_dir, cfg!(windows))?;
|
||||||
Ok(rel) => format!("/home/agent/{}", rel.display()),
|
if let Some(parent) = sandbox_path_parent(&dest_path)
|
||||||
Err(_) => password_file.display().to_string(),
|
&& !parent.is_empty()
|
||||||
};
|
|
||||||
if let Some(parent) = Path::new(&dest_path).parent()
|
|
||||||
&& let Some(parent_str) = parent.to_str()
|
|
||||||
&& !parent_str.is_empty()
|
|
||||||
{
|
{
|
||||||
ensure_sandbox_dir(name, parent_str)?;
|
ensure_sandbox_dir(name, parent)?;
|
||||||
}
|
}
|
||||||
let dest = format!("{name}:{dest_path}");
|
let dest = format!("{name}:{dest_path}");
|
||||||
sbx_cp(&password_file.display().to_string(), &dest)?;
|
sbx_cp(&password_file.display().to_string(), &dest)?;
|
||||||
@@ -408,6 +404,60 @@ fn copy_host_files(name: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn host_to_sandbox_path(
|
||||||
|
host_path: &Path,
|
||||||
|
home_dir: &Path,
|
||||||
|
is_windows_host: bool,
|
||||||
|
) -> Result<String> {
|
||||||
|
let host_str = host_path.to_str().context("Host path is not valid UTF-8")?;
|
||||||
|
let home_str = home_dir
|
||||||
|
.to_str()
|
||||||
|
.context("Home directory is not valid UTF-8")?;
|
||||||
|
|
||||||
|
if let Some(rel) = strip_host_home(host_str, home_str) {
|
||||||
|
let unixified = rel.replace('\\', "/");
|
||||||
|
return Ok(format!("/home/agent/{unixified}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_windows_host {
|
||||||
|
bail!(
|
||||||
|
"Path '{host_str}' is outside your Windows user profile ({home_str}). \
|
||||||
|
Sandbox mode cannot copy files from outside %USERPROFILE% into a Linux \
|
||||||
|
sandbox. Move the file under your user profile and update your config \
|
||||||
|
accordingly."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(host_str.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn strip_host_home(path: &str, home: &str) -> Option<String> {
|
||||||
|
let path_norm: String = path
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c == '\\' { '/' } else { c })
|
||||||
|
.collect();
|
||||||
|
let home_norm: String = home
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c == '\\' { '/' } else { c })
|
||||||
|
.collect();
|
||||||
|
let home_norm = home_norm.trim_end_matches('/');
|
||||||
|
|
||||||
|
if home_norm.is_empty() || path_norm.len() <= home_norm.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (head, tail) = path_norm.split_at(home_norm.len());
|
||||||
|
if head != home_norm || !tail.starts_with('/') {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(tail[1..].to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sandbox_path_parent(linux_path: &str) -> Option<&str> {
|
||||||
|
linux_path.rsplit_once('/').map(|(parent, _)| parent)
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_sandbox_dir(sandbox: &str, dir: &str) -> Result<()> {
|
fn ensure_sandbox_dir(sandbox: &str, dir: &str) -> Result<()> {
|
||||||
let dir_q = shell_words::quote(dir);
|
let dir_q = shell_words::quote(dir);
|
||||||
let cmd = format!("sudo mkdir -p {dir_q} && sudo chown agent:agent {dir_q}");
|
let cmd = format!("sudo mkdir -p {dir_q} && sudo chown agent:agent {dir_q}");
|
||||||
@@ -692,4 +742,133 @@ mod tests {
|
|||||||
assert_eq!(h1.len(), 64);
|
assert_eq!(h1.len(), 64);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod host_to_sandbox_path_tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linux_under_home() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new("/home/atusa/.coyote_password"),
|
||||||
|
Path::new("/home/atusa"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dest, "/home/agent/.coyote_password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linux_nested_under_home() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new("/home/atusa/.config/coyote/.password"),
|
||||||
|
Path::new("/home/atusa"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dest, "/home/agent/.config/coyote/.password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn linux_outside_home_returns_verbatim() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new("/etc/coyote/.password"),
|
||||||
|
Path::new("/home/atusa"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dest, "/etc/coyote/.password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn macos_under_home_with_spaces() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new("/Users/atusa/Library/Application Support/coyote/.password"),
|
||||||
|
Path::new("/Users/atusa"),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dest,
|
||||||
|
"/home/agent/Library/Application Support/coyote/.password"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_under_home_converts_backslashes() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new(r"C:\Users\atusa\.coyote_password"),
|
||||||
|
Path::new(r"C:\Users\atusa"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dest, "/home/agent/.coyote_password");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_nested_under_home() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new(r"C:\Users\atusa\Documents\my\vault.txt"),
|
||||||
|
Path::new(r"C:\Users\atusa"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dest, "/home/agent/Documents/my/vault.txt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_outside_home_bails_with_clear_error() {
|
||||||
|
let err = host_to_sandbox_path(
|
||||||
|
Path::new(r"C:\Program Files\Coyote\vault.txt"),
|
||||||
|
Path::new(r"C:\Users\atusa"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap_err();
|
||||||
|
|
||||||
|
let msg = err.to_string();
|
||||||
|
assert!(
|
||||||
|
msg.contains("Program Files"),
|
||||||
|
"error should name the offending path: {msg}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
msg.contains("user profile"),
|
||||||
|
"error should explain the limitation: {msg}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn windows_tolerates_trailing_slash_in_home() {
|
||||||
|
let dest = host_to_sandbox_path(
|
||||||
|
Path::new(r"C:\Users\atusa\foo"),
|
||||||
|
Path::new(r"C:\Users\atusa\"),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(dest, "/home/agent/foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sandbox_path_parent_extracts_parent_for_nested() {
|
||||||
|
assert_eq!(
|
||||||
|
sandbox_path_parent("/home/agent/.coyote_password"),
|
||||||
|
Some("/home/agent")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
sandbox_path_parent("/etc/coyote/.password"),
|
||||||
|
Some("/etc/coyote")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sandbox_path_parent_handles_edge_cases() {
|
||||||
|
assert_eq!(sandbox_path_parent("/file"), Some(""));
|
||||||
|
assert_eq!(sandbox_path_parent("noparent"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user