use std::path::{Path, PathBuf};
use std::process::Command;
use super::*;
const SECRET: &str = "synthetic-denied-secret";
struct Fixture {
_root: tempfile::TempDir,
workspace: PathBuf,
denied: PathBuf,
}
fn fixture() -> Fixture {
let root = tempfile::tempdir().expect("fixture root");
let workspace = std::fs::canonicalize(root.path())
.expect("canonical root")
.join("workspace");
let denied = workspace.join("a/dir/secret.json");
std::fs::create_dir_all(denied.parent().unwrap()).expect("denied parent");
std::fs::write(&denied, SECRET).expect("denied file");
std::fs::write(workspace.join("a.txt"), "a").expect("source file");
Fixture {
_root: root,
workspace,
denied,
}
}
fn profile(fixture: &Fixture) -> String {
let policy = CapabilityPolicy {
sandbox_profile: SandboxProfile::Worktree,
workspace_roots: vec![fixture.workspace.display().to_string()],
process_sandbox: Box::new(crate::orchestration::ProcessSandboxPolicy {
read_deny_roots: vec![fixture.denied.display().to_string()],
..Default::default()
}),
..CapabilityPolicy::default()
};
render_profile(&policy)
}
fn run(profile: &str, workspace: &Path, script: &str) -> String {
let output = Command::new(SANDBOX_EXEC_PATH)
.args(["-p", profile, "--", "/bin/sh", "-c", script])
.current_dir(workspace)
.output()
.expect("run sandbox-exec");
String::from_utf8_lossy(&output.stdout).into_owned()
}
const RENAMES: &[(&str, &str)] = &[
("the file", "mv a/dir/secret.json moved; cat moved"),
("its directory", "mv a/dir moved; cat moved/secret.json"),
(
"the directory above",
"mv a moved; cat moved/dir/secret.json",
),
];
#[test]
fn a_denied_file_inside_a_writable_root_cannot_be_renamed_out_of_its_denial() {
let fixture = fixture();
let profile = profile(&fixture);
for (moved, script) in RENAMES {
let stdout = run(&profile, &fixture.workspace, script);
assert!(
!stdout.contains(SECRET),
"renaming {moved} exposed the denied file"
);
assert!(
fixture.denied.exists(),
"renaming {moved} moved the denied file"
);
}
let stdout = run(
&profile,
&fixture.workspace,
"cp a.txt b.txt && touch a/dir/created && rm a/dir/created && echo created",
);
assert_eq!(stdout.trim(), "created");
}
#[test]
fn without_the_unlink_rules_each_rename_reaches_the_denied_file() {
for (moved, script) in RENAMES {
let fixture = fixture();
let read_deny_only = profile(&fixture)
.lines()
.filter(|line| !line.contains("file-write-unlink"))
.collect::<Vec<_>>()
.join("\n");
let stdout = run(&read_deny_only, &fixture.workspace, script);
assert!(
stdout.contains(SECRET),
"renaming {moved} did not reach the secret even without the unlink rules, so \
the test above proves nothing"
);
}
}