use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RawDispatchPolicy {
#[default]
Forbid,
AllowWithAudit,
AllowAll,
}
impl RawDispatchPolicy {
pub fn as_str(self) -> &'static str {
match self {
Self::Forbid => "forbid",
Self::AllowWithAudit => "allow_with_audit",
Self::AllowAll => "allow_all",
}
}
pub fn from_env(var: &str) -> Self {
std::env::var(var)
.ok()
.and_then(|v| match v.trim().to_ascii_lowercase().as_str() {
"forbid" | "deny" | "reject" => Some(Self::Forbid),
"allow_with_audit" | "audit" => Some(Self::AllowWithAudit),
"allow_all" | "allow" => Some(Self::AllowAll),
_ => None,
})
.unwrap_or_default()
}
pub fn permits(self) -> bool {
!matches!(self, Self::Forbid)
}
pub fn requires_audit(self) -> bool {
matches!(self, Self::AllowWithAudit)
}
}
#[derive(Debug, Clone, Serialize)]
pub struct RawDispatchAudit {
pub tenant_id: Option<String>,
pub project_id: Option<String>,
pub backend: String,
pub instance: Option<String>,
pub message_type: String,
pub body_preview: String,
pub policy: RawDispatchPolicy,
}
impl RawDispatchAudit {
pub fn record(
tenant_id: Option<&str>,
project_id: Option<&str>,
backend: impl Into<String>,
instance: Option<&str>,
message_type: impl Into<String>,
body: &str,
policy: RawDispatchPolicy,
) -> Self {
const PREVIEW: usize = 200;
let mut preview = body.chars().take(PREVIEW).collect::<String>();
if body.len() > preview.len() {
preview.push_str(" …");
}
Self {
tenant_id: tenant_id.map(str::to_string),
project_id: project_id.map(str::to_string),
backend: backend.into(),
instance: instance.map(str::to_string),
message_type: message_type.into(),
body_preview: preview,
policy,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_is_forbid() {
assert_eq!(RawDispatchPolicy::default(), RawDispatchPolicy::Forbid);
assert!(!RawDispatchPolicy::Forbid.permits());
assert!(!RawDispatchPolicy::Forbid.requires_audit());
}
#[test]
fn allow_with_audit_demands_audit_records() {
assert!(RawDispatchPolicy::AllowWithAudit.permits());
assert!(RawDispatchPolicy::AllowWithAudit.requires_audit());
}
#[test]
fn allow_all_skips_audit() {
assert!(RawDispatchPolicy::AllowAll.permits());
assert!(!RawDispatchPolicy::AllowAll.requires_audit());
}
#[test]
fn from_env_recognises_aliases() {
let var = "UDB_TEST_RAW_DISPATCH_UNSET";
assert_eq!(RawDispatchPolicy::from_env(var), RawDispatchPolicy::Forbid);
}
#[test]
fn audit_record_truncates_body_preview() {
let huge = "x".repeat(500);
let audit = RawDispatchAudit::record(
Some("t"),
None,
"postgres",
Some("primary"),
"Customer",
&huge,
RawDispatchPolicy::AllowWithAudit,
);
assert!(audit.body_preview.len() <= 210); assert!(audit.body_preview.ends_with(" …"));
assert_eq!(audit.policy, RawDispatchPolicy::AllowWithAudit);
}
}