manta_server/server/audit/
mod.rs1mod auth;
22pub mod event;
23pub mod kafka;
24mod middleware;
25
26use std::{sync::Arc, time::Duration};
27
28use serde::{Deserialize, Serialize};
29
30pub use auth::send_auth_audit;
31pub use middleware::{AuditNote, audit_requests};
32
33use event::{AuditEvent, Emitter, EventKind};
34
35pub const AUDIT_TARGET: &str = "manta::audit";
37
38#[derive(Serialize, Deserialize, Debug, Clone)]
40pub struct Auditor {
41 pub kafka: kafka::Kafka,
43}
44
45pub trait AuditSink: Send + Sync {
51 fn emit(&self, event: &AuditEvent);
53 fn flush(&self, _timeout: Duration) {}
56}
57
58struct TracingSink;
62
63impl AuditSink for TracingSink {
64 fn emit(&self, event: &AuditEvent) {
65 match serde_json::to_string(event) {
66 Ok(json) => {
67 tracing::info!(target: AUDIT_TARGET, event = %json, "{}", event.summary);
68 }
69 Err(e) => tracing::warn!(
70 target: AUDIT_TARGET,
71 event_id = %event.event_id,
72 "audit event not serialisable: {e}"
73 ),
74 }
75 }
76}
77
78#[derive(Clone)]
81pub struct AuditLog {
82 sinks: Vec<Arc<dyn AuditSink>>,
83 emitter: Emitter,
84}
85
86impl Default for AuditLog {
87 fn default() -> Self {
89 Self::with_sinks(vec![Arc::new(TracingSink)])
90 }
91}
92
93impl AuditLog {
94 pub fn new(kafka: Option<kafka::KafkaSink>) -> Self {
96 let mut sinks: Vec<Arc<dyn AuditSink>> = vec![Arc::new(TracingSink)];
97 if let Some(k) = kafka {
98 sinks.push(Arc::new(k));
99 }
100 Self::with_sinks(sinks)
101 }
102
103 pub fn with_sinks(sinks: Vec<Arc<dyn AuditSink>>) -> Self {
105 Self {
106 sinks,
107 emitter: Emitter {
108 service: "manta-server",
109 version: env!("CARGO_PKG_VERSION"),
110 host: host_name(),
111 },
112 }
113 }
114
115 pub fn event(&self, kind: EventKind, action: String) -> AuditEvent {
118 AuditEvent {
119 schema_version: event::SCHEMA_VERSION,
120 event_id: uuid::Uuid::new_v4().to_string(),
121 timestamp: chrono::Utc::now()
122 .to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
123 kind,
124 action,
125 summary: String::new(),
126 actor: Default::default(),
127 client: Default::default(),
128 site: None,
129 request: Default::default(),
130 outcome: event::Outcome {
131 result: event::Verdict::Success,
132 status: 0,
133 duration_ms: 0,
134 },
135 dry_run: None,
136 targets: Default::default(),
137 nodes: Default::default(),
138 produced: Default::default(),
139 details: Default::default(),
140 emitter: self.emitter.clone(),
141 }
142 }
143
144 pub fn emit(&self, mut event: AuditEvent) {
147 event.summary = event.describe();
148 for sink in &self.sinks {
149 sink.emit(&event);
150 }
151 }
152
153 pub fn flush(&self, timeout: Duration) {
155 for sink in &self.sinks {
156 sink.flush(timeout);
157 }
158 }
159}
160
161fn host_name() -> Option<String> {
163 std::env::var("HOSTNAME")
164 .ok()
165 .or_else(|| std::fs::read_to_string("/etc/hostname").ok())
166 .map(|h| h.trim().to_string())
167 .filter(|h| !h.is_empty())
168}
169
170#[cfg(test)]
172#[derive(Default)]
173pub(crate) struct CaptureSink(pub std::sync::Mutex<Vec<AuditEvent>>);
174
175#[cfg(test)]
176impl AuditSink for CaptureSink {
177 fn emit(&self, event: &AuditEvent) {
178 self.0.lock().unwrap().push(event.clone());
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn event_prefills_identity_fields() {
188 let log = AuditLog::default();
189 let a = log.event(EventKind::ApiRequest, "x.y".into());
190 let b = log.event(EventKind::ApiRequest, "x.y".into());
191 assert_eq!(a.schema_version, event::SCHEMA_VERSION);
192 assert_ne!(a.event_id, b.event_id, "event ids must be unique");
193 assert!(a.timestamp.ends_with('Z'), "UTC timestamp: {}", a.timestamp);
194 assert_eq!(a.emitter.service, "manta-server");
195 assert_eq!(a.emitter.version, env!("CARGO_PKG_VERSION"));
196 }
197
198 #[test]
199 fn emit_fans_out_to_every_sink() {
200 let first = Arc::new(CaptureSink::default());
201 let second = Arc::new(CaptureSink::default());
202 let log = AuditLog::with_sinks(vec![first.clone(), second.clone()]);
203 log.emit(log.event(EventKind::ApiRequest, "x.y".into()));
204 assert_eq!(first.0.lock().unwrap().len(), 1);
205 assert_eq!(
206 first.0.lock().unwrap()[0].summary,
207 "anonymous: x y → success"
208 );
209 assert_eq!(second.0.lock().unwrap().len(), 1);
210 }
211
212 #[test]
213 fn serialised_event_omits_empty_fields() {
214 let mut e = AuditLog::default().event(EventKind::ApiRequest, "x.y".into());
215 e.emitter.host = None;
216 let json = serde_json::to_string(&e).unwrap();
217 assert!(!json.contains("null"), "{json}");
218 for absent in ["\"dry_run\"", "\"site\"", "\"targets\"", "\"details\""] {
219 assert!(!json.contains(absent), "{absent} in {json}");
220 }
221 }
222
223 #[test]
229 fn schema_has_no_credential_field() {
230 let mut e = AuditLog::default().event(EventKind::ApiRequest, "x.y".into());
231 e.actor = event::Actor {
232 username: Some("u".into()),
233 name: Some("n".into()),
234 subject: Some("s".into()),
235 };
236 e.client = event::Client {
237 ip: Some("i".into()),
238 forwarded_for: Some("f".into()),
239 };
240 e.site = Some("s".into());
241 e.request.query = Some("q".into());
242 e.dry_run = Some(true);
243 e.emitter.host = Some("h".into());
244
245 fn keys(v: &serde_json::Value, out: &mut Vec<String>) {
246 if let Some(map) = v.as_object() {
247 for (k, v) in map {
248 out.push(k.clone());
249 keys(v, out);
250 }
251 }
252 }
253 let mut all = Vec::new();
254 keys(&serde_json::to_value(&e).unwrap(), &mut all);
255 for key in ["forwarded_for", "query", "dry_run", "host", "subject"] {
256 assert!(
257 all.iter().any(|k| k == key),
258 "`{key}` not populated: {all:?}"
259 );
260 }
261 for key in &all {
262 for forbidden in ["token", "password", "authorization", "secret"] {
263 assert!(!key.contains(forbidden), "schema field `{key}`");
264 }
265 }
266 }
267}