//! Table tests for `policy::decide`: among the grants that cover a call, which one wins, what //! the result is labelled, and which reason is given when none is left. Do not edit. #[path = "support/build.rs"] mod build; use brokerd::policy::{Label, decide}; use build::{ allowed, asked, denied, fetch, grant, now, private, read, reason, request, secret, set, shell, }; use proto::{DataClass, DenyReason, Mode}; #[test] fn the_most_restrictive_mode_wins_among_three_matching_grants() { let paths = &["/home/kyle/notes"]; let auto = || grant("b-auto", "read_file", Mode::Auto).paths(paths); let ask = || grant("c-ask", "read_file", Mode::Ask).paths(paths); let deny = || grant("a-deny", "read_file", Mode::Deny).paths(paths); let req = || read("/home/kyle/notes/a.md"); let denial = denied(decide( req(), &set(vec![auto(), ask(), deny()]), private(), now(), )); assert_eq!(denial.reason, DenyReason::DeniedByGrant); assert_eq!(denial.grant.as_deref(), Some("a-deny")); assert_eq!(denial.grant_sha256, Some(deny().done().sha256)); let ask_wins = asked(decide(req(), &set(vec![auto(), ask()]), private(), now())); assert_eq!(ask_wins.grant(), "c-ask"); assert_eq!( allowed(decide(req(), &set(vec![auto()]), private(), now())).grant(), "b-auto" ); // Deny beats a longer path and a lower id: the mode comes first. let narrow_auto = grant("a-auto", "read_file", Mode::Auto).paths(&["/home/kyle/notes/deep"]); let wide_deny = grant("z-deny", "read_file", Mode::Deny).paths(&["/home"]); let outcome = decide( read("/home/kyle/notes/deep/x"), &set(vec![narrow_auto, wide_deny]), private(), now(), ); assert_eq!(denied(outcome).grant.as_deref(), Some("z-deny")); } #[test] fn within_a_mode_the_longest_matched_path_wins_and_then_the_lowest_id() { let grants = set(vec![ grant("a-wide", "read_file", Mode::Auto).paths(&["/home/kyle"]), grant("z-narrow", "read_file", Mode::Auto).paths(&["/home/kyle/notes"]), ]); let d = allowed(decide( read("/home/kyle/notes/a.md"), &grants, private(), now(), )); assert_eq!( (d.grant(), d.matched_path()), ("z-narrow", Some("/home/kyle/notes")) ); let d = allowed(decide(read("/home/kyle/other"), &grants, private(), now())); assert_eq!(d.grant(), "a-wide"); // Equal paths: the lowest id in byte order, whatever order the grants were given in. let tie = set(vec![ grant("g-10", "read_file", Mode::Auto).paths(&["/srv"]), grant("g-2", "read_file", Mode::Auto).paths(&["/srv"]), grant("g-1z", "read_file", Mode::Auto).paths(&["/srv"]), ]); assert_eq!( allowed(decide(read("/srv/x"), &tie, private(), now())).grant(), "g-10" ); // Grants with no matched path all tie, so the id decides. let hosts = set(vec![ grant("m", "http_fetch", Mode::Auto).hosts(&["*.example.com"]), grant("b", "http_fetch", Mode::Auto).hosts(&["www.example.com"]), ]); let d = allowed(decide( fetch("https://www.example.com/"), &hosts, private(), now(), )); assert_eq!(d.grant(), "b"); } /// The example in the spec. Whichever id sorts first, the read is labelled `secret`, and the /// narrower grant is the one recorded and mounted. #[test] fn the_label_is_combined_over_every_matching_grant() { for (home, keys) in [("a-home", "b-keys"), ("z-home", "b-keys")] { let grants = set(vec![ grant(home, "read_file", Mode::Auto) .paths(&["/home/kyle"]) .trusted(), grant(keys, "read_file", Mode::Auto) .paths(&["/home/kyle/keys"]) .class(DataClass::Secret) .trusted(), ]); let d = allowed(decide( read("/home/kyle/keys/id"), &grants, private(), now(), )); assert_eq!(d.grant(), keys); assert_eq!( d.label(), Label { class: DataClass::Secret, untrusted: false } ); // Outside `keys` only the wide grant matches, so only its label counts. let d = allowed(decide(read("/home/kyle/todo"), &grants, private(), now())); assert_eq!( d.label(), Label { class: DataClass::Private, untrusted: false } ); } // The winner says trusted and public; another matching grant says otherwise, and it counts. let grants = set(vec![ grant("narrow", "read_file", Mode::Auto) .paths(&["/srv/pub/docs"]) .class(DataClass::Public) .trusted(), grant("wide", "read_file", Mode::Auto) .paths(&["/srv/pub"]) .class(DataClass::Private), ]); let d = allowed(decide(read("/srv/pub/docs/x"), &grants, private(), now())); assert_eq!(d.grant(), "narrow"); assert_eq!( d.label(), Label { class: DataClass::Private, untrusted: true } ); // An `ask` winner carries the combined label too. let grants = set(vec![ grant("asks", "read_file", Mode::Ask) .paths(&["/srv"]) .class(DataClass::Public) .trusted(), grant("labels", "read_file", Mode::Auto) .paths(&["/srv"]) .class(DataClass::Secret), ]); let ask = asked(decide(read("/srv/x"), &grants, private(), now())); assert_eq!( ask.label(), Label { class: DataClass::Secret, untrusted: true } ); } #[test] fn a_grant_ruled_out_by_taint_or_expiry_adds_nothing_to_the_label() { let grants = set(vec![ grant("live", "read_file", Mode::Auto) .paths(&["/srv"]) .class(DataClass::Public) .trusted(), grant("old", "read_file", Mode::Auto) .paths(&["/srv"]) .class(DataClass::Secret) .expires("2026-01-01T00:00:00.000Z"), ]); let d = allowed(decide(read("/srv/x"), &grants, private(), now())); assert_eq!( d.label(), Label { class: DataClass::Public, untrusted: false } ); } #[test] fn a_grant_expires_exactly_at_its_time() { let at = |when: &str| set(vec![grant("g", "shell", Mode::Auto).expires(when)]); let d = allowed(decide( shell(None), &at("2026-09-18T12:00:00.001Z"), private(), now(), )); assert_eq!(d.expires(), Some(build::ts("2026-09-18T12:00:00.001Z"))); for when in [ "2026-09-18T12:00:00.000Z", "2026-09-18T11:59:59.999Z", "2020-01-01T00:00:00.000Z", ] { assert_eq!( reason(decide(shell(None), &at(when), private(), now())), DenyReason::GrantExpired, "{when}" ); } let never = set(vec![grant("g", "shell", Mode::Auto)]); assert_eq!( allowed(decide(shell(None), &never, private(), now())).expires(), None ); } #[test] fn a_grant_applies_up_to_its_max_taint() { let grants = set(vec![ grant("g", "shell", Mode::Auto).max_taint(DataClass::Private), ]); allowed(decide(shell(None), &grants, private(), now())); allowed(decide( shell(None), &grants, build::at(DataClass::Public), now(), )); assert_eq!( reason(decide(shell(None), &grants, secret(), now())), DenyReason::TaintTooHigh ); // The untrusted flag is not an input to matching. let mut state = private(); state.untrusted = true; allowed(decide(shell(None), &grants, state, now())); } #[test] fn the_reason_when_nothing_is_left() { let expired = || grant("e", "shell", Mode::Auto).expires("2026-01-01T00:00:00.000Z"); let tainted = || grant("t", "shell", Mode::Auto).max_taint(DataClass::Private); let both = || { grant("b", "shell", Mode::Auto) .expires("2026-01-01T00:00:00.000Z") .max_taint(DataClass::Private) }; let cases = [ // One candidate expired and another too tainted: expiry is reported first. (set(vec![expired(), tainted()]), DenyReason::GrantExpired), (set(vec![tainted(), both()]), DenyReason::TaintTooHigh), // Ruled out by both is ruled out "only" by neither. (set(vec![both()]), DenyReason::NoGrant), (set(vec![expired()]), DenyReason::GrantExpired), ]; for (grants, want) in cases { assert_eq!(reason(decide(shell(None), &grants, secret(), now())), want); } // "Only by expiry" means it would have matched: an expired grant for other arguments, or // for another tool, is no reason to say `grant_expired`. let elsewhere = set(vec![ grant("p", "read_file", Mode::Auto) .paths(&["/srv"]) .expires("2026-01-01T00:00:00.000Z"), grant("w", "write_file", Mode::Auto) .paths(&["/home"]) .expires("2026-01-01T00:00:00.000Z"), ]); assert_eq!( reason(decide(read("/home/kyle/x"), &elsewhere, private(), now())), DenyReason::NoGrant ); } #[test] fn a_deny_grant_denies_at_every_taint_until_it_expires() { let grants = |deny_expires: Option<&str>| { let deny = grant("no-internal", "http_fetch", Mode::Deny).hosts(&["internal.example.com"]); let deny = match deny_expires { Some(when) => deny.expires(when), None => deny, }; set(vec![ grant("any", "http_fetch", Mode::Auto).hosts(&["*.example.com"]), deny, ]) }; let url = "https://internal.example.com/"; for state in [build::at(DataClass::Public), private(), secret()] { let denial = denied(decide(fetch(url), &grants(None), state, now())); assert_eq!(denial.reason, DenyReason::DeniedByGrant); assert_eq!(denial.grant.as_deref(), Some("no-internal")); } allowed(decide( fetch("https://www.example.com/"), &grants(None), secret(), now(), )); // An expired deny no longer denies: `expires` on a deny grant means "forbid this until then". let lapsed = grants(Some("2026-09-18T12:00:00.000Z")); assert_eq!( allowed(decide(fetch(url), &lapsed, private(), now())).grant(), "any" ); } /// Documented, not liked: an `ask` grant with a lower `max_taint` than an `auto` grant over the /// same arguments drops out when taint rises, and the call then runs without asking. #[test] fn an_ask_grant_with_a_lower_max_taint_stops_asking_when_taint_rises() { let grants = set(vec![ grant("asks", "shell", Mode::Ask).max_taint(DataClass::Private), grant("runs", "shell", Mode::Auto), ]); assert_eq!( asked(decide(shell(None), &grants, private(), now())).grant(), "asks" ); assert_eq!( allowed(decide(shell(None), &grants, secret(), now())).grant(), "runs" ); } #[test] fn a_decision_and_an_ask_carry_what_the_broker_and_the_runner_need() { let grants = set(vec![ grant("asks", "write_file", Mode::Ask) .paths(&["/home/kyle/scratch"]) .expires("2027-01-01T00:00:00.000Z") .class(DataClass::Public), ]); let req = request( "write_file", r#"{ "content": "hello", "path": "/home/kyle/scratch/a.txt" }"#, ); let ask = asked(decide(req.clone(), &grants, private(), now())); assert_eq!(ask.request(), &req); assert_eq!( ask.args().canonical_json(), r#"{"path":"/home/kyle/scratch/a.txt","content":"hello"}"# ); assert_eq!(ask.grant(), "asks"); assert_eq!(ask.grant_sha256(), proto::sha256(b"asks").unwrap()); assert_eq!(ask.matched_path(), Some("/home/kyle/scratch")); assert_eq!(ask.paths(), ["/home/kyle/scratch"]); assert!(ask.hosts().is_empty()); assert_eq!(ask.expires(), Some(build::ts("2027-01-01T00:00:00.000Z"))); assert_eq!( ask.label(), Label { class: DataClass::Public, untrusted: true } ); }