263 lines
7.7 KiB
Rust
263 lines
7.7 KiB
Rust
//! 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<DenyReason> {
|
|
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<String>,
|
|
Option<String>,
|
|
DecisionRecord,
|
|
Option<String>,
|
|
);
|
|
|
|
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()
|
|
);
|
|
}
|