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/// ```rust
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
349        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        // This should never panic, even if there's an error
483        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        // This should never panic
521        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}