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