use std::collections::BTreeSet;
use std::fmt;
use sha2::{Digest, Sha256};
use thiserror::Error;
use crate::{ActorRef, Gate, GateDecision, GateError, GateRef, GateRequest};
const FINGERPRINT_VERSION: &[u8] = b"khive.mailbox-read-gate.v1\0";
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum MailboxPolicyError {
#[error("mailbox readers require an explicit, non-local actor owner")]
InvalidOwner,
#[error("mailbox reader must be a non-anonymous, non-local actor with a nonblank label of at most 255 bytes and no control characters")]
InvalidReader,
#[error("mailbox_readers accepts at most 256 entries before deduplication")]
TooManyReaders,
#[error("mailbox_actor must be a nonblank, non-local string of at most 255 bytes with no control characters")]
InvalidSelector,
}
pub fn is_valid_mailbox_actor_label(label: &str) -> bool {
!label.trim().is_empty()
&& label.len() <= 255
&& !label.chars().any(char::is_control)
&& label != "local"
}
fn valid_actor(actor: &ActorRef) -> bool {
!actor.is_anonymous()
&& !actor.kind.trim().is_empty()
&& actor.kind.len() <= 255
&& !actor.kind.chars().any(char::is_control)
&& is_valid_mailbox_actor_label(&actor.id)
}
pub fn mailbox_read_owner(req: &GateRequest) -> Result<Option<ActorRef>, MailboxPolicyError> {
let selector = match req.verb.as_str() {
"comm.inbox" | "comm.thread" => "mailbox_actor",
"comm.probe" => "actor",
_ => return Ok(None),
};
let Some(value) = req.args.get(selector) else {
return Ok(None);
};
let label = value
.as_str()
.filter(|label| {
is_valid_mailbox_actor_label(label) || (req.verb == "comm.probe" && *label == "local")
})
.ok_or(MailboxPolicyError::InvalidSelector)?;
let owner = if req.verb == "comm.probe" && label == "local" && req.actor.is_anonymous() {
ActorRef::anonymous()
} else {
ActorRef::new("actor", label)
};
Ok((owner != req.actor).then_some(owner))
}
fn apply_mailbox_policy(
gate: &(impl Gate + ?Sized),
req: &GateRequest,
base: GateDecision,
) -> Result<GateDecision, GateError> {
let GateDecision::Allow { mut obligations } = base else {
return Ok(base);
};
let Some(owner) = mailbox_read_owner(req).map_err(|e| GateError::Policy(e.to_string()))? else {
return Ok(GateDecision::allow_with(obligations));
};
if !valid_actor(&req.actor) {
return Ok(GateDecision::deny("mailbox_read_not_granted"));
}
match gate.check_mailbox_read(req, &owner)? {
GateDecision::Allow { obligations: extra } => {
obligations.extend(extra);
Ok(GateDecision::allow_with(obligations))
}
denied => Ok(denied),
}
}
pub fn check_with_mailbox_policy(
gate: &(impl Gate + ?Sized),
req: &GateRequest,
) -> Result<GateDecision, GateError> {
apply_mailbox_policy(gate, req, gate.check(req)?)
}
#[derive(Clone)]
pub struct MailboxReadGate {
inner: GateRef,
owner: ActorRef,
readers: BTreeSet<(String, String)>,
fingerprint: String,
}
impl MailboxReadGate {
pub fn new(
inner: GateRef,
owner: ActorRef,
readers: Vec<ActorRef>,
) -> Result<Self, MailboxPolicyError> {
if !valid_actor(&owner) {
return Err(MailboxPolicyError::InvalidOwner);
}
if readers.len() > 256 {
return Err(MailboxPolicyError::TooManyReaders);
}
if readers.iter().any(|reader| !valid_actor(reader)) {
return Err(MailboxPolicyError::InvalidReader);
}
let readers: BTreeSet<_> = readers.into_iter().map(|a| (a.kind, a.id)).collect();
let mut hash = Sha256::new();
hash.update(FINGERPRINT_VERSION);
match inner.configuration_fingerprint() {
Some(value) => {
hash.update([1]);
hash_field(&mut hash, value);
}
None => hash.update([0]),
}
hash_field(&mut hash, &owner.kind);
hash_field(&mut hash, &owner.id);
hash.update((readers.len() as u64).to_be_bytes());
for (kind, id) in &readers {
hash_field(&mut hash, kind);
hash_field(&mut hash, id);
}
let fingerprint = format!("sha256:{:x}", hash.finalize());
Ok(Self {
inner,
owner,
readers,
fingerprint,
})
}
}
fn hash_field(hash: &mut Sha256, value: &str) {
hash.update((value.len() as u64).to_be_bytes());
hash.update(value.as_bytes());
}
impl fmt::Debug for MailboxReadGate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MailboxReadGate")
.field("inner", &self.inner.impl_name())
.field("reader_count", &self.readers.len())
.finish_non_exhaustive()
}
}
impl Gate for MailboxReadGate {
fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
apply_mailbox_policy(self, req, self.inner.check(req)?)
}
fn check_mailbox_read(
&self,
req: &GateRequest,
owner: &ActorRef,
) -> Result<GateDecision, GateError> {
if matches!(
req.verb.as_str(),
"comm.inbox" | "comm.thread" | "comm.probe"
) && valid_actor(&req.actor)
&& owner == &self.owner
&& self
.readers
.contains(&(req.actor.kind.clone(), req.actor.id.clone()))
{
Ok(GateDecision::allow())
} else {
Ok(GateDecision::deny("mailbox_read_not_granted"))
}
}
fn configuration_fingerprint(&self) -> Option<&str> {
Some(&self.fingerprint)
}
}
#[cfg(test)]
mod tests;