91 lines
3.2 KiB
Rust
91 lines
3.2 KiB
Rust
//! 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<String>,
|
|
}
|
|
|
|
/// `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<String>,
|
|
}
|
|
|
|
/// `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(())
|
|
}
|
|
}
|