Add the ledger: the audit writer and session state behind one lock
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,262 @@
|
||||
//! 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()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user