1use crate::domain::entities::{AuditEvent, AuditAction, AuditOutcome, Actor};
2use crate::domain::value_objects::TenantId;
3use crate::domain::repositories::AuditEventRepository;
4use crate::error::AllSourceError;
5use std::sync::Arc;
6use serde_json::Value as JsonValue;
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(
126 self.tenant_id,
127 self.action,
128 self.actor,
129 self.outcome,
130 );
131
132 if let (Some(resource_type), Some(resource_id)) = (self.resource_type, self.resource_id) {
133 event = event.with_resource(resource_type, resource_id);
134 }
135
136 if let Some(ip) = self.ip_address {
137 event = event.with_ip_address(ip);
138 }
139
140 if let Some(ua) = self.user_agent {
141 event = event.with_user_agent(ua);
142 }
143
144 if let Some(req_id) = self.request_id {
145 event = event.with_request_id(req_id);
146 }
147
148 if let Some(err) = self.error_message {
149 event = event.with_error(err);
150 }
151
152 if let Some(meta) = self.metadata {
153 event = event.with_metadata(meta);
154 }
155
156 event
157 }
158
159 pub async fn record<R: AuditEventRepository>(self, repo: &R) -> Result<(), AllSourceError> {
160 let event = self.build();
161 repo.append(event).await
162 }
163}
164
165pub struct AuditLogger<R: AuditEventRepository> {
189 repository: Arc<R>,
190}
191
192impl<R: AuditEventRepository> AuditLogger<R> {
193 pub fn new(repository: Arc<R>) -> Self {
195 Self { repository }
196 }
197
198 pub fn log(
200 &self,
201 tenant_id: TenantId,
202 action: AuditAction,
203 actor: Actor,
204 ) -> AuditLogEntry<'_, R> {
205 AuditLogEntry {
206 logger: self,
207 builder: AuditLogBuilder::new(tenant_id, action, actor),
208 }
209 }
210
211 pub async fn log_success(
213 &self,
214 tenant_id: TenantId,
215 action: AuditAction,
216 actor: Actor,
217 ) -> Result<(), AllSourceError> {
218 let event = AuditEvent::new(tenant_id, action, actor, AuditOutcome::Success);
219 self.repository.append(event).await
220 }
221
222 pub async fn log_failure(
224 &self,
225 tenant_id: TenantId,
226 action: AuditAction,
227 actor: Actor,
228 error_message: String,
229 ) -> Result<(), AllSourceError> {
230 let event = AuditEvent::new(tenant_id, action, actor, AuditOutcome::Failure)
231 .with_error(error_message);
232 self.repository.append(event).await
233 }
234
235 pub async fn log_resource_action(
237 &self,
238 tenant_id: TenantId,
239 action: AuditAction,
240 actor: Actor,
241 resource_type: String,
242 resource_id: String,
243 outcome: AuditOutcome,
244 ) -> Result<(), AllSourceError> {
245 let event = AuditEvent::new(tenant_id, action, actor, outcome)
246 .with_resource(resource_type, resource_id);
247 self.repository.append(event).await
248 }
249
250 pub async fn record_silently(&self, event: AuditEvent) {
253 if let Err(e) = self.repository.append(event).await {
254 error!("Failed to record audit event: {}", e);
255 }
256 }
257
258 pub async fn log_batch(&self, events: Vec<AuditEvent>) -> Result<(), AllSourceError> {
260 self.repository.append_batch(events).await
261 }
262
263 pub async fn log_batch_silently(&self, events: Vec<AuditEvent>) {
265 if let Err(e) = self.repository.append_batch(events).await {
266 error!("Failed to record audit event batch: {}", e);
267 }
268 }
269}
270
271pub struct AuditLogEntry<'a, R: AuditEventRepository> {
273 logger: &'a AuditLogger<R>,
274 builder: AuditLogBuilder,
275}
276
277impl<'a, R: AuditEventRepository> AuditLogEntry<'a, R> {
278 pub fn with_outcome(mut self, outcome: AuditOutcome) -> Self {
279 self.builder = self.builder.with_outcome(outcome);
280 self
281 }
282
283 pub fn with_resource(mut self, resource_type: String, resource_id: String) -> Self {
284 self.builder = self.builder.with_resource(resource_type, resource_id);
285 self
286 }
287
288 pub fn with_context(mut self, context: RequestContext) -> Self {
289 self.builder = self.builder.with_context(context);
290 self
291 }
292
293 pub fn with_ip_address(mut self, ip: String) -> Self {
294 self.builder = self.builder.with_ip_address(ip);
295 self
296 }
297
298 pub fn with_user_agent(mut self, user_agent: String) -> Self {
299 self.builder = self.builder.with_user_agent(user_agent);
300 self
301 }
302
303 pub fn with_request_id(mut self, request_id: String) -> Self {
304 self.builder = self.builder.with_request_id(request_id);
305 self
306 }
307
308 pub fn with_error(mut self, error_message: String) -> Self {
309 self.builder = self.builder.with_error(error_message);
310 self
311 }
312
313 pub fn with_metadata(mut self, metadata: JsonValue) -> Self {
314 self.builder = self.builder.with_metadata(metadata);
315 self
316 }
317
318 pub async fn record(self) -> Result<(), AllSourceError> {
320 let event = self.builder.build();
321 self.logger.repository.append(event).await
322 }
323
324 pub async fn record_silently(self) {
326 let event = self.builder.build();
327 self.logger.record_silently(event).await;
328 }
329}
330
331#[cfg(test)]
332mod tests {
333 use super::*;
334 use crate::infrastructure::repositories::InMemoryAuditRepository;
335 use crate::domain::entities::AuditAction;
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 assert!(true);
355 }
356
357 #[tokio::test]
358 async fn test_log_success() {
359 let logger = setup_logger();
360 let result = logger.log_success(
361 test_tenant_id(),
362 AuditAction::Login,
363 test_actor(),
364 ).await;
365
366 assert!(result.is_ok());
367 }
368
369 #[tokio::test]
370 async fn test_log_failure() {
371 let logger = setup_logger();
372 let result = logger.log_failure(
373 test_tenant_id(),
374 AuditAction::LoginFailed,
375 test_actor(),
376 "Invalid credentials".to_string(),
377 ).await;
378
379 assert!(result.is_ok());
380 }
381
382 #[tokio::test]
383 async fn test_log_with_resource() {
384 let logger = setup_logger();
385 let result = logger.log_resource_action(
386 test_tenant_id(),
387 AuditAction::EventIngested,
388 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
389 "event_stream".to_string(),
390 "stream-456".to_string(),
391 AuditOutcome::Success,
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.log(
402 test_tenant_id(),
403 AuditAction::EventIngested,
404 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
405 )
406 .with_resource("event_stream".to_string(), "stream-456".to_string())
407 .with_ip_address("192.168.1.1".to_string())
408 .with_request_id("req-789".to_string())
409 .record()
410 .await;
411
412 assert!(result.is_ok());
413 }
414
415 #[tokio::test]
416 async fn test_builder_with_context() {
417 let logger = setup_logger();
418
419 let context = RequestContext::new()
420 .with_ip("10.0.0.1".to_string())
421 .with_user_agent("Mozilla/5.0".to_string())
422 .with_request_id("req-abc".to_string());
423
424 let result = logger.log(
425 test_tenant_id(),
426 AuditAction::Login,
427 test_actor(),
428 )
429 .with_context(context)
430 .record()
431 .await;
432
433 assert!(result.is_ok());
434 }
435
436 #[tokio::test]
437 async fn test_builder_with_error() {
438 let logger = setup_logger();
439
440 let result = logger.log(
441 test_tenant_id(),
442 AuditAction::PermissionDenied,
443 test_actor(),
444 )
445 .with_error("Insufficient permissions".to_string())
446 .record()
447 .await;
448
449 assert!(result.is_ok());
450 }
451
452 #[tokio::test]
453 async fn test_builder_with_metadata() {
454 let logger = setup_logger();
455
456 let metadata = serde_json::json!({
457 "reason": "rate_limit",
458 "limit": 100,
459 "current": 150
460 });
461
462 let result = logger.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![
517 AuditEvent::new(
518 test_tenant_id(),
519 AuditAction::Login,
520 test_actor(),
521 AuditOutcome::Success,
522 ),
523 ];
524
525 logger.log_batch_silently(events).await;
527 }
528
529 #[tokio::test]
530 async fn test_request_context_builder() {
531 let context = RequestContext::new()
532 .with_ip("192.168.1.1".to_string())
533 .with_user_agent("curl/7.64.1".to_string())
534 .with_request_id("req-123".to_string());
535
536 assert_eq!(context.ip_address, Some("192.168.1.1".to_string()));
537 assert_eq!(context.user_agent, Some("curl/7.64.1".to_string()));
538 assert_eq!(context.request_id, Some("req-123".to_string()));
539 }
540}