manta_server/server/common/
audit.rs1use manta_shared::common::error::MantaError;
4use serde::{Deserialize, Serialize};
5
6use crate::server::common::kafka::Kafka;
7
8#[derive(Serialize, Deserialize, Debug, Clone)]
9pub struct Auditor {
11 pub kafka: Kafka,
14}
15
16pub trait Audit {
18 #[allow(async_fn_in_trait)]
23 async fn produce_message(&self, data: &[u8]) -> Result<(), MantaError>;
24}
25
26async fn send_audit_message(kafka: &Kafka, msg_json: serde_json::Value) {
32 let msg_data = match serde_json::to_string(&msg_json) {
33 Ok(data) => data,
34 Err(e) => {
35 tracing::warn!("Failed serializing audit message: {}", e);
36 return;
37 }
38 };
39
40 if let Err(e) = kafka.produce_message(msg_data.as_bytes()).await {
41 tracing::warn!("Failed producing audit message: {}", e);
42 }
43}
44
45pub(crate) fn build_auth_audit_message(
50 outcome: &str,
51 username: &str,
52 source_ip: &str,
53 site: &str,
54) -> serde_json::Value {
55 serde_json::json!({
56 "event": "auth_attempt",
57 "outcome": outcome,
58 "username": username,
59 "source_ip": source_ip,
60 "site": site,
61 })
62}
63
64pub async fn send_auth_audit(
73 kafka_opt: Option<&Kafka>,
74 outcome: &str,
75 username: &str,
76 source_ip: &str,
77 site: &str,
78) {
79 let Some(kafka) = kafka_opt else { return };
80 send_audit_message(
81 kafka,
82 build_auth_audit_message(outcome, username, source_ip, site),
83 )
84 .await;
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
94 fn auth_audit_has_expected_wire_shape() {
95 let msg = build_auth_audit_message("success", "alice", "10.0.0.1", "alps");
96 assert_eq!(msg["event"], "auth_attempt");
97 assert_eq!(msg["outcome"], "success");
98 assert_eq!(msg["username"], "alice");
99 assert_eq!(msg["source_ip"], "10.0.0.1");
100 assert_eq!(msg["site"], "alps");
101 }
102
103 #[test]
104 fn auth_audit_payload_has_no_password_field_by_construction() {
105 let msg = build_auth_audit_message("failure", "alice", "10.0.0.1", "alps");
108 let obj = msg.as_object().expect("payload is an object");
109 for forbidden in ["password", "passwd", "secret", "token"] {
110 assert!(
111 !obj.contains_key(forbidden),
112 "auth audit payload must not contain `{forbidden}`"
113 );
114 }
115 }
116
117 #[test]
118 fn auth_audit_handles_empty_strings_without_panicking() {
119 let msg = build_auth_audit_message("failure", "", "", "");
123 assert_eq!(msg["username"], "");
124 assert_eq!(msg["source_ip"], "");
125 assert_eq!(msg["site"], "");
126 assert_eq!(msg["event"], "auth_attempt");
127 }
128}