//! Table tests for `policy::decide`: which arguments each tool's grants cover. Do not edit. //! `policy_matching.rs` covers how a winner, a label and a reason are picked, `policy_redecide.rs` //! covers approvals, and `policy_property.rs` checks all of it against an oracle. #[path = "support/build.rs"] mod build; use brokerd::policy::decide; use build::{allowed, fetch, grant, now, private, read, reason, request, set, shell, write}; use proto::{DenyReason, Mode}; #[test] fn no_grants_means_no_grant() { let none = set(vec![]); for req in [ read("/etc/hosts"), write("/tmp/x"), shell(None), fetch("https://example.com/"), ] { assert_eq!( reason(decide(req, &none, private(), now())), DenyReason::NoGrant ); } } #[test] fn a_tool_that_is_not_one_of_the_four_is_no_grant_and_its_arguments_are_not_parsed() { let grants = set(vec![grant("s", "shell", Mode::Auto)]); for tool in ["echo", "clock", "call_tool", "", "Shell"] { for arguments in ["{}", "not json", r#"{"command":"ls"}"#] { let outcome = decide(request(tool, arguments), &grants, private(), now()); assert_eq!( reason(outcome), DenyReason::NoGrant, "{tool:?} {arguments:?}" ); } } } /// Invalid arguments are refused before matching, so the answer is the same with a grant that /// would cover them, with a `deny` grant, and with no grant at all. #[test] fn invalid_arguments_are_refused_before_matching() { let covering = set(vec![ grant("r", "read_file", Mode::Auto).paths(&["/home/kyle/notes"]), grant("no", "read_file", Mode::Deny).paths(&["/home/kyle"]), ]); let none = set(vec![]); for grants in [&covering, &none] { for req in [ // The rows of the "Paths" table that are about form. read("/home/kyle/notes/../.ssh/id"), read("notes/a.md"), read("/home/kyle//notes/./a.md"), request("read_file", "{}"), request( "read_file", r#"{"path":"/home/kyle/notes/a.md","mode":"r"}"#, ), request("read_file", "not json"), write("/home/kyle/notes/"), shell(Some("relative")), fetch("http://example.com/"), fetch("https://127.0.0.1/"), fetch("https://user@example.com/"), ] { let text = req.arguments.clone(); assert_eq!( reason(decide(req, grants, private(), now())), DenyReason::InvalidArguments, "{text}" ); } } } /// The "Paths" table, the rows about containment. #[test] fn read_file_is_covered_inside_a_granted_path() { let grants = set(vec![ grant("notes", "read_file", Mode::Auto).paths(&["/home/kyle/notes"]), ]); for path in [ "/home/kyle/notes/a.md", "/home/kyle/notes", "/home/kyle/notes/x/y/z", ] { let d = allowed(decide(read(path), &grants, private(), now())); assert_eq!(d.grant(), "notes"); assert_eq!(d.matched_path(), Some("/home/kyle/notes")); } for path in [ "/home/kyle/notes2/a.md", "/home/kyle", "/", "/etc/passwd", "/home/kyle/note", ] { assert_eq!( reason(decide(read(path), &grants, private(), now())), DenyReason::NoGrant, "{path}" ); } // A grant is for one tool. let outcome = decide(write("/home/kyle/notes/a.md"), &grants, private(), now()); assert_eq!(reason(outcome), DenyReason::NoGrant); } #[test] fn write_file_is_covered_inside_a_granted_path_but_not_at_the_path_itself() { let grants = set(vec![ grant("scratch", "write_file", Mode::Auto) .paths(&["/home/kyle/scratch", "/home/kyle/scratch/out"]), ]); let d = allowed(decide( write("/home/kyle/scratch/a.txt"), &grants, private(), now(), )); assert_eq!(d.matched_path(), Some("/home/kyle/scratch")); // The longest path that holds the argument is the matched one. let d = allowed(decide( write("/home/kyle/scratch/out/b.txt"), &grants, private(), now(), )); assert_eq!(d.matched_path(), Some("/home/kyle/scratch/out")); // A granted path itself cannot be written, but it can lie inside another granted path. let d = allowed(decide( write("/home/kyle/scratch/out"), &grants, private(), now(), )); assert_eq!(d.matched_path(), Some("/home/kyle/scratch")); let outcome = decide(write("/home/kyle/scratch"), &grants, private(), now()); assert_eq!(reason(outcome), DenyReason::NoGrant); } #[test] fn shell_is_covered_by_no_paths_and_no_cwd_or_by_a_cwd_inside_a_path() { let bare = set(vec![grant("bare", "shell", Mode::Auto)]); let d = allowed(decide(shell(None), &bare, private(), now())); assert_eq!((d.matched_path(), d.paths().len()), (None, 0)); assert_eq!( reason(decide(shell(Some("/home/kyle")), &bare, private(), now())), DenyReason::NoGrant ); let scoped = set(vec![ grant("scoped", "shell", Mode::Auto).paths(&["/home/kyle/a", "/srv/b"]), ]); let d = allowed(decide(shell(Some("/srv/b/sub")), &scoped, private(), now())); assert_eq!(d.matched_path(), Some("/srv/b")); // The runner mounts every path of the grant, so the decision carries them all. assert_eq!(d.paths(), ["/home/kyle/a", "/srv/b"]); assert_eq!( reason(decide(shell(None), &scoped, private(), now())), DenyReason::NoGrant ); assert_eq!( reason(decide(shell(Some("/srv")), &scoped, private(), now())), DenyReason::NoGrant ); } /// The "Hosts" table, row by row. #[test] fn http_fetch_is_covered_when_the_host_matches() { let exact = set(vec![ grant("exact", "http_fetch", Mode::Auto).hosts(&["example.com"]), ]); let wild = set(vec![ grant("wild", "http_fetch", Mode::Auto).hosts(&["*.example.com"]), ]); let d = allowed(decide( fetch("https://example.com/a?b=c"), &exact, private(), now(), )); assert_eq!(d.hosts(), ["example.com"]); assert_eq!(d.matched_path(), None); assert_eq!( reason(decide( fetch("https://www.example.com/"), &exact, private(), now() )), DenyReason::NoGrant ); for url in ["https://www.example.com/", "https://a.b.example.com:443/x"] { allowed(decide(fetch(url), &wild, private(), now())); } for url in [ "https://example.com/", "https://badexample.com/", "https://example.com.evil.org/", ] { assert_eq!( reason(decide(fetch(url), &wild, private(), now())), DenyReason::NoGrant, "{url}" ); } }