1use std::collections::BTreeSet;
2use std::fmt;
3
4use sha2::{Digest, Sha256};
5use thiserror::Error;
6
7use crate::{ActorRef, Gate, GateDecision, GateError, GateRef, GateRequest};
8
9const FINGERPRINT_VERSION: &[u8] = b"khive.mailbox-read-gate.v1\0";
10
11#[derive(Clone, Debug, Error, PartialEq, Eq)]
13pub enum MailboxPolicyError {
14 #[error("mailbox readers require an explicit, non-local actor owner")]
15 InvalidOwner,
16 #[error("mailbox reader must be a non-anonymous, non-local actor with a nonblank label of at most 255 bytes and no control characters")]
17 InvalidReader,
18 #[error("mailbox_readers accepts at most 256 entries before deduplication")]
19 TooManyReaders,
20 #[error("mailbox_actor must be a nonblank, non-local string of at most 255 bytes with no control characters")]
21 InvalidSelector,
22}
23
24pub fn is_valid_mailbox_actor_label(label: &str) -> bool {
29 !label.trim().is_empty()
30 && label.len() <= 255
31 && !label.chars().any(char::is_control)
32 && label != "local"
33}
34
35fn valid_actor(actor: &ActorRef) -> bool {
36 !actor.is_anonymous()
37 && !actor.kind.trim().is_empty()
38 && actor.kind.len() <= 255
39 && !actor.kind.chars().any(char::is_control)
40 && is_valid_mailbox_actor_label(&actor.id)
41}
42
43pub fn mailbox_read_owner(req: &GateRequest) -> Result<Option<ActorRef>, MailboxPolicyError> {
48 let selector = match req.verb.as_str() {
49 "comm.inbox" | "comm.thread" => "mailbox_actor",
50 "comm.probe" => "actor",
51 _ => return Ok(None),
52 };
53 let Some(value) = req.args.get(selector) else {
54 return Ok(None);
55 };
56 let label = value
57 .as_str()
58 .filter(|label| {
59 is_valid_mailbox_actor_label(label) || (req.verb == "comm.probe" && *label == "local")
60 })
61 .ok_or(MailboxPolicyError::InvalidSelector)?;
62 let owner = if req.verb == "comm.probe" && label == "local" && req.actor.is_anonymous() {
64 ActorRef::anonymous()
65 } else {
66 ActorRef::new("actor", label)
67 };
68 Ok((owner != req.actor).then_some(owner))
69}
70
71fn apply_mailbox_policy(
72 gate: &(impl Gate + ?Sized),
73 req: &GateRequest,
74 base: GateDecision,
75) -> Result<GateDecision, GateError> {
76 let GateDecision::Allow { mut obligations } = base else {
77 return Ok(base);
78 };
79 let Some(owner) = mailbox_read_owner(req).map_err(|e| GateError::Policy(e.to_string()))? else {
80 return Ok(GateDecision::allow_with(obligations));
81 };
82 if !valid_actor(&req.actor) {
83 return Ok(GateDecision::deny("mailbox_read_not_granted"));
84 }
85 match gate.check_mailbox_read(req, &owner)? {
86 GateDecision::Allow { obligations: extra } => {
87 obligations.extend(extra);
88 Ok(GateDecision::allow_with(obligations))
89 }
90 denied => Ok(denied),
91 }
92}
93
94pub fn check_with_mailbox_policy(
99 gate: &(impl Gate + ?Sized),
100 req: &GateRequest,
101) -> Result<GateDecision, GateError> {
102 apply_mailbox_policy(gate, req, gate.check(req)?)
103}
104
105#[derive(Clone)]
111pub struct MailboxReadGate {
112 inner: GateRef,
113 owner: ActorRef,
114 readers: BTreeSet<(String, String)>,
115 fingerprint: String,
116}
117
118impl MailboxReadGate {
119 pub fn new(
122 inner: GateRef,
123 owner: ActorRef,
124 readers: Vec<ActorRef>,
125 ) -> Result<Self, MailboxPolicyError> {
126 if !valid_actor(&owner) {
127 return Err(MailboxPolicyError::InvalidOwner);
128 }
129 if readers.len() > 256 {
130 return Err(MailboxPolicyError::TooManyReaders);
131 }
132 if readers.iter().any(|reader| !valid_actor(reader)) {
133 return Err(MailboxPolicyError::InvalidReader);
134 }
135 let readers: BTreeSet<_> = readers.into_iter().map(|a| (a.kind, a.id)).collect();
136 let mut hash = Sha256::new();
137 hash.update(FINGERPRINT_VERSION);
138 match inner.configuration_fingerprint() {
139 Some(value) => {
140 hash.update([1]);
141 hash_field(&mut hash, value);
142 }
143 None => hash.update([0]),
144 }
145 hash_field(&mut hash, &owner.kind);
148 hash_field(&mut hash, &owner.id);
149 hash.update((readers.len() as u64).to_be_bytes());
150 for (kind, id) in &readers {
151 hash_field(&mut hash, kind);
152 hash_field(&mut hash, id);
153 }
154 let fingerprint = format!("sha256:{:x}", hash.finalize());
155 Ok(Self {
156 inner,
157 owner,
158 readers,
159 fingerprint,
160 })
161 }
162}
163
164fn hash_field(hash: &mut Sha256, value: &str) {
165 hash.update((value.len() as u64).to_be_bytes());
166 hash.update(value.as_bytes());
167}
168
169impl fmt::Debug for MailboxReadGate {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.debug_struct("MailboxReadGate")
172 .field("inner", &self.inner.impl_name())
173 .field("reader_count", &self.readers.len())
174 .finish_non_exhaustive()
175 }
176}
177
178impl Gate for MailboxReadGate {
179 fn check(&self, req: &GateRequest) -> Result<GateDecision, GateError> {
180 apply_mailbox_policy(self, req, self.inner.check(req)?)
181 }
182
183 fn check_mailbox_read(
184 &self,
185 req: &GateRequest,
186 owner: &ActorRef,
187 ) -> Result<GateDecision, GateError> {
188 if matches!(
189 req.verb.as_str(),
190 "comm.inbox" | "comm.thread" | "comm.probe"
191 ) && valid_actor(&req.actor)
192 && owner == &self.owner
193 && self
194 .readers
195 .contains(&(req.actor.kind.clone(), req.actor.id.clone()))
196 {
197 Ok(GateDecision::allow())
198 } else {
199 Ok(GateDecision::deny("mailbox_read_not_granted"))
200 }
201 }
202
203 fn configuration_fingerprint(&self) -> Option<&str> {
204 Some(&self.fingerprint)
205 }
206}
207
208#[cfg(test)]
209mod tests;