Handle approvals, refusals and grant checks on admin.sock
Implemented-By: OpenCode session (model recorded in docs/implementer-log.md)
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
//! One `admin.sock` connection: list, approve, refuse, check the grants; and expiry, which answers
|
||||
//! an approval the same way. Whoever takes an entry out of the table answers it.
|
||||
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use crate::approvals::Entry;
|
||||
use crate::broker::{Broker, forbid, read_request, send};
|
||||
use crate::grants;
|
||||
use crate::ledger::{Answer, Answered};
|
||||
use proto::{
|
||||
ApprovalList, Approve, ApproveResult, DecisionRecord, DenyReason, Empty, ErrorCode,
|
||||
GrantsReport, Message, Refuse, Timestamp, WireError,
|
||||
};
|
||||
|
||||
/// The Approval record's `by` for answers through admin.sock.
|
||||
pub const BY: &str = "bxctl";
|
||||
/// The refusal `bxctl` sees when the ledger could not record it.
|
||||
pub const REFUSAL_INTERNAL: &str =
|
||||
"the refusal could not be recorded; the call is denied; see docs/runbook.md#audit-unavailable";
|
||||
|
||||
/// Whoever takes an entry out of the table answers it: the ledger records the answer, then the
|
||||
/// verdict goes to the waiting thread, and only then is the record returned.
|
||||
pub fn answer(broker: &Broker, entry: Entry, answer: Answer, now: Timestamp) -> DecisionRecord {
|
||||
// 1. The grants as they are now: an approval decides again with them.
|
||||
let grants = broker.grants();
|
||||
|
||||
// 2. Record the answer; the ledger writes the Approval record and returns the verdict and
|
||||
// outcome together, or the audit-unavailable denial when it could not record.
|
||||
let Entry { info, ask, reply } = entry;
|
||||
let Answered { verdict, outcome } =
|
||||
broker
|
||||
.ledger()
|
||||
.answer(ask, info.approval, answer, &grants, now);
|
||||
|
||||
// 3. The record is written either way; if the waiting thread is gone, there is nothing else to do.
|
||||
if reply.send(verdict).is_err() {
|
||||
broker.log(&format!(
|
||||
"brokerd: approval {} was answered after its requester had gone",
|
||||
info.approval
|
||||
));
|
||||
}
|
||||
|
||||
// 4. The outcome the caller reports to bxctl.
|
||||
outcome
|
||||
}
|
||||
|
||||
/// Answer every approval whose time has run out, the same way an owner's refusal would.
|
||||
pub fn expire_due(broker: &Broker, now: Timestamp) -> usize {
|
||||
let entries = broker.table().take_expired(now);
|
||||
let count = entries.len();
|
||||
for entry in entries {
|
||||
answer(broker, entry, Answer::Expired, now);
|
||||
}
|
||||
count
|
||||
}
|
||||
|
||||
/// Serve one `admin.sock` connection from bxctl: one request, one final answer.
|
||||
pub fn handle(mut stream: UnixStream, broker: &Broker) {
|
||||
// 1. The peer is gone before it speaks, or its frame is malformed: answer and return.
|
||||
let Some(envelope) = read_request(&mut stream) else {
|
||||
return;
|
||||
};
|
||||
let id = envelope.id;
|
||||
let msg = envelope.msg;
|
||||
let now = Timestamp::now();
|
||||
|
||||
// 2. By message kind. `take` is the only way in: no one looks at an entry and removes it later.
|
||||
let response = match msg {
|
||||
Message::Approvals(_) => Message::ApprovalList(ApprovalList {
|
||||
items: broker.table().list(),
|
||||
}),
|
||||
Message::Approve(Approve { approval }) => match broker.table().take(approval) {
|
||||
None => Message::Error(WireError {
|
||||
code: ErrorCode::NoSuchApproval,
|
||||
detail: format!("approval {approval} is not pending"),
|
||||
}),
|
||||
Some(entry) => {
|
||||
let outcome = answer(
|
||||
broker,
|
||||
entry,
|
||||
Answer::Approved {
|
||||
by: Some(BY.to_string()),
|
||||
},
|
||||
now,
|
||||
);
|
||||
Message::ApproveResult(ApproveResult { outcome })
|
||||
}
|
||||
},
|
||||
Message::Refuse(Refuse { approval, reason }) => match broker.table().take(approval) {
|
||||
None => Message::Error(WireError {
|
||||
code: ErrorCode::NoSuchApproval,
|
||||
detail: format!("approval {approval} is not pending"),
|
||||
}),
|
||||
Some(entry) => match answer(
|
||||
broker,
|
||||
entry,
|
||||
Answer::Refused {
|
||||
by: Some(BY.to_string()),
|
||||
reason,
|
||||
},
|
||||
now,
|
||||
) {
|
||||
DecisionRecord::Denied {
|
||||
reason: DenyReason::ApprovalRefused,
|
||||
} => Message::Ok(Empty {}),
|
||||
// The refusal could not be recorded: bxctl gets an Internal, the waiting call a denial.
|
||||
_ => Message::Error(WireError {
|
||||
code: ErrorCode::Internal,
|
||||
detail: REFUSAL_INTERNAL.to_string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
// bxctl grants check shows the problems itself and must not use up the print-once of the broker.
|
||||
Message::CheckGrants(_) => {
|
||||
let problems = match grants::load(&broker.cfg().paths.grants) {
|
||||
Ok(_) => Vec::new(),
|
||||
Err(problems) => problems,
|
||||
};
|
||||
Message::GrantsReport(GrantsReport { problems })
|
||||
}
|
||||
other => {
|
||||
forbid(broker, &mut stream, id, &other, "admin.sock");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 3. The final frame; a failed send is ignored.
|
||||
let _ = send(&mut stream, id, true, response);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! The broker: the only role that holds authority.
|
||||
|
||||
pub mod admin;
|
||||
pub mod approvals;
|
||||
pub mod args;
|
||||
pub mod audit;
|
||||
|
||||
Reference in New Issue
Block a user