fix: auto-translation of user-prefixed Mac and Linux paths for the vault password file when running inside a sandbox
This commit is contained in:
@@ -274,10 +274,25 @@ impl AppConfig {
|
||||
|
||||
pub fn vault_password_file(&self) -> PathBuf {
|
||||
match &self.vault_password_file {
|
||||
Some(path) => match path.exists() {
|
||||
true => path.clone(),
|
||||
false => gman::config::Config::local_provider_password_file(),
|
||||
},
|
||||
Some(path) => {
|
||||
if path.exists() {
|
||||
return path.clone();
|
||||
}
|
||||
|
||||
if let Some(translated) = paths::translate_sandboxed_home_path(path)
|
||||
&& translated.exists()
|
||||
{
|
||||
info!(
|
||||
"vault_password_file '{}' not found; resolved to sandboxed path '{}'",
|
||||
path.display(),
|
||||
translated.display()
|
||||
);
|
||||
|
||||
return translated;
|
||||
}
|
||||
|
||||
gman::config::Config::local_provider_password_file()
|
||||
}
|
||||
None => gman::config::Config::local_provider_password_file(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,70 @@ pub fn sandbox_kit_override() -> Option<PathBuf> {
|
||||
env::var_os(get_env_name("sandbox_kit")).map(PathBuf::from)
|
||||
}
|
||||
|
||||
pub fn translate_sandboxed_home_path(path: &Path) -> Option<PathBuf> {
|
||||
if env::var_os("IS_SANDBOX").is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let s = path.to_str()?;
|
||||
|
||||
if let Some(translated) = translate_unix_home_style(s, "/home/") {
|
||||
return Some(translated);
|
||||
}
|
||||
|
||||
if let Some(translated) = translate_unix_home_style(s, "/Users/") {
|
||||
return Some(translated);
|
||||
}
|
||||
|
||||
translate_windows_users_path(s)
|
||||
}
|
||||
|
||||
fn translate_unix_home_style(s: &str, prefix: &str) -> Option<PathBuf> {
|
||||
let rest = s.strip_prefix(prefix)?;
|
||||
let (user, tail) = match rest.split_once('/') {
|
||||
Some((u, t)) => (u, t),
|
||||
None => (rest, ""),
|
||||
};
|
||||
|
||||
if user.is_empty() || user == "agent" {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(if tail.is_empty() {
|
||||
PathBuf::from("/home/agent")
|
||||
} else {
|
||||
PathBuf::from(format!("/home/agent/{tail}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn translate_windows_users_path(s: &str) -> Option<PathBuf> {
|
||||
let bytes = s.as_bytes();
|
||||
if bytes.len() < 4
|
||||
|| !bytes[0].is_ascii_alphabetic()
|
||||
|| bytes[1] != b':'
|
||||
|| bytes[2] != b'\\'
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let after_drive = &s[3..];
|
||||
let rest = after_drive.strip_prefix("Users\\")?;
|
||||
let (user, tail) = match rest.split_once('\\') {
|
||||
Some((u, t)) => (u, t.replace('\\', "/")),
|
||||
None => (rest, String::new()),
|
||||
};
|
||||
|
||||
if user.is_empty() || user == "agent" {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(if tail.is_empty() {
|
||||
PathBuf::from("/home/agent")
|
||||
} else {
|
||||
PathBuf::from(format!("/home/agent/{tail}"))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sbx_mixin_file() -> PathBuf {
|
||||
config_dir().join(SBX_MIXIN_FILE_NAME)
|
||||
}
|
||||
@@ -407,6 +471,173 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
mod sandbox_home_translation {
|
||||
use super::*;
|
||||
use serial_test::serial;
|
||||
|
||||
fn with_sandbox<F: FnOnce()>(f: F) {
|
||||
let prev = env::var_os("IS_SANDBOX");
|
||||
unsafe {
|
||||
env::set_var("IS_SANDBOX", "1");
|
||||
}
|
||||
f();
|
||||
unsafe {
|
||||
match prev {
|
||||
Some(v) => env::set_var("IS_SANDBOX", v),
|
||||
None => env::remove_var("IS_SANDBOX"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn without_sandbox<F: FnOnce()>(f: F) {
|
||||
let prev = env::var_os("IS_SANDBOX");
|
||||
unsafe {
|
||||
env::remove_var("IS_SANDBOX");
|
||||
}
|
||||
f();
|
||||
unsafe {
|
||||
if let Some(v) = prev {
|
||||
env::set_var("IS_SANDBOX", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_when_not_in_sandbox() {
|
||||
without_sandbox(|| {
|
||||
let p = Path::new("/home/atusa/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_host_home_to_agent_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/home/atusa/.coyote_password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
Some(PathBuf::from("/home/agent/.coyote_password"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_nested_host_home_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/home/atusa/.config/coyote/.password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
Some(PathBuf::from("/home/agent/.config/coyote/.password"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_when_path_already_targets_agent_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/home/agent/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_when_path_is_outside_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/etc/coyote/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_for_relative_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new(".coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_for_first_segment_not_home() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/opt/atusa/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_macos_users_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/Users/atusa/.coyote_password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
Some(PathBuf::from("/home/agent/.coyote_password"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_macos_nested_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/Users/atusa/.config/coyote/.password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
Some(PathBuf::from("/home/agent/.config/coyote/.password"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_when_macos_path_already_targets_agent() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("/Users/agent/.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_windows_drive_letter_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("C:\\Users\\atusa\\.coyote_password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
Some(PathBuf::from("/home/agent/.coyote_password"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn translates_windows_nested_path() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("D:\\Users\\atusa\\.config\\coyote\\.password");
|
||||
assert_eq!(
|
||||
translate_sandboxed_home_path(p),
|
||||
Some(PathBuf::from("/home/agent/.config/coyote/.password"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn returns_none_when_windows_path_already_targets_agent() {
|
||||
with_sandbox(|| {
|
||||
let p = Path::new("C:\\Users\\agent\\.coyote_password");
|
||||
assert_eq!(translate_sandboxed_home_path(p), None);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_kit_override_reflects_env_var_state() {
|
||||
let env_name = get_env_name("sandbox_kit");
|
||||
|
||||
+27
-1
@@ -17,7 +17,7 @@ use gman::providers::SecretProvider;
|
||||
use gman::providers::SupportedProvider;
|
||||
use gman::providers::local::LocalProvider;
|
||||
use inquire::{Password, PasswordDisplayMode, required};
|
||||
use log::warn;
|
||||
use log::{info, warn};
|
||||
use serde_yaml::Value;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use tokio::runtime::Handle;
|
||||
@@ -25,6 +25,31 @@ use uuid::Uuid;
|
||||
|
||||
pub static SECRET_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\{\{([^{}]+)}}").unwrap());
|
||||
|
||||
fn apply_sandboxed_home_translation(provider_def: &mut LocalProvider) {
|
||||
let Some(ref pf) = provider_def.password_file else {
|
||||
return;
|
||||
};
|
||||
|
||||
if pf.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(translated) = paths::translate_sandboxed_home_path(pf) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !translated.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
info!(
|
||||
"vault password file '{}' not found; resolved to sandboxed path '{}'",
|
||||
pf.display(),
|
||||
translated.display()
|
||||
);
|
||||
provider_def.password_file = Some(translated);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Vault {
|
||||
pub(crate) provider: SupportedProvider,
|
||||
@@ -92,6 +117,7 @@ impl Vault {
|
||||
};
|
||||
|
||||
if let SupportedProvider::Local { provider_def } = &mut provider {
|
||||
apply_sandboxed_home_translation(provider_def);
|
||||
ensure_password_file_initialized(provider_def)?;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user