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