Skip to main content

allsource_core/application/services/
audit_logger.rs

1use crate::{
2    domain::{
3        entities::{Actor, AuditAction, AuditEvent, AuditOutcome},
4        repositories::AuditEventRepository,
5        value_objects::TenantId,
6    },
7    error::AllSourceError,
8};
9use serde_json::Value as JsonValue;
10use std::sync::Arc;
11use tracing::error;
12
13/// Request context extracted from HTTP requests
14#[derive(Debug, Clone)]
15pub struct RequestContext {
16    pub ip_address: Option<String>,
17    pub user_agent: Option<String>,
18    pub request_id: Option<String>,
19}
20
21impl RequestContext {
22    pub fn new() -> Self {
23        Self {
24            ip_address: None,
25            user_agent: None,
26            request_id: None,
27        }
28    }
29
30    pub fn with_ip(mut self, ip: String) -> Self {
31        self.ip_address = Some(ip);
32        self
33    }
34
35    pub fn with_user_agent(mut self, user_agent: String) -> Self {
36        self.user_agent = Some(user_agent);
37        self
38    }
39
40    pub fn with_request_id(mut self, request_id: String) -> Self {
41        self.request_id = Some(request_id);
42        self
43    }
44}
45
46impl Default for RequestContext {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52/// Builder for creating audit log entries
53pub struct AuditLogBuilder {
54    tenant_id: TenantId,
55    action: AuditAction,
56    actor: Actor,
57    outcome: AuditOutcome,
58    resource_type: Option<String>,
59    resource_id: Option<String>,
60    ip_address: Option<String>,
61    user_agent: Option<String>,
62    request_id: Option<String>,
63    error_message: Option<String>,
64    metadata: Option<JsonValue>,
65}
66
67impl AuditLogBuilder {
68    fn new(tenant_id: TenantId, action: AuditAction, actor: Actor) -> Self {
69        Self {
70            tenant_id,
71            action,
72            actor,
73            outcome: AuditOutcome::Success,
74            resource_type: None,
75            resource_id: None,
76            ip_address: None,
77            user_agent: None,
78            request_id: None,
79            error_message: None,
80            metadata: None,
81        }
82    }
83
84    pub fn with_outcome(mut self, outcome: AuditOutcome) -> Self {
85        self.outcome = outcome;
86        self
87    }
88
89    pub fn with_resource(mut self, resource_type: String, resource_id: String) -> Self {
90        self.resource_type = Some(resource_type);
91        self.resource_id = Some(resource_id);
92        self
93    }
94
95    pub fn with_context(mut self, context: RequestContext) -> Self {
96        self.ip_address = context.ip_address;
97        self.user_agent = context.user_agent;
98        self.request_id = context.request_id;
99        self
100    }
101
102    pub fn with_ip_address(mut self, ip: String) -> Self {
103        self.ip_address = Some(ip);
104        self
105    }
106
107    pub fn with_user_agent(mut self, user_agent: String) -> Self {
108        self.user_agent = Some(user_agent);
109        self
110    }
111
112    pub fn with_request_id(mut self, request_id: String) -> Self {
113        self.request_id = Some(request_id);
114        self
115    }
116
117    pub fn with_error(mut self, error_message: String) -> Self {
118        self.error_message = Some(error_message);
119        self.outcome = AuditOutcome::Failure;
120        self
121    }
122
123    pub fn with_metadata(mut self, metadata: JsonValue) -> Self {
124        self.metadata = Some(metadata);
125        self
126    }
127
128    fn build(self) -> AuditEvent {
129        let mut event = AuditEvent::new(self.tenant_id, self.action, self.actor, self.outcome);
130
131        if let (Some(resource_type), Some(resource_id)) = (self.resource_type, self.resource_id) {
132            event = event.with_resource(resource_type, resource_id);
133        }
134
135        if let Some(ip) = self.ip_address {
136            event = event.with_ip_address(ip);
137        }
138
139        if let Some(ua) = self.user_agent {
140            event = event.with_user_agent(ua);
141        }
142
143        if let Some(req_id) = self.request_id {
144            event = event.with_request_id(req_id);
145        }
146
147        if let Some(err) = self.error_message {
148            event = event.with_error(err);
149        }
150
151        if let Some(meta) = self.metadata {
152            event = event.with_metadata(meta);
153        }
154
155        event
156    }
157
158    pub async fn record<R: AuditEventRepository>(self, repo: &R) -> Result<(), AllSourceError> {
159        let event = self.build();
160        repo.append(event).await
161    }
162}
163
164/// AuditLogger service for simplified audit event recording
165///
166/// This service provides a convenient API for recording audit events
167/// from application code and middleware. It handles:
168/// - Automatic context extraction from HTTP requests
169/// - Actor detection from authentication context
170/// - Async, non-blocking logging
171/// - Error handling (audit failures are logged but don't break requests)
172///
173/// # Example
174/// ```ignore
175/// let audit_logger = AuditLogger::new(audit_repo);
176///
177/// audit_logger.log(
178///     tenant_id,
179///     AuditAction::EventIngested,
180///     Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
181/// )
182/// .with_resource("event_stream".to_string(), "stream-456".to_string())
183/// .with_context(request_context)
184/// .record_async()
185/// .await;
186/// ```
187pub struct AuditLogger<R: AuditEventRepository> {
188    repository: Arc<R>,
189}
190
191impl<R: AuditEventRepository> AuditLogger<R> {
192    /// Create a new AuditLogger with the given repository
193    pub fn new(repository: Arc<R>) -> Self {
194        Self { repository }
195    }
196
197    /// Start building an audit log entry
198    pub fn log(
199        &self,
200        tenant_id: TenantId,
201        action: AuditAction,
202        actor: Actor,
203    ) -> AuditLogEntry<'_, R> {
204        AuditLogEntry {
205            logger: self,
206            builder: AuditLogBuilder::new(tenant_id, action, actor),
207        }
208    }
209
210    /// Log a successful event (convenience method)
211    pub async fn log_success(
212        &self,
213        tenant_id: TenantId,
214        action: AuditAction,
215        actor: Actor,
216    ) -> Result<(), AllSourceError> {
217        let event = AuditEvent::new(tenant_id, action, actor, AuditOutcome::Success);
218        self.repository.append(event).await
219    }
220
221    /// Log a failed event (convenience method)
222    pub async fn log_failure(
223        &self,
224        tenant_id: TenantId,
225        action: AuditAction,
226        actor: Actor,
227        error_message: String,
228    ) -> Result<(), AllSourceError> {
229        let event = AuditEvent::new(tenant_id, action, actor, AuditOutcome::Failure)
230            .with_error(error_message);
231        self.repository.append(event).await
232    }
233
234    /// Log an event with resource information (convenience method)
235    pub async fn log_resource_action(
236        &self,
237        tenant_id: TenantId,
238        action: AuditAction,
239        actor: Actor,
240        resource_type: String,
241        resource_id: String,
242        outcome: AuditOutcome,
243    ) -> Result<(), AllSourceError> {
244        let event = AuditEvent::new(tenant_id, action, actor, outcome)
245            .with_resource(resource_type, resource_id);
246        self.repository.append(event).await
247    }
248
249    /// Record an event without returning an error (logs errors internally)
250    /// This is useful for middleware where audit failures shouldn't break requests
251    pub async fn record_silently(&self, event: AuditEvent) {
252        if let Err(e) = self.repository.append(event).await {
253            error!("Failed to record audit event: {}", e);
254        }
255    }
256
257    /// Batch log multiple events
258    pub async fn log_batch(&self, events: Vec<AuditEvent>) -> Result<(), AllSourceError> {
259        self.repository.append_batch(events).await
260    }
261
262    /// Batch log multiple events without returning an error
263    pub async fn log_batch_silently(&self, events: Vec<AuditEvent>) {
264        if let Err(e) = self.repository.append_batch(events).await {
265            error!("Failed to record audit event batch: {}", e);
266        }
267    }
268}
269
270/// Builder for a single audit log entry
271pub struct AuditLogEntry<'a, R: AuditEventRepository> {
272    logger: &'a AuditLogger<R>,
273    builder: AuditLogBuilder,
274}
275
276impl<'a, R: AuditEventRepository> AuditLogEntry<'a, R> {
277    pub fn with_outcome(mut self, outcome: AuditOutcome) -> Self {
278        self.builder = self.builder.with_outcome(outcome);
279        self
280    }
281
282    pub fn with_resource(mut self, resource_type: String, resource_id: String) -> Self {
283        self.builder = self.builder.with_resource(resource_type, resource_id);
284        self
285    }
286
287    pub fn with_context(mut self, context: RequestContext) -> Self {
288        self.builder = self.builder.with_context(context);
289        self
290    }
291
292    pub fn with_ip_address(mut self, ip: String) -> Self {
293        self.builder = self.builder.with_ip_address(ip);
294        self
295    }
296
297    pub fn with_user_agent(mut self, user_agent: String) -> Self {
298        self.builder = self.builder.with_user_agent(user_agent);
299        self
300    }
301
302    pub fn with_request_id(mut self, request_id: String) -> Self {
303        self.builder = self.builder.with_request_id(request_id);
304        self
305    }
306
307    pub fn with_error(mut self, error_message: String) -> Self {
308        self.builder = self.builder.with_error(error_message);
309        self
310    }
311
312    pub fn with_metadata(mut self, metadata: JsonValue) -> Self {
313        self.builder = self.builder.with_metadata(metadata);
314        self
315    }
316
317    /// Record the audit event
318    pub async fn record(self) -> Result<(), AllSourceError> {
319        let event = self.builder.build();
320        self.logger.repository.append(event).await
321    }
322
323    /// Record the audit event silently (logs errors instead of returning them)
324    pub async fn record_silently(self) {
325        let event = self.builder.build();
326        self.logger.record_silently(event).await;
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333    use crate::{
334        domain::entities::AuditAction, infrastructure::repositories::InMemoryAuditRepository,
335    };
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 - verify we can log
354        let result = logger
355            .log_success(test_tenant_id(), AuditAction::Login, test_actor())
356            .await;
357        assert!(result.is_ok());
358    }
359
360    #[tokio::test]
361    async fn test_log_success() {
362        let logger = setup_logger();
363        let result = logger
364            .log_success(test_tenant_id(), AuditAction::Login, test_actor())
365            .await;
366
367        assert!(result.is_ok());
368    }
369
370    #[tokio::test]
371    async fn test_log_failure() {
372        let logger = setup_logger();
373        let result = logger
374            .log_failure(
375                test_tenant_id(),
376                AuditAction::LoginFailed,
377                test_actor(),
378                "Invalid credentials".to_string(),
379            )
380            .await;
381
382        assert!(result.is_ok());
383    }
384
385    #[tokio::test]
386    async fn test_log_with_resource() {
387        let logger = setup_logger();
388        let result = logger
389            .log_resource_action(
390                test_tenant_id(),
391                AuditAction::EventIngested,
392                Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
393                "event_stream".to_string(),
394                "stream-456".to_string(),
395                AuditOutcome::Success,
396            )
397            .await;
398
399        assert!(result.is_ok());
400    }
401
402    #[tokio::test]
403    async fn test_builder_api() {
404        let logger = setup_logger();
405
406        let result = logger
407            .log(
408                test_tenant_id(),
409                AuditAction::EventIngested,
410                Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
411            )
412            .with_resource("event_stream".to_string(), "stream-456".to_string())
413            .with_ip_address("192.168.1.1".to_string())
414            .with_request_id("req-789".to_string())
415            .record()
416            .await;
417
418        assert!(result.is_ok());
419    }
420
421    #[tokio::test]
422    async fn test_builder_with_context() {
423        let logger = setup_logger();
424
425        let context = RequestContext::new()
426            .with_ip("10.0.0.1".to_string())
427            .with_user_agent("Mozilla/5.0".to_string())
428            .with_request_id("req-abc".to_string());
429
430        let result = logger
431            .log(test_tenant_id(), AuditAction::Login, test_actor())
432            .with_context(context)
433            .record()
434            .await;
435
436        assert!(result.is_ok());
437    }
438
439    #[tokio::test]
440    async fn test_builder_with_error() {
441        let logger = setup_logger();
442
443        let result = logger
444            .log(
445                test_tenant_id(),
446                AuditAction::PermissionDenied,
447                test_actor(),
448            )
449            .with_error("Insufficient permissions".to_string())
450            .record()
451            .await;
452
453        assert!(result.is_ok());
454    }
455
456    #[tokio::test]
457    async fn test_builder_with_metadata() {
458        let logger = setup_logger();
459
460        let metadata = serde_json::json!({
461            "reason": "rate_limit",
462            "limit": 100,
463            "current": 150
464        });
465
466        let result = logger
467            .log(
468                test_tenant_id(),
469                AuditAction::RateLimitExceeded,
470                test_actor(),
471            )
472            .with_metadata(metadata)
473            .record()
474            .await;
475
476        assert!(result.is_ok());
477    }
478
479    #[tokio::test]
480    async fn test_record_silently() {
481        let logger = setup_logger();
482
483        let event = AuditEvent::new(
484            test_tenant_id(),
485            AuditAction::Login,
486            test_actor(),
487            AuditOutcome::Success,
488        );
489
490        // This should never panic, even if there's an error
491        logger.record_silently(event).await;
492    }
493
494    #[tokio::test]
495    async fn test_batch_logging() {
496        let logger = setup_logger();
497
498        let events = vec![
499            AuditEvent::new(
500                test_tenant_id(),
501                AuditAction::Login,
502                test_actor(),
503                AuditOutcome::Success,
504            ),
505            AuditEvent::new(
506                test_tenant_id(),
507                AuditAction::EventIngested,
508                Actor::api_key("key-123".to_string(), "prod-api-key".to_string()),
509                AuditOutcome::Success,
510            ),
511        ];
512
513        let result = logger.log_batch(events).await;
514        assert!(result.is_ok());
515    }
516
517    #[tokio::test]
518    async fn test_batch_logging_silently() {
519        let logger = setup_logger();
520
521        let events = vec![AuditEvent::new(
522            test_tenant_id(),
523            AuditAction::Login,
524            test_actor(),
525            AuditOutcome::Success,
526        )];
527
528        // This should never panic
529        logger.log_batch_silently(events).await;
530    }
531
532    #[tokio::test]
533    async fn test_request_context_builder() {
534        let context = RequestContext::new()
535            .with_ip("192.168.1.1".to_string())
536            .with_user_agent("curl/7.64.1".to_string())
537            .with_request_id("req-123".to_string());
538
539        assert_eq!(context.ip_address, Some("192.168.1.1".to_string()));
540        assert_eq!(context.user_agent, Some("curl/7.64.1".to_string()));
541        assert_eq!(context.request_id, Some("req-123".to_string()));
542    }
543}