use std::path::Path;
use super::super::{check_fs_path_scope, scope_memo, FsAccess};
use crate::orchestration::{
pop_execution_policy, push_execution_policy, CapabilityPolicy, SandboxProfile,
};
fn with_workspace_policy<T>(workspace: &Path, body: impl FnOnce() -> T) -> T {
push_execution_policy(CapabilityPolicy {
sandbox_profile: SandboxProfile::Worktree,
workspace_roots: vec![workspace.to_string_lossy().into_owned()],
..CapabilityPolicy::default()
});
scope_memo::clear();
let out = body();
pop_execution_policy();
scope_memo::clear();
out
}
#[test]
fn repeated_reads_under_one_policy_derive_the_roots_once() {
let _env_lock = crate::runtime_paths::test_env_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let workspace = tempfile::tempdir().unwrap();
with_workspace_policy(workspace.path(), || {
let before = scope_memo::derivations();
for index in 0..50 {
let file = workspace.path().join(format!("f{index}.txt"));
assert!(check_fs_path_scope(&file, FsAccess::Read).is_ok());
}
assert_eq!(scope_memo::derivations() - before, 1);
});
}
#[test]
fn a_memo_older_than_the_bound_is_derived_again() {
let _env_lock = crate::runtime_paths::test_env_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let workspace = tempfile::tempdir().unwrap();
with_workspace_policy(workspace.path(), || {
let file = workspace.path().join("a.txt");
assert!(check_fs_path_scope(&file, FsAccess::Read).is_ok());
let before = scope_memo::derivations();
scope_memo::age(scope_memo::MEMO_TTL + std::time::Duration::from_millis(1));
assert!(check_fs_path_scope(&file, FsAccess::Read).is_ok());
assert_eq!(scope_memo::derivations() - before, 1);
});
}
#[cfg(unix)]
#[test]
fn a_deny_root_linked_into_the_workspace_after_a_write_is_refused() {
let _env_lock = crate::runtime_paths::test_env_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let home = tempfile::tempdir().unwrap();
let workspace = tempfile::tempdir().unwrap();
let saved_home = std::env::var_os("HOME");
std::env::set_var("HOME", home.path());
let secret = workspace.path().join("secret");
std::fs::create_dir(&secret).unwrap();
let key = secret.join("id_ed25519");
let refused = with_workspace_policy(workspace.path(), || {
assert!(check_fs_path_scope(&key, FsAccess::Read).is_ok());
assert!(check_fs_path_scope(&workspace.path().join("out.txt"), FsAccess::Write).is_ok());
std::os::unix::fs::symlink(&secret, home.path().join(".ssh")).unwrap();
check_fs_path_scope(&key, FsAccess::Read).is_err()
});
match saved_home {
Some(value) => std::env::set_var("HOME", value),
None => std::env::remove_var("HOME"),
}
assert!(
refused,
"a read through a freshly linked ~/.ssh must be refused"
);
}
#[cfg(unix)]
#[test]
fn a_workspace_link_re_pointed_at_a_deny_root_between_reads_is_refused() {
let _env_lock = crate::runtime_paths::test_env_lock()
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let home = tempfile::tempdir().unwrap();
let workspace = tempfile::tempdir().unwrap();
let saved_home = std::env::var_os("HOME");
std::env::set_var("HOME", home.path());
std::fs::create_dir(home.path().join(".ssh")).unwrap();
let plain = workspace.path().join("plain");
std::fs::create_dir(&plain).unwrap();
let link = workspace.path().join("link");
std::os::unix::fs::symlink(&plain, &link).unwrap();
let key = link.join("id_ed25519");
let (first_allowed, second_refused, derived) = with_workspace_policy(workspace.path(), || {
let before = scope_memo::derivations();
let first_allowed = check_fs_path_scope(&key, FsAccess::Read).is_ok();
std::fs::remove_file(&link).unwrap();
std::os::unix::fs::symlink(home.path().join(".ssh"), &link).unwrap();
let second_refused = check_fs_path_scope(&key, FsAccess::Read).is_err();
(
first_allowed,
second_refused,
scope_memo::derivations() - before,
)
});
match saved_home {
Some(value) => std::env::set_var("HOME", value),
None => std::env::remove_var("HOME"),
}
assert!(first_allowed, "the link first points inside the workspace");
assert_eq!(derived, 1, "the second read reused the memoized roots");
assert!(second_refused, "a path re-linked to ~/.ssh must be refused");
}