Skip to main content

allsource_core/application/services/
audit_logger.rs

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/// 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(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
160/// AuditLogger service for simplified audit event recording
161///
162/// This service provides a convenient API for recording audit events
163/// from application code and middleware. It handles:
164/// - Automatic context extraction from HTTP requests
165/// - Actor detection from authentication context
166/// - Async, non-blocking logging
167/// - Error handling (audit failures are logged but don't break requests)
168///
169/// # Example
170/// ```ignore
171/// let audit_logger = AuditLogger::new(audit_repo);
172///
173/// audit_logger.log(
174///     tenant_id,
175///     AuditAction::EventIngested,
176///     Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
177/// )
178/// .with_resource("event_stream".to_string(), "stream-456".to_string())
179/// .with_context(request_context)
180/// .record_async()
181/// .await;
182/// ```
183pub struct AuditLogger<R: AuditEventRepository> {
184    repository: Arc<R>,
185}
186
187impl<R: AuditEventRepository> AuditLogger<R> {
188    /// Create a new AuditLogger with the given repository
189    pub fn new(repository: Arc<R>) -> Self {
190        Self { repository }
191    }
192
193    /// Start building an audit log entry
194    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    /// Log a successful event (convenience method)
207    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    /// Log a failed event (convenience method)
218    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    /// Log an event with resource information (convenience method)
231    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    /// Record an event without returning an error (logs errors internally)
246    /// This is useful for middleware where audit failures shouldn't break requests
247    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    /// Batch log multiple events
254    pub async fn log_batch(&self, events: Vec<AuditEvent>) -> Result<(), AllSourceError> {
255        self.repository.append_batch(events).await
256    }
257
258    /// Batch log multiple events without returning an error
259    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
266/// Builder for a single audit log entry
267pub 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    /// Record the audit event
314    pub async fn record(self) -> Result<(), AllSourceError> {
315        let event = self.builder.build();
316        self.logger.repository.append(event).await
317    }
318
319    /// Record the audit event silently (logs errors instead of returning them)
320    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        // Logger should be created successfully - verify we can log
349        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        // 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![AuditEvent::new(
517            test_tenant_id(),
518            AuditAction::Login,
519            test_actor(),
520            AuditOutcome::Success,
521        )];
522
523        // This should never panic
524        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}