//! The ledger's second step: an approval decides again and is recorded; a refusal and an expiry //! are recorded as denials. Do not edit. #[path = "support/build.rs"] mod build; #[path = "support/rig.rs"] mod rig; #[path = "support/sink.rs"] mod sink; #[path = "support/tmp.rs"] mod tmp; use brokerd::approvals::Verdict; use brokerd::ledger::{Answer, Answered, Decided, Grants, Ledger}; use brokerd::policy::{Ask, Label}; use build::{grant, now, read, set}; use proto::{ ApprovalAnswer, AuditEvent, CallId, DataClass, DecisionRecord, DenyReason, Mode, SessionId, }; use rig::Rig; fn asking() -> Grants { Ok(set(vec![grant("n", "read_file", Mode::Ask).paths(&["/n"])])) } fn pending(ledger: &Ledger) -> Ask { match ledger.decide(read("/n/deep/a"), &asking(), now()) { Decided::Ask { ask, seq: 0, .. } => ask, other => panic!("{other:?}"), } } fn bxctl() -> Answer { Answer::Approved { by: Some("bxctl".to_string()), } } fn denied(reason: DenyReason) -> DecisionRecord { DecisionRecord::Denied { reason } } fn verdict_reason(answered: &Answered) -> Option { match &answered.verdict { Verdict::Run(_) => None, Verdict::Denied(reason) => Some(*reason), } } /// The one `Approval` record, as (answer, by, reason, outcome, grant). type Row = ( ApprovalAnswer, Option, Option, DecisionRecord, Option, ); fn approval(rig: &Rig) -> Row { let events = rig.events(); assert_eq!(events.len(), 2, "a decision and one approval: {events:?}"); match &events[1] { AuditEvent::Approval { session, call, decision, answer, by, post, reason, outcome, grant, .. } => { assert_eq!(session, &SessionId::new("s1").unwrap()); assert_eq!((*call, *decision, post), (CallId(1), 0, &None)); ( *answer, by.clone(), reason.clone(), outcome.clone(), grant.clone(), ) } other => panic!("{other:?}"), } } #[test] fn an_approval_that_still_asks_lets_the_call_run() { let rig = Rig::new("answer-ask"); let ledger = rig.ledger(); let ask = pending(&ledger); let answered = ledger.answer(ask, 0, bxctl(), &asking(), now()); match &answered.verdict { Verdict::Run(decision) => assert_eq!(decision.grant(), "n"), other => panic!("{other:?}"), } assert_eq!(answered.outcome, DecisionRecord::Ask {}); let row = approval(&rig); assert_eq!( row, ( ApprovalAnswer::Approved, Some("bxctl".to_string()), None, DecisionRecord::Ask {}, Some("n".to_string()) ) ); } #[test] fn an_approval_under_a_grant_that_is_now_auto_records_allowed() { let rig = Rig::new("answer-auto"); let ledger = rig.ledger(); let ask = pending(&ledger); let now_auto = Ok(set(vec![ grant("n", "read_file", Mode::Auto).paths(&["/n"]), ])); let answered = ledger.answer(ask, 0, bxctl(), &now_auto, now()); assert!(matches!(answered.verdict, Verdict::Run(_))); assert_eq!(answered.outcome, DecisionRecord::Allowed {}); } #[test] fn the_approval_names_the_grant_matched_now() { let rig = Rig::new("answer-grant"); let ledger = rig.ledger(); let ask = pending(&ledger); let wider = Ok(set(vec![ grant("n", "read_file", Mode::Ask).paths(&["/n"]), grant("z-deep", "read_file", Mode::Ask).paths(&["/n/deep"]), ])); let answered = ledger.answer(ask, 0, bxctl(), &wider, now()); match &answered.verdict { Verdict::Run(decision) => assert_eq!(decision.grant(), "z-deep"), other => panic!("{other:?}"), } assert_eq!(approval(&rig).4.as_deref(), Some("z-deep")); } #[test] fn an_approval_after_the_grant_is_gone_is_denied() { let rig = Rig::new("answer-gone"); let ledger = rig.ledger(); let ask = pending(&ledger); let answered = ledger.answer(ask, 0, bxctl(), &Ok(set(Vec::new())), now()); assert_eq!(verdict_reason(&answered), Some(DenyReason::NoGrant)); assert_eq!(answered.outcome, denied(DenyReason::NoGrant)); let row = approval(&rig); assert_eq!( (row.0, row.3, row.4), (ApprovalAnswer::Approved, denied(DenyReason::NoGrant), None) ); } #[test] fn an_approval_after_the_taint_rose_is_denied() { let rig = Rig::new("answer-taint"); let ledger = rig.ledger(); let low = || { Ok(set(vec![ grant("n", "read_file", Mode::Ask) .paths(&["/n"]) .max_taint(DataClass::Private), ])) }; let ask = match ledger.decide(read("/n/a"), &low(), now()) { Decided::Ask { ask, .. } => ask, other => panic!("{other:?}"), }; // Another call of the session read a secret while this one waited. let s1 = SessionId::new("s1").unwrap(); let label = Label { class: DataClass::Secret, untrusted: false, }; rig.state() .raise(&s1, rig.state().read(&s1).unwrap(), label) .unwrap(); let answered = ledger.answer(ask, 0, bxctl(), &low(), now()); assert_eq!(verdict_reason(&answered), Some(DenyReason::TaintTooHigh)); match &rig.events()[1] { AuditEvent::Approval { taint, outcome, .. } => { assert_eq!(*taint, DataClass::Secret, "the state at the re-decision"); assert_eq!(*outcome, denied(DenyReason::TaintTooHigh)); } other => panic!("{other:?}"), } } #[test] fn an_approval_with_invalid_grants_is_denied() { let rig = Rig::new("answer-invalid"); let ledger = rig.ledger(); let ask = pending(&ledger); let answered = ledger.answer(ask, 0, bxctl(), &Err(Vec::new()), now()); assert_eq!(verdict_reason(&answered), Some(DenyReason::GrantsInvalid)); } #[test] fn a_refusal_is_recorded_with_the_owners_reason() { let rig = Rig::new("answer-refuse"); let ledger = rig.ledger(); let ask = pending(&ledger); let refused = Answer::Refused { by: Some("bxctl".to_string()), reason: Some("not now".to_string()), }; let answered = ledger.answer(ask, 0, refused, &asking(), now()); assert_eq!(verdict_reason(&answered), Some(DenyReason::ApprovalRefused)); assert_eq!( approval(&rig), ( ApprovalAnswer::Refused, Some("bxctl".to_string()), Some("not now".to_string()), denied(DenyReason::ApprovalRefused), None ) ); } #[test] fn an_expiry_is_recorded_by_nobody() { let rig = Rig::new("answer-expire"); let ledger = rig.ledger(); let ask = pending(&ledger); let answered = ledger.answer(ask, 0, Answer::Expired, &asking(), now()); assert_eq!(verdict_reason(&answered), Some(DenyReason::ApprovalExpired)); assert_eq!( approval(&rig), ( ApprovalAnswer::Expired, None, None, denied(DenyReason::ApprovalExpired), None ) ); } #[test] fn an_approval_that_cannot_be_recorded_does_not_run() { let rig = Rig::new("answer-norecord"); let ledger = rig.ledger(); let ask = pending(&ledger); rig.switch.fail(true); let answered = ledger.answer(ask, 0, bxctl(), &asking(), now()); assert_eq!( verdict_reason(&answered), Some(DenyReason::AuditUnavailable) ); assert_eq!(answered.outcome, denied(DenyReason::AuditUnavailable)); assert_eq!(rig.events().len(), 1, "only the decision"); assert!( !rig.lines .with("see docs/runbook.md#audit-unavailable") .is_empty() ); }