//! The messages on `gateway.sock`, between `brokerd` and `gatewayd`: one connection carries one //! pending approval to Mattermost and its answer back (M4b spec, section 4). use serde::{Deserialize, Serialize}; use crate::wire::DenyReason; use crate::wire::PendingApproval; /// The most characters a refusal's reason may have. pub const MAX_REASON: usize = 500; /// `brokerd` → `gatewayd`, first: the approval to post. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ApprovalRequest { pub approval: PendingApproval, } /// `gatewayd` → `brokerd`: it is posted, in these posts, first part first. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ApprovalPosted { pub posts: Vec, } /// `gatewayd` → `brokerd`: an approver answered, with this post. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ApprovalReply { pub approval: u64, pub approved: bool, pub by: String, pub post: String, pub reason: Option, } /// `brokerd` → `gatewayd`, last: what came of the answer. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ApprovalOutcome { pub outcome: GatewayOutcome, } // JSON: {"outcome":"denied","reason":"no_grant"} ; the tag sits beside the fields #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "outcome", rename_all = "snake_case", deny_unknown_fields)] pub enum GatewayOutcome { /// Approved, and the grants as they are now still allow it. Runs {}, /// Approved, but denied on the re-check, or the answer could not be recorded. Denied { reason: DenyReason }, /// The refusal is recorded. Refused {}, /// It was no longer pending when the answer came. Gone {}, } /// A Mattermost id: 26 characters of `a-z0-9`. pub fn is_mattermost_id(id: &str) -> bool { // `id.len() == 26` and every byte `b.is_ascii_lowercase() || b.is_ascii_digit()`. id.len() == 26 && id .bytes() .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit()) } impl ApprovalReply { /// `by` and `post` are Mattermost ids and the reason is at most `MAX_REASON` characters; the /// first problem otherwise. pub fn check(&self) -> Result<(), String> { // In this order, each an Err with exactly this text: // 1. `!is_mattermost_id(&self.by)`: "the answer's `by` is not a Mattermost id". // 2. `!is_mattermost_id(&self.post)`: "the answer's `post` is not a Mattermost id". // 3. a reason with more than `MAX_REASON` characters (`r.chars().count()`, not bytes): // `format!("the reason is over {MAX_REASON} characters")`. // 4. Ok(()). if !is_mattermost_id(&self.by) { return Err("the answer's `by` is not a Mattermost id".to_string()); } if !is_mattermost_id(&self.post) { return Err("the answer's `post` is not a Mattermost id".to_string()); } if let Some(reason) = &self.reason && reason.chars().count() > MAX_REASON { return Err(format!("the reason is over {MAX_REASON} characters")); } Ok(()) } }