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)
118 lines
4.0 KiB
Rust
118 lines
4.0 KiB
Rust
//! Table tests for `policy::redecide`: an approval lets a call through only if the grants and
|
|
//! the session's state, as they are when it is approved, still say `ask` or `auto`. Do not edit.
|
|
|
|
#[path = "support/build.rs"]
|
|
mod build;
|
|
|
|
use brokerd::grants::GrantSet;
|
|
use brokerd::policy::{Ask, Label, Outcome, decide, redecide};
|
|
use build::{grant, now, private, read, secret, set};
|
|
use proto::{DataClass, DenyReason, Mode};
|
|
|
|
const PATH: &str = "/home/kyle/notes/a.md";
|
|
|
|
fn asking() -> build::Build {
|
|
grant("asks", "read_file", Mode::Ask).paths(&["/home/kyle/notes"])
|
|
}
|
|
|
|
/// An `Ask` for `PATH`, decided under `asking()` alone at `private`.
|
|
fn pending() -> Ask {
|
|
match decide(read(PATH), &set(vec![asking()]), private(), now()) {
|
|
Outcome::Ask(ask) => ask,
|
|
other => panic!("expected ask, got {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn still_ask_lets_the_call_run_under_the_same_grant() {
|
|
let decision = redecide(pending(), &set(vec![asking()]), private(), now()).unwrap();
|
|
assert_eq!(decision.grant(), "asks");
|
|
assert_eq!(decision.request(), &read(PATH));
|
|
assert_eq!(decision.matched_path(), Some("/home/kyle/notes"));
|
|
assert_eq!(
|
|
decision.label(),
|
|
Label {
|
|
class: DataClass::Private,
|
|
untrusted: true
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn auto_now_lets_the_call_run_under_the_grant_that_matches_now() {
|
|
// The owner has since replaced the ask grant with an auto grant of another name and label.
|
|
let grants = set(vec![
|
|
grant("now-auto", "read_file", Mode::Auto)
|
|
.paths(&["/home/kyle"])
|
|
.class(DataClass::Secret)
|
|
.trusted(),
|
|
]);
|
|
let decision = redecide(pending(), &grants, private(), now()).unwrap();
|
|
assert_eq!(decision.grant(), "now-auto");
|
|
assert_eq!(decision.grant_sha256(), proto::sha256(b"now-auto").unwrap());
|
|
assert_eq!(decision.matched_path(), Some("/home/kyle"));
|
|
assert_eq!(decision.paths(), ["/home/kyle"]);
|
|
assert_eq!(
|
|
decision.label(),
|
|
Label {
|
|
class: DataClass::Secret,
|
|
untrusted: false
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_grant_file_was_removed() {
|
|
let denial = redecide(pending(), &GrantSet::default(), private(), now()).unwrap_err();
|
|
assert_eq!(denial.reason, DenyReason::NoGrant);
|
|
assert_eq!(denial.grant, None);
|
|
}
|
|
|
|
#[test]
|
|
fn the_taint_rose_past_max_taint_while_the_approval_waited() {
|
|
let narrow = || asking().max_taint(DataClass::Private);
|
|
let ask = match decide(read(PATH), &set(vec![narrow()]), private(), now()) {
|
|
Outcome::Ask(ask) => ask,
|
|
other => panic!("expected ask, got {other:?}"),
|
|
};
|
|
let denial = redecide(ask, &set(vec![narrow()]), secret(), now()).unwrap_err();
|
|
assert_eq!(denial.reason, DenyReason::TaintTooHigh);
|
|
}
|
|
|
|
#[test]
|
|
fn the_grant_expired_while_the_approval_waited() {
|
|
let grants = set(vec![asking().expires("2026-09-18T12:10:00.000Z")]);
|
|
let ask = match decide(read(PATH), &grants, private(), now()) {
|
|
Outcome::Ask(ask) => ask,
|
|
other => panic!("expected ask, got {other:?}"),
|
|
};
|
|
assert_eq!(ask.expires(), Some(build::ts("2026-09-18T12:10:00.000Z")));
|
|
let later = build::ts("2026-09-18T12:10:00.000Z");
|
|
let denial = redecide(ask, &grants, private(), later).unwrap_err();
|
|
assert_eq!(denial.reason, DenyReason::GrantExpired);
|
|
}
|
|
|
|
#[test]
|
|
fn a_deny_grant_was_added_while_the_approval_waited() {
|
|
let grants = set(vec![
|
|
asking(),
|
|
grant("no-notes", "read_file", Mode::Deny).paths(&["/home/kyle"]),
|
|
]);
|
|
let denial = redecide(pending(), &grants, private(), now()).unwrap_err();
|
|
assert_eq!(denial.reason, DenyReason::DeniedByGrant);
|
|
assert_eq!(denial.grant.as_deref(), Some("no-notes"));
|
|
assert_eq!(
|
|
denial.grant_sha256,
|
|
Some(proto::sha256(b"no-notes").unwrap())
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_grants_now_cover_other_arguments_only() {
|
|
let grants = set(vec![
|
|
grant("asks", "read_file", Mode::Ask).paths(&["/home/kyle/other"]),
|
|
]);
|
|
let denial = redecide(pending(), &grants, private(), now()).unwrap_err();
|
|
assert_eq!(denial.reason, DenyReason::NoGrant);
|
|
}
|