1use crate::domain::entities::{Actor, AuditAction, AuditEvent, AuditOutcome};
2use crate::domain::repositories::AuditEventRepository;
3use crate::domain::value_objects::TenantId;
4use crate::error::AllSourceError;
5use serde_json::Value as JsonValue;
6use std::sync::Arc;
7use tracing::error;
8
9#[derive(Debug, Clone)]
11pub struct RequestContext {
12 pub ip_address: Option<String>,
13 pub user_agent: Option<String>,
14 pub request_id: Option<String>,
15}
16
17impl RequestContext {
18 pub fn new() -> Self {
19 Self {
20 ip_address: None,
21 user_agent: None,
22 request_id: None,
23 }
24 }
25
26 pub fn with_ip(mut self, ip: String) -> Self {
27 self.ip_address = Some(ip);
28 self
29 }
30
31 pub fn with_user_agent(mut self, user_agent: String) -> Self {
32 self.user_agent = Some(user_agent);
33 self
34 }
35
36 pub fn with_request_id(mut self, request_id: String) -> Self {
37 self.request_id = Some(request_id);
38 self
39 }
40}
41
42impl Default for RequestContext {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48pub struct AuditLogBuilder {
50 tenant_id: TenantId,
51 action: AuditAction,
52 actor: Actor,
53 outcome: AuditOutcome,
54 resource_type: Option<String>,
55 resource_id: Option<String>,
56 ip_address: Option<String>,
57 user_agent: Option<String>,
58 request_id: Option<String>,
59 error_message: Option<String>,
60 metadata: Option<JsonValue>,
61}
62
63impl AuditLogBuilder {
64 fn new(tenant_id: TenantId, action: AuditAction, actor: Actor) -> Self {
65 Self {
66 tenant_id,
67 action,
68 actor,
69 outcome: AuditOutcome::Success,
70 resource_type: None,
71 resource_id: None,
72 ip_address: None,
73 user_agent: None,
74 request_id: None,
75 error_message: None,
76 metadata: None,
77 }
78 }
79
80 pub fn with_outcome(mut self, outcome: AuditOutcome) -> Self {
81 self.outcome = outcome;
82 self
83 }
84
85 pub fn with_resource(mut self, resource_type: String, resource_id: String) -> Self {
86 self.resource_type = Some(resource_type);
87 self.resource_id = Some(resource_id);
88 self
89 }
90
91 pub fn with_context(mut self, context: RequestContext) -> Self {
92 self.ip_address = context.ip_address;
93 self.user_agent = context.user_agent;
94 self.request_id = context.request_id;
95 self
96 }
97
98 pub fn with_ip_address(mut self, ip: String) -> Self {
99 self.ip_address = Some(ip);
100 self
101 }
102
103 pub fn with_user_agent(mut self, user_agent: String) -> Self {
104 self.user_agent = Some(user_agent);
105 self
106 }
107
108 pub fn with_request_id(mut self, request_id: String) -> Self {
109 self.request_id = Some(request_id);
110 self
111 }
112
113 pub fn with_error(mut self, error_message: String) -> Self {
114 self.error_message = Some(error_message);
115 self.outcome = AuditOutcome::Failure;
116 self
117 }
118
119 pub fn with_metadata(mut self, metadata: JsonValue) -> Self {
120 self.metadata = Some(metadata);
121 self
122 }
123
124 fn build(self) -> AuditEvent {
125 let mut event = AuditEvent::new(self.tenant_id, self.action, self.actor, self.outcome);
126
127 if let (Some(resource_type), Some(resource_id)) = (self.resource_type, self.resource_id) {
128 event = event.with_resource(resource_type, resource_id);
129 }
130
131 if let Some(ip) = self.ip_address {
132 event = event.with_ip_address(ip);
133 }
134
135 if let Some(ua) = self.user_agent {
136 event = event.with_user_agent(ua);
137 }
138
139 if let Some(req_id) = self.request_id {
140 event = event.with_request_id(req_id);
141 }
142
143 if let Some(err) = self.error_message {
144 event = event.with_error(err);
145 }
146
147 if let Some(meta) = self.metadata {
148 event = event.with_metadata(meta);
149 }
150
151 event
152 }
153
154 pub async fn record<R: AuditEventRepository>(self, repo: &R) -> Result<(), AllSourceError> {
155 let event = self.build();
156 repo.append(event).await
157 }
158}
159
160pub struct AuditLogger<R: AuditEventRepository> {
184 repository: Arc<R>,
185}
186
187impl<R: AuditEventRepository> AuditLogger<R> {
188 pub fn new(repository: Arc<R>) -> Self {
190 Self { repository }
191 }
192
193 pub fn log(
195 &self,
196 tenant_id: TenantId,
197 action: AuditAction,
198 actor: Actor,
199 ) -> AuditLogEntry<'_, R> {
200 AuditLogEntry {
201 logger: self,
202 builder: AuditLogBuilder::new(tenant_id, action, actor),
203 }
204 }
205
206 pub async fn log_success(
208 &self,
209 tenant_id: TenantId,
210 action: AuditAction,
211 actor: Actor,
212 ) -> Result<(), AllSourceError> {
213 let event = AuditEvent::new(tenant_id, action, actor, AuditOutcome::Success);
214 self.repository.append(event).await
215 }
216
217 pub async fn log_failure(
219 &self,
220 tenant_id: TenantId,
221 action: AuditAction,
222 actor: Actor,
223 error_message: String,
224 ) -> Result<(), AllSourceError> {
225 let event = AuditEvent::new(tenant_id, action, actor, AuditOutcome::Failure)
226 .with_error(error_message);
227 self.repository.append(event).await
228 }
229
230 pub async fn log_resource_action(
232 &self,
233 tenant_id: TenantId,
234 action: AuditAction,
235 actor: Actor,
236 resource_type: String,
237 resource_id: String,
238 outcome: AuditOutcome,
239 ) -> Result<(), AllSourceError> {
240 let event = AuditEvent::new(tenant_id, action, actor, outcome)
241 .with_resource(resource_type, resource_id);
242 self.repository.append(event).await
243 }
244
245 pub async fn record_silently(&self, event: AuditEvent) {
248 if let Err(e) = self.repository.append(event).await {
249 error!("Failed to record audit event: {}", e);
250 }
251 }
252
253 pub async fn log_batch(&self, events: Vec<AuditEvent>) -> Result<(), AllSourceError> {
255 self.repository.append_batch(events).await
256 }
257
258 pub async fn log_batch_silently(&self, events: Vec<AuditEvent>) {
260 if let Err(e) = self.repository.append_batch(events).await {
261 error!("Failed to record audit event batch: {}", e);
262 }
263 }
264}
265
266pub struct AuditLogEntry<'a, R: AuditEventRepository> {
268 logger: &'a AuditLogger<R>,
269 builder: AuditLogBuilder,
270}
271
272impl<'a, R: AuditEventRepository> AuditLogEntry<'a, R> {
273 pub fn with_outcome(mut self, outcome: AuditOutcome) -> Self {
274 self.builder = self.builder.with_outcome(outcome);
275 self
276 }
277
278 pub fn with_resource(mut self, resource_type: String, resource_id: String) -> Self {
279 self.builder = self.builder.with_resource(resource_type, resource_id);
280 self
281 }
282
283 pub fn with_context(mut self, context: RequestContext) -> Self {
284 self.builder = self.builder.with_context(context);
285 self
286 }
287
288 pub fn with_ip_address(mut self, ip: String) -> Self {
289 self.builder = self.builder.with_ip_address(ip);
290 self
291 }
292
293 pub fn with_user_agent(mut self, user_agent: String) -> Self {
294 self.builder = self.builder.with_user_agent(user_agent);
295 self
296 }
297
298 pub fn with_request_id(mut self, request_id: String) -> Self {
299 self.builder = self.builder.with_request_id(request_id);
300 self
301 }
302
303 pub fn with_error(mut self, error_message: String) -> Self {
304 self.builder = self.builder.with_error(error_message);
305 self
306 }
307
308 pub fn with_metadata(mut self, metadata: JsonValue) -> Self {
309 self.builder = self.builder.with_metadata(metadata);
310 self
311 }
312
313 pub async fn record(self) -> Result<(), AllSourceError> {
315 let event = self.builder.build();
316 self.logger.repository.append(event).await
317 }
318
319 pub async fn record_silently(self) {
321 let event = self.builder.build();
322 self.logger.record_silently(event).await;
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::domain::entities::AuditAction;
330 use crate::infrastructure::repositories::InMemoryAuditRepository;
331
332 fn setup_logger() -> AuditLogger<InMemoryAuditRepository> {
333 let repo = Arc::new(InMemoryAuditRepository::new());
334 AuditLogger::new(repo)
335 }
336
337 fn test_tenant_id() -> TenantId {
338 TenantId::new("test-tenant".to_string()).unwrap()
339 }
340
341 fn test_actor() -> Actor {
342 Actor::user("user-123".to_string(), "john-doe".to_string())
343 }
344
345 #[tokio::test]
346 async fn test_audit_logger_creation() {
347 let logger = setup_logger();
348 let result = logger
350 .log_success(test_tenant_id(), AuditAction::Login, test_actor())
351 .await;
352 assert!(result.is_ok());
353 }
354
355 #[tokio::test]
356 async fn test_log_success() {
357 let logger = setup_logger();
358 let result = logger
359 .log_success(test_tenant_id(), AuditAction::Login, test_actor())
360 .await;
361
362 assert!(result.is_ok());
363 }
364
365 #[tokio::test]
366 async fn test_log_failure() {
367 let logger = setup_logger();
368 let result = logger
369 .log_failure(
370 test_tenant_id(),
371 AuditAction::LoginFailed,
372 test_actor(),
373 "Invalid credentials".to_string(),
374 )
375 .await;
376
377 assert!(result.is_ok());
378 }
379
380 #[tokio::test]
381 async fn test_log_with_resource() {
382 let logger = setup_logger();
383 let result = logger
384 .log_resource_action(
385 test_tenant_id(),
386 AuditAction::EventIngested,
387 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
388 "event_stream".to_string(),
389 "stream-456".to_string(),
390 AuditOutcome::Success,
391 )
392 .await;
393
394 assert!(result.is_ok());
395 }
396
397 #[tokio::test]
398 async fn test_builder_api() {
399 let logger = setup_logger();
400
401 let result = logger
402 .log(
403 test_tenant_id(),
404 AuditAction::EventIngested,
405 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
406 )
407 .with_resource("event_stream".to_string(), "stream-456".to_string())
408 .with_ip_address("192.168.1.1".to_string())
409 .with_request_id("req-789".to_string())
410 .record()
411 .await;
412
413 assert!(result.is_ok());
414 }
415
416 #[tokio::test]
417 async fn test_builder_with_context() {
418 let logger = setup_logger();
419
420 let context = RequestContext::new()
421 .with_ip("10.0.0.1".to_string())
422 .with_user_agent("Mozilla/5.0".to_string())
423 .with_request_id("req-abc".to_string());
424
425 let result = logger
426 .log(test_tenant_id(), AuditAction::Login, test_actor())
427 .with_context(context)
428 .record()
429 .await;
430
431 assert!(result.is_ok());
432 }
433
434 #[tokio::test]
435 async fn test_builder_with_error() {
436 let logger = setup_logger();
437
438 let result = logger
439 .log(
440 test_tenant_id(),
441 AuditAction::PermissionDenied,
442 test_actor(),
443 )
444 .with_error("Insufficient permissions".to_string())
445 .record()
446 .await;
447
448 assert!(result.is_ok());
449 }
450
451 #[tokio::test]
452 async fn test_builder_with_metadata() {
453 let logger = setup_logger();
454
455 let metadata = serde_json::json!({
456 "reason": "rate_limit",
457 "limit": 100,
458 "current": 150
459 });
460
461 let result = logger
462 .log(
463 test_tenant_id(),
464 AuditAction::RateLimitExceeded,
465 test_actor(),
466 )
467 .with_metadata(metadata)
468 .record()
469 .await;
470
471 assert!(result.is_ok());
472 }
473
474 #[tokio::test]
475 async fn test_record_silently() {
476 let logger = setup_logger();
477
478 let event = AuditEvent::new(
479 test_tenant_id(),
480 AuditAction::Login,
481 test_actor(),
482 AuditOutcome::Success,
483 );
484
485 logger.record_silently(event).await;
487 }
488
489 #[tokio::test]
490 async fn test_batch_logging() {
491 let logger = setup_logger();
492
493 let events = vec![
494 AuditEvent::new(
495 test_tenant_id(),
496 AuditAction::Login,
497 test_actor(),
498 AuditOutcome::Success,
499 ),
500 AuditEvent::new(
501 test_tenant_id(),
502 AuditAction::EventIngested,
503 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
504 AuditOutcome::Success,
505 ),
506 ];
507
508 let result = logger.log_batch(events).await;
509 assert!(result.is_ok());
510 }
511
512 #[tokio::test]
513 async fn test_batch_logging_silently() {
514 let logger = setup_logger();
515
516 let events = vec![AuditEvent::new(
517 test_tenant_id(),
518 AuditAction::Login,
519 test_actor(),
520 AuditOutcome::Success,
521 )];
522
523 logger.log_batch_silently(events).await;
525 }
526
527 #[tokio::test]
528 async fn test_request_context_builder() {
529 let context = RequestContext::new()
530 .with_ip("192.168.1.1".to_string())
531 .with_user_agent("curl/7.64.1".to_string())
532 .with_request_id("req-123".to_string());
533
534 assert_eq!(context.ip_address, Some("192.168.1.1".to_string()));
535 assert_eq!(context.user_agent, Some("curl/7.64.1".to_string()));
536 assert_eq!(context.request_id, Some("req-123".to_string()));
537 }
538}