allsource_core/application/services/
audit_logger.rs

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/// Request context extracted from HTTP requests
10#[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
48/// Builder for creating audit log entries
49pub 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
165/// AuditLogger service for simplified audit event recording
166///
167/// This service provides a convenient API for recording audit events
168/// from application code and middleware. It handles:
169/// - Automatic context extraction from HTTP requests
170/// - Actor detection from authentication context
171/// - Async, non-blocking logging
172/// - Error handling (audit failures are logged but don't break requests)
173///
174/// # Example
175/// ```rust
176/// let audit_logger = AuditLogger::new(audit_repo);
177///
178/// audit_logger.log(
179///     tenant_id,
180///     AuditAction::EventIngested,
181///     Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
182/// )
183/// .with_resource("event_stream".to_string(), "stream-456".to_string())
184/// .with_context(request_context)
185/// .record_async()
186/// .await;
187/// ```
188pub struct AuditLogger<R: AuditEventRepository> {
189    repository: Arc<R>,
190}
191
192impl<R: AuditEventRepository> AuditLogger<R> {
193    /// Create a new AuditLogger with the given repository
194    pub fn new(repository: Arc<R>) -> Self {
195        Self { repository }
196    }
197
198    /// Start building an audit log entry
199    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    /// Log a successful event (convenience method)
212    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    /// Log a failed event (convenience method)
223    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    /// Log an event with resource information (convenience method)
236    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    /// Record an event without returning an error (logs errors internally)
251    /// This is useful for middleware where audit failures shouldn't break requests
252    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    /// Batch log multiple events
259    pub async fn log_batch(&self, events: Vec<AuditEvent>) -> Result<(), AllSourceError> {
260        self.repository.append_batch(events).await
261    }
262
263    /// Batch log multiple events without returning an error
264    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
271/// Builder for a single audit log entry
272pub 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    /// Record the audit event
319    pub async fn record(self) -> Result<(), AllSourceError> {
320        let event = self.builder.build();
321        self.logger.repository.append(event).await
322    }
323
324    /// Record the audit event silently (logs errors instead of returning them)
325    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        // Logger should be created successfully
354        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        // This should never panic, even if there's an error
486        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        // This should never panic
526        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}