manta_server/server/common/
audit.rs1use manta_shared::common::error::MantaError;
10use serde::{Deserialize, Serialize};
11
12use crate::server::common::kafka::Kafka;
13
14#[derive(Serialize, Deserialize, Debug, Clone)]
15pub struct Auditor {
17 pub kafka: Kafka,
20}
21
22pub trait Audit {
24 #[allow(async_fn_in_trait)]
29 async fn produce_message(&self, data: &[u8]) -> Result<(), MantaError>;
30}
31
32async fn send_audit_message(kafka: &Kafka, msg_json: serde_json::Value) {
38 let msg_data = match serde_json::to_string(&msg_json) {
39 Ok(data) => data,
40 Err(e) => {
41 tracing::warn!("Failed serializing audit message: {}", e);
42 return;
43 }
44 };
45
46 if let Err(e) = kafka.produce_message(msg_data.as_bytes()).await {
47 tracing::warn!("Failed producing audit message: {}", e);
48 }
49}
50
51pub(crate) fn build_auth_audit_message(
56 outcome: &str,
57 username: &str,
58 source_ip: &str,
59 site: &str,
60) -> serde_json::Value {
61 serde_json::json!({
62 "event": "auth_attempt",
63 "outcome": outcome,
64 "username": username,
65 "source_ip": source_ip,
66 "site": site,
67 })
68}
69
70pub async fn send_auth_audit(
79 kafka_opt: Option<&Kafka>,
80 outcome: &str,
81 username: &str,
82 source_ip: &str,
83 site: &str,
84) {
85 let Some(kafka) = kafka_opt else { return };
86 send_audit_message(
87 kafka,
88 build_auth_audit_message(outcome, username, source_ip, site),
89 )
90 .await;
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
100 fn auth_audit_has_expected_wire_shape() {
101 let msg = build_auth_audit_message("success", "alice", "10.0.0.1", "alps");
102 assert_eq!(msg["event"], "auth_attempt");
103 assert_eq!(msg["outcome"], "success");
104 assert_eq!(msg["username"], "alice");
105 assert_eq!(msg["source_ip"], "10.0.0.1");
106 assert_eq!(msg["site"], "alps");
107 }
108
109 #[test]
110 fn auth_audit_payload_has_no_password_field_by_construction() {
111 let msg = build_auth_audit_message("failure", "alice", "10.0.0.1", "alps");
114 let obj = msg.as_object().expect("payload is an object");
115 for forbidden in ["password", "passwd", "secret", "token"] {
116 assert!(
117 !obj.contains_key(forbidden),
118 "auth audit payload must not contain `{forbidden}`"
119 );
120 }
121 }
122
123 #[test]
124 fn auth_audit_handles_empty_strings_without_panicking() {
125 let msg = build_auth_audit_message("failure", "", "", "");
129 assert_eq!(msg["username"], "");
130 assert_eq!(msg["source_ip"], "");
131 assert_eq!(msg["site"], "");
132 assert_eq!(msg["event"], "auth_attempt");
133 }
134}