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