231 lines
7.4 KiB
Rust
231 lines
7.4 KiB
Rust
//! A call an `ask` grant matched: the pending frame, the table entry, the wait, and a requester
|
|
//! that goes away. The tests answer entries by hand, as `admin` will. Do not edit.
|
|
|
|
#[path = "support/client.rs"]
|
|
mod client;
|
|
#[path = "support/rig.rs"]
|
|
mod rig;
|
|
#[path = "support/runtime.rs"]
|
|
mod runtime;
|
|
#[path = "support/sink.rs"]
|
|
mod sink;
|
|
#[path = "support/tmp.rs"]
|
|
mod tmp;
|
|
|
|
use std::os::unix::net::UnixStream;
|
|
use std::sync::Arc;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use brokerd::broker::{self, Broker, GONE};
|
|
use brokerd::ledger::Answer;
|
|
use client::{Serve, next, open};
|
|
use proto::{
|
|
AuditEvent, DataClass, DecisionRecord, DenyReason, Message, ResultStatus, Timestamp,
|
|
ToolResponse,
|
|
};
|
|
use rig::{Rig, grant_text, request};
|
|
use runtime::Recording;
|
|
|
|
fn ask_notes(rig: &Rig, extra: &str) {
|
|
rig.grant(
|
|
"notes",
|
|
&grant_text("read_file", "ask", extra, "paths = [\"/n\"]"),
|
|
);
|
|
}
|
|
|
|
/// Sends a call and reads its pending frame. Returns the connection and the frame's values.
|
|
fn start(broker: &Arc<Broker>, arguments: &str) -> (UnixStream, u64, Timestamp) {
|
|
let req = request("s1", 9, "read_file", arguments);
|
|
let mut stream = open(broker, broker::handle, 9, Message::ToolRequest(req));
|
|
let env = next(&mut stream);
|
|
assert_eq!((env.id, env.r#final), (9, false), "{env:?}");
|
|
match env.msg {
|
|
Message::ToolResponse(ToolResponse::PendingApproval { approval, expires }) => {
|
|
(stream, approval, expires)
|
|
}
|
|
other => panic!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
/// What `admin` does for an entry it took: record the answer, send the verdict.
|
|
fn answer_by_hand(broker: &Broker, id: u64, answer: Answer) -> DecisionRecord {
|
|
let entry = broker.table().take(id).expect("the entry is pending");
|
|
answer_entry(broker, entry, answer)
|
|
}
|
|
|
|
fn answer_entry(
|
|
broker: &Broker,
|
|
entry: brokerd::approvals::Entry,
|
|
answer: Answer,
|
|
) -> DecisionRecord {
|
|
let grants = broker.grants();
|
|
let done = broker.ledger().answer(
|
|
entry.ask,
|
|
entry.info.approval,
|
|
answer,
|
|
&grants,
|
|
Timestamp::now(),
|
|
);
|
|
entry.reply.send(done.verdict).unwrap();
|
|
done.outcome
|
|
}
|
|
|
|
fn approved() -> Answer {
|
|
Answer::Approved {
|
|
by: Some("bxctl".to_string()),
|
|
}
|
|
}
|
|
|
|
fn final_answer(stream: &mut UnixStream) -> ToolResponse {
|
|
let env = next(stream);
|
|
assert!(env.r#final, "{env:?}");
|
|
match env.msg {
|
|
Message::ToolResponse(r) => r,
|
|
other => panic!("{other:?}"),
|
|
}
|
|
}
|
|
|
|
fn eventually(what: &str, mut done: impl FnMut() -> bool) {
|
|
let until = Instant::now() + Duration::from_secs(10);
|
|
while !done() {
|
|
assert!(Instant::now() < until, "never happened: {what}");
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn an_ask_call_waits_is_listed_and_runs_when_approved() {
|
|
let rig = Rig::new("pending-approve");
|
|
ask_notes(&rig, "");
|
|
let rt = Recording::answering("the notes");
|
|
let broker = rig.broker(&rt);
|
|
let before = Timestamp::now();
|
|
// Spaced JSON: the table shows the arguments as policy parsed them, not as sent.
|
|
let (mut stream, approval, expires) = start(&broker, r#"{ "path" : "/n/a" }"#);
|
|
assert_eq!(approval, 0, "the approval id is the decision's seq");
|
|
let items = broker.table().list();
|
|
assert_eq!(items.len(), 1);
|
|
let item = &items[0];
|
|
assert_eq!(item.approval, 0);
|
|
assert_eq!((item.session.as_str(), item.call.0), ("s1", 9));
|
|
assert_eq!(item.tool, "read_file");
|
|
assert_eq!(item.arguments, r#"{"path":"/n/a"}"#);
|
|
assert_eq!(
|
|
(item.grant.as_str(), item.taint),
|
|
("notes", DataClass::Private)
|
|
);
|
|
assert!(item.created >= before);
|
|
assert_eq!(item.expires, expires);
|
|
assert_eq!(
|
|
expires.unix_millis() - item.created.unix_millis(),
|
|
900_000,
|
|
"now plus ttl_ms"
|
|
);
|
|
assert_eq!(rt.count(), 0, "nothing runs while it waits");
|
|
|
|
assert_eq!(
|
|
answer_by_hand(&broker, 0, approved()),
|
|
DecisionRecord::Ask {}
|
|
);
|
|
assert!(matches!(
|
|
final_answer(&mut stream),
|
|
ToolResponse::Result { .. }
|
|
));
|
|
assert_eq!(rt.count(), 1);
|
|
let events = rig.events();
|
|
assert!(
|
|
matches!(
|
|
events.as_slice(),
|
|
[
|
|
AuditEvent::Decision { .. },
|
|
AuditEvent::Approval { decision: 0, .. },
|
|
AuditEvent::Result { decision: 0, .. }
|
|
]
|
|
),
|
|
"{events:?}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_expiry_is_the_grants_when_that_is_earlier() {
|
|
let rig = Rig::new("pending-grant-expiry");
|
|
let soon = Timestamp::from_unix_millis(Timestamp::now().unix_millis() + 60_000).unwrap();
|
|
ask_notes(&rig, &format!("expires = \"{}\"", soon.to_rfc3339()));
|
|
let rt = Recording::answering("x");
|
|
let broker = rig.broker(&rt);
|
|
let (_stream, _, expires) = start(&broker, r#"{"path":"/n/a"}"#);
|
|
assert_eq!(expires, soon);
|
|
}
|
|
|
|
#[test]
|
|
fn a_denied_verdict_reaches_the_requester_and_nothing_runs() {
|
|
let rig = Rig::new("pending-refuse");
|
|
ask_notes(&rig, "");
|
|
let rt = Recording::answering("x");
|
|
let broker = rig.broker(&rt);
|
|
let (mut stream, approval, _) = start(&broker, r#"{"path":"/n/a"}"#);
|
|
let refused = Answer::Refused {
|
|
by: Some("bxctl".to_string()),
|
|
reason: None,
|
|
};
|
|
answer_by_hand(&broker, approval, refused);
|
|
assert_eq!(
|
|
final_answer(&mut stream),
|
|
ToolResponse::Denied {
|
|
reason: DenyReason::ApprovalRefused
|
|
}
|
|
);
|
|
assert_eq!(rt.count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_requester_that_leaves_takes_its_own_entry_and_nothing_is_written() {
|
|
let rig = Rig::new("pending-leave");
|
|
ask_notes(&rig, "");
|
|
let rt = Recording::answering("x");
|
|
let broker = rig.broker(&rt);
|
|
let (stream, _, _) = start(&broker, r#"{"path":"/n/a"}"#);
|
|
drop(stream);
|
|
// The waiting thread looks at its connection every second.
|
|
eventually("the entry is removed", || broker.table().list().is_empty());
|
|
std::thread::sleep(Duration::from_millis(200));
|
|
assert_eq!(rig.events().len(), 1, "only the decision");
|
|
let report = brokerd::audit::verify_dir(&rig.cfg.audit_dir()).unwrap();
|
|
assert_eq!(report.abandoned, [0]);
|
|
assert!(report.failure.is_none());
|
|
assert_eq!(rt.count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn a_requester_gone_at_the_last_look_runs_nothing_and_closes_the_call() {
|
|
let rig = Rig::new("pending-lastlook");
|
|
ask_notes(&rig, "");
|
|
let rt = Recording::answering("x");
|
|
let broker = rig.broker(&rt);
|
|
let (stream, approval, _) = start(&broker, r#"{"path":"/n/a"}"#);
|
|
// Take the entry first, so the waiting thread cannot take it back when it sees the
|
|
// connection gone; then approve.
|
|
let entry = broker.table().take(approval).unwrap();
|
|
drop(stream);
|
|
answer_entry(&broker, entry, approved());
|
|
eventually("the call is closed", || rig.events().len() == 3);
|
|
match &rig.events()[2] {
|
|
AuditEvent::Result {
|
|
status,
|
|
decision,
|
|
sha256,
|
|
bytes,
|
|
..
|
|
} => {
|
|
assert_eq!((*status, *decision), (ResultStatus::Failed, 0));
|
|
assert_eq!(*sha256, proto::sha256(GONE.as_bytes()).unwrap());
|
|
assert_eq!(*bytes, GONE.len() as u64);
|
|
}
|
|
other => panic!("{other:?}"),
|
|
}
|
|
assert_eq!(GONE, "the requester went away");
|
|
assert_eq!(rt.count(), 0);
|
|
let report = brokerd::audit::verify_dir(&rig.cfg.audit_dir()).unwrap();
|
|
assert!(report.unfinished.is_empty() && report.abandoned.is_empty());
|
|
}
|