use openmoat_core::{DEFAULT_POLICY, EvalContext, Policy};
use super::*;
use crate::sandbox::{Grants, landlock};
fn shown(path: &str) -> Grant {
Grant {
path: path.to_owned(),
shown: true,
}
}
fn empty(path: &str) -> Grant {
Grant {
path: path.to_owned(),
shown: false,
}
}
#[test]
fn within_ignores_trailing_slashes_and_needs_whole_components() {
assert!(within("/tmp/x/.moat", "/tmp", false));
assert!(within("/tmp/x/.moat/", "/tmp/", false));
assert!(within("/tmp", "/tmp/", false));
assert!(within("/tmp/", "/tmp", false));
assert!(within("/anything", "/", false));
assert!(within("/", "/", false));
assert!(!within("/tmpfoo/.moat", "/tmp", false));
assert!(!within("/tmp", "/tmp/x", false));
assert!(!within("/", "/tmp", false));
}
#[test]
fn within_follows_the_file_systems_case() {
assert!(
!within("/Proj/.moat", "/proj", false),
"Linux: a different path"
);
assert!(
within("/Proj/.moat", "/proj", true),
"macOS, Windows: the same"
);
assert!(within("/proj/STATE", "/PROJ/", true));
}
#[test]
fn a_tree_holding_the_state_directory_exposes_it() {
let grants = [shown("/usr"), shown("/tmp"), shown("/home/me/proj")];
assert_eq!(
exposing("/tmp/ci/.moat", &grants, false),
Some(Exposure::Inside("/tmp".into()))
);
assert_eq!(
exposing("/home/me/proj/state/", &grants, false),
Some(Exposure::Inside("/home/me/proj".into()))
);
assert_eq!(exposing("/home/me/.moat", &grants, false), None);
assert_eq!(exposing("/home/me/projects/.moat", &grants, false), None);
}
#[test]
fn a_grant_inside_the_state_directory_exposes_it() {
let grants = [shown("/usr"), shown("/home/me/.moat/mcp")];
assert_eq!(
exposing("/home/me/.moat", &grants, false),
Some(Exposure::Holds("/home/me/.moat/mcp".into()))
);
let itself = [shown("/home/me/.moat/")];
assert_eq!(
exposing("/home/me/.moat", &itself, false),
Some(Exposure::Inside("/home/me/.moat/".into()))
);
}
#[test]
fn an_empty_tmpfs_hides_it_unless_something_is_bound_over_it() {
let same = [shown("/tmp"), empty("/tmp")];
assert_eq!(exposing("/tmp/ci/.moat", &same, false), None);
let deeper = [shown("/tmp"), empty("/tmp/user")];
assert_eq!(exposing("/tmp/user/.moat", &deeper, false), None);
assert_eq!(
exposing("/tmp/other/.moat", &deeper, false),
Some(Exposure::Inside("/tmp".into()))
);
let bound = [empty("/tmp"), shown("/tmp/ci/.moat/mcp")];
assert_eq!(
exposing("/tmp/ci/.moat", &bound, false),
Some(Exposure::Holds("/tmp/ci/.moat/mcp".into()))
);
}
#[test]
fn grants_are_compared_with_their_symlinks_resolved() {
let dir = tempfile::tempdir().unwrap();
let real = dir.path().join("real");
std::fs::create_dir_all(real.join("state")).unwrap();
let link = dir.path().join("link");
std::os::unix::fs::symlink(&real, &link).unwrap();
let rules = Rules {
read: vec![path_string(&link)],
..Rules::default()
};
let (state, exposure) = find(&link.join("state"), &rules, None).unwrap();
let real = resolved(&real);
assert_eq!(state, format!("{real}/state"));
assert_eq!(exposure, Exposure::Inside(real.clone()));
let rules = Rules {
read: vec![real.clone()],
..Rules::default()
};
assert!(find(&link.join("state"), &rules, None).is_some());
assert!(find(&dir.path().join("elsewhere"), &rules, None).is_none());
}
fn default_rules(moved_dirs: Vec<(String, String)>) -> Rules {
let policy = Policy::parse(DEFAULT_POLICY).unwrap();
let ctx = EvalContext {
home: "/home/me".into(),
project: Some("/home/me/proj".into()),
real_home: None,
real_project: None,
moved_dirs,
cwd: "/home/me/proj".into(),
case_insensitive_paths: false,
};
let grants = Grants {
proxy_port: Some(18080),
tmpdir: Some("/tmp".into()),
program: Some("/home/me/.local/bin/agent".into()),
writes: Vec::new(),
};
let ir = openmoat_core::ir::lower(&policy, &ctx).unwrap();
landlock::generate(&ir, &grants).unwrap().rules
}
#[test]
fn the_default_state_directory_is_outside_the_default_grants() {
let rules = default_rules(Vec::new());
let grants: Vec<Grant> = rules
.read
.iter()
.chain(&rules.write)
.map(|p| shown(p))
.collect();
assert_eq!(exposing("/home/me/.moat", &grants, false), None);
let moved = |dir: &str| vec![("~/.moat".to_owned(), dir.to_owned())];
for (dir, root) in [("/home/me/proj/state", "/home/me/proj"), ("/tmp/m", "/tmp")] {
let rules = default_rules(moved(dir));
let grants: Vec<Grant> = rules
.read
.iter()
.chain(&rules.write)
.map(|p| shown(p))
.collect();
assert_eq!(
exposing(dir, &grants, false),
Some(Exposure::Inside(root.into())),
"{dir}"
);
}
let rules = default_rules(moved("/srv/moat"));
let grants: Vec<Grant> = rules
.read
.iter()
.chain(&rules.write)
.map(|p| shown(p))
.collect();
assert_eq!(exposing("/srv/moat", &grants, false), None);
}
#[test]
fn the_refusal_names_the_root_and_offers_isolate_only_where_it_hides() {
let dir = tempfile::tempdir().unwrap();
let tmp = resolved(dir.path());
let project = format!("{tmp}/proj");
std::fs::create_dir_all(format!("{project}/state")).unwrap();
let rules = Rules {
read: vec!["/usr".into()],
write: vec![tmp.clone(), project.clone()],
connect_port: None,
};
let in_project = Path::new(&project).join("state");
let message = refusal(&in_project, &rules, Some(&tmp), false).unwrap();
assert!(
message.contains(&format!("Move MOAT_HOME out of {project}")),
"{message}"
);
assert!(!message.contains("--isolate"), "{message}");
assert!(refusal(&in_project, &rules, Some(&tmp), true).is_some());
let in_tmp = dir.path().join("moat");
let message = refusal(&in_tmp, &rules, Some(&tmp), false).unwrap();
assert!(
message.contains(&format!("Move MOAT_HOME out of {tmp}")),
"{message}"
);
assert!(message.contains("`moat run --isolate`"), "{message}");
assert_eq!(refusal(&in_tmp, &rules, Some(&tmp), true), None);
assert_eq!(
refusal(Path::new("/nowhere/granted"), &rules, Some(&tmp), false),
None
);
}