Files
boxmaker/crates/brokerd/tests/policy.rs
T
kyle e1e6c7a338 Decide tool calls against grants, taint and time
Implemented decide and redecide in crates/brokerd/src/policy.rs:
SessionState, Label, Denial, private Matched, and Decision/Ask (private
fields, Debug only, nine getters each) with the Outcome enum. decide
rejects an unknown tool (args not parsed) and malformed arguments before
matching, then runs the M1-M5 matching pass in id order and returns
Allowed/Ask/Denied by the winner's mode; redecide re-runs matching now
and rebuilds the Decision from the Ask. Seven doctests (six compile_fail,
one compiling) guard the two facts. policy 7, policy_matching 10,
policy_redecide 7, policy_property 4, doc 7 all pass; make gate ok.

Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
2026-09-19 03:05:01 -07:00

214 lines
6.9 KiB
Rust

//! 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}"
);
}
}