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 assert!(true);
350 }
351
352 #[tokio::test]
353 async fn test_log_success() {
354 let logger = setup_logger();
355 let result = logger
356 .log_success(test_tenant_id(), AuditAction::Login, test_actor())
357 .await;
358
359 assert!(result.is_ok());
360 }
361
362 #[tokio::test]
363 async fn test_log_failure() {
364 let logger = setup_logger();
365 let result = logger
366 .log_failure(
367 test_tenant_id(),
368 AuditAction::LoginFailed,
369 test_actor(),
370 "Invalid credentials".to_string(),
371 )
372 .await;
373
374 assert!(result.is_ok());
375 }
376
377 #[tokio::test]
378 async fn test_log_with_resource() {
379 let logger = setup_logger();
380 let result = logger
381 .log_resource_action(
382 test_tenant_id(),
383 AuditAction::EventIngested,
384 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
385 "event_stream".to_string(),
386 "stream-456".to_string(),
387 AuditOutcome::Success,
388 )
389 .await;
390
391 assert!(result.is_ok());
392 }
393
394 #[tokio::test]
395 async fn test_builder_api() {
396 let logger = setup_logger();
397
398 let result = logger
399 .log(
400 test_tenant_id(),
401 AuditAction::EventIngested,
402 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
403 )
404 .with_resource("event_stream".to_string(), "stream-456".to_string())
405 .with_ip_address("192.168.1.1".to_string())
406 .with_request_id("req-789".to_string())
407 .record()
408 .await;
409
410 assert!(result.is_ok());
411 }
412
413 #[tokio::test]
414 async fn test_builder_with_context() {
415 let logger = setup_logger();
416
417 let context = RequestContext::new()
418 .with_ip("10.0.0.1".to_string())
419 .with_user_agent("Mozilla/5.0".to_string())
420 .with_request_id("req-abc".to_string());
421
422 let result = logger
423 .log(test_tenant_id(), AuditAction::Login, test_actor())
424 .with_context(context)
425 .record()
426 .await;
427
428 assert!(result.is_ok());
429 }
430
431 #[tokio::test]
432 async fn test_builder_with_error() {
433 let logger = setup_logger();
434
435 let result = logger
436 .log(
437 test_tenant_id(),
438 AuditAction::PermissionDenied,
439 test_actor(),
440 )
441 .with_error("Insufficient permissions".to_string())
442 .record()
443 .await;
444
445 assert!(result.is_ok());
446 }
447
448 #[tokio::test]
449 async fn test_builder_with_metadata() {
450 let logger = setup_logger();
451
452 let metadata = serde_json::json!({
453 "reason": "rate_limit",
454 "limit": 100,
455 "current": 150
456 });
457
458 let result = logger
459 .log(
460 test_tenant_id(),
461 AuditAction::RateLimitExceeded,
462 test_actor(),
463 )
464 .with_metadata(metadata)
465 .record()
466 .await;
467
468 assert!(result.is_ok());
469 }
470
471 #[tokio::test]
472 async fn test_record_silently() {
473 let logger = setup_logger();
474
475 let event = AuditEvent::new(
476 test_tenant_id(),
477 AuditAction::Login,
478 test_actor(),
479 AuditOutcome::Success,
480 );
481
482 logger.record_silently(event).await;
484 }
485
486 #[tokio::test]
487 async fn test_batch_logging() {
488 let logger = setup_logger();
489
490 let events = vec![
491 AuditEvent::new(
492 test_tenant_id(),
493 AuditAction::Login,
494 test_actor(),
495 AuditOutcome::Success,
496 ),
497 AuditEvent::new(
498 test_tenant_id(),
499 AuditAction::EventIngested,
500 Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
501 AuditOutcome::Success,
502 ),
503 ];
504
505 let result = logger.log_batch(events).await;
506 assert!(result.is_ok());
507 }
508
509 #[tokio::test]
510 async fn test_batch_logging_silently() {
511 let logger = setup_logger();
512
513 let events = vec![AuditEvent::new(
514 test_tenant_id(),
515 AuditAction::Login,
516 test_actor(),
517 AuditOutcome::Success,
518 )];
519
520 logger.log_batch_silently(events).await;
522 }
523
524 #[tokio::test]
525 async fn test_request_context_builder() {
526 let context = RequestContext::new()
527 .with_ip("192.168.1.1".to_string())
528 .with_user_agent("curl/7.64.1".to_string())
529 .with_request_id("req-123".to_string());
530
531 assert_eq!(context.ip_address, Some("192.168.1.1".to_string()));
532 assert_eq!(context.user_agent, Some("curl/7.64.1".to_string()));
533 assert_eq!(context.request_id, Some("req-123".to_string()));
534 }
535}