1use crate::{RelationTuple, Result};
44use serde::{Deserialize, Serialize};
45use std::collections::HashMap;
46use std::sync::Arc;
47use std::time::{SystemTime, UNIX_EPOCH};
48use tokio::sync::RwLock;
49use uuid::Uuid;
50
51#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(tag = "type", rename_all = "snake_case")]
54pub enum AuditEventType {
55 PermissionCheck {
57 subject: String,
58 resource: String,
59 relation: String,
60 allowed: bool,
61 cached: bool,
62 },
63
64 TupleWrite {
66 namespace: String,
67 object_id: String,
68 relation: String,
69 subject: String,
70 },
71
72 TupleDelete {
74 namespace: String,
75 object_id: String,
76 relation: String,
77 subject: String,
78 },
79
80 BatchOperation {
82 operation_type: String,
83 count: usize,
84 success_count: usize,
85 },
86
87 PolicyChange {
89 namespace: String,
90 change_type: String,
91 description: String,
92 },
93
94 CrossTenantAccess {
96 source_tenant: String,
97 target_tenant: String,
98 resource: String,
99 allowed: bool,
100 },
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct AuditEvent {
106 pub id: String,
108
109 pub timestamp: i64,
111
112 pub tenant_id: Option<String>,
114
115 pub event_type: AuditEventType,
117
118 pub actor: Option<String>,
120
121 pub ip_address: Option<String>,
123
124 pub user_agent: Option<String>,
126
127 pub request_id: Option<String>,
129
130 pub metadata: HashMap<String, String>,
132
133 pub integrity_hash: Option<String>,
135}
136
137impl AuditEvent {
138 pub fn new(event_type: AuditEventType) -> Self {
140 Self {
141 id: Uuid::new_v4().to_string(),
142 timestamp: SystemTime::now()
143 .duration_since(UNIX_EPOCH)
144 .unwrap()
145 .as_millis() as i64,
146 tenant_id: None,
147 event_type,
148 actor: None,
149 ip_address: None,
150 user_agent: None,
151 request_id: None,
152 metadata: HashMap::new(),
153 integrity_hash: None,
154 }
155 }
156
157 pub fn permission_check(
159 subject: impl Into<String>,
160 resource: impl Into<String>,
161 relation: impl Into<String>,
162 allowed: bool,
163 tenant_id: Option<String>,
164 ) -> Self {
165 let mut event = Self::new(AuditEventType::PermissionCheck {
166 subject: subject.into(),
167 resource: resource.into(),
168 relation: relation.into(),
169 allowed,
170 cached: false,
171 });
172 event.tenant_id = tenant_id;
173 event
174 }
175
176 pub fn tuple_write(tuple: &RelationTuple, tenant_id: Option<String>) -> Self {
178 let mut event = Self::new(AuditEventType::TupleWrite {
179 namespace: tuple.namespace.clone(),
180 object_id: tuple.object_id.clone(),
181 relation: tuple.relation.clone(),
182 subject: tuple.subject.to_string(),
183 });
184 event.tenant_id = tenant_id;
185 event
186 }
187
188 pub fn tuple_delete(tuple: &RelationTuple, tenant_id: Option<String>) -> Self {
190 let mut event = Self::new(AuditEventType::TupleDelete {
191 namespace: tuple.namespace.clone(),
192 object_id: tuple.object_id.clone(),
193 relation: tuple.relation.clone(),
194 subject: tuple.subject.to_string(),
195 });
196 event.tenant_id = tenant_id;
197 event
198 }
199
200 pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
202 self.actor = Some(actor.into());
203 self
204 }
205
206 pub fn with_ip(mut self, ip: impl Into<String>) -> Self {
208 self.ip_address = Some(ip.into());
209 self
210 }
211
212 pub fn with_request_id(mut self, request_id: impl Into<String>) -> Self {
214 self.request_id = Some(request_id.into());
215 self
216 }
217
218 pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
220 self.metadata.insert(key.into(), value.into());
221 self
222 }
223
224 pub fn compute_integrity_hash(&mut self) {
226 let data = format!(
227 "{}:{}:{}:{:?}",
228 self.id,
229 self.timestamp,
230 self.tenant_id.as_deref().unwrap_or(""),
231 self.event_type
232 );
233 self.integrity_hash = Some(format!("{:x}", md5::compute(data)));
235 }
236
237 pub fn verify_integrity(&self) -> bool {
239 if self.integrity_hash.is_none() {
240 return false;
241 }
242
243 let data = format!(
244 "{}:{}:{}:{:?}",
245 self.id,
246 self.timestamp,
247 self.tenant_id.as_deref().unwrap_or(""),
248 self.event_type
249 );
250 let expected_hash = format!("{:x}", md5::compute(data));
251 self.integrity_hash.as_ref() == Some(&expected_hash)
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize)]
257pub struct AuditConfig {
258 pub enabled: bool,
260
261 pub sampling_rate: f64,
264
265 pub always_log_denials: bool,
267
268 pub always_log_mutations: bool,
270
271 pub always_log_cross_tenant: bool,
273
274 pub buffer_size: usize,
276
277 pub enable_integrity_hash: bool,
279
280 pub storage_backend: AuditStorageBackend,
282}
283
284impl Default for AuditConfig {
285 fn default() -> Self {
286 Self {
287 enabled: true,
288 sampling_rate: 0.1, always_log_denials: true,
290 always_log_mutations: true,
291 always_log_cross_tenant: true,
292 buffer_size: 1000,
293 enable_integrity_hash: true,
294 storage_backend: AuditStorageBackend::InMemory,
295 }
296 }
297}
298
299impl AuditConfig {
300 pub fn with_sampling_rate(mut self, rate: f64) -> Self {
301 self.sampling_rate = rate.clamp(0.0, 1.0);
302 self
303 }
304
305 pub fn with_always_log_denials(mut self, always: bool) -> Self {
306 self.always_log_denials = always;
307 self
308 }
309
310 pub fn with_buffer_size(mut self, size: usize) -> Self {
311 self.buffer_size = size;
312 self
313 }
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
318#[serde(tag = "type", rename_all = "snake_case")]
319pub enum AuditStorageBackend {
320 InMemory,
322
323 PostgreSQL { connection_url: String },
325
326 File { path: String },
328
329 External { endpoint: String },
331}
332
333pub struct AuditLogger {
335 config: AuditConfig,
336 events: Arc<RwLock<Vec<AuditEvent>>>,
337 stats: Arc<RwLock<AuditStats>>,
338}
339
340impl AuditLogger {
341 pub fn new(config: AuditConfig) -> Self {
343 Self {
344 config,
345 events: Arc::new(RwLock::new(Vec::new())),
346 stats: Arc::new(RwLock::new(AuditStats::default())),
347 }
348 }
349
350 pub async fn log(&self, mut event: AuditEvent) -> Result<()> {
352 if !self.config.enabled {
353 return Ok(());
354 }
355
356 if self.config.enable_integrity_hash {
358 event.compute_integrity_hash();
359 }
360
361 let mut events = self.events.write().await;
363 events.push(event.clone());
364
365 let mut stats = self.stats.write().await;
367 stats.total_events += 1;
368 match &event.event_type {
369 AuditEventType::PermissionCheck { allowed, .. } => {
370 stats.permission_checks += 1;
371 if *allowed {
372 stats.allowed_checks += 1;
373 } else {
374 stats.denied_checks += 1;
375 }
376 }
377 AuditEventType::TupleWrite { .. } => stats.tuple_writes += 1,
378 AuditEventType::TupleDelete { .. } => stats.tuple_deletes += 1,
379 AuditEventType::BatchOperation { .. } => stats.batch_operations += 1,
380 AuditEventType::PolicyChange { .. } => stats.policy_changes += 1,
381 AuditEventType::CrossTenantAccess { .. } => stats.cross_tenant_accesses += 1,
382 }
383
384 if events.len() >= self.config.buffer_size {
386 drop(events); self.flush().await?;
388 }
389
390 Ok(())
391 }
392
393 pub fn should_log(&self, event_type: &AuditEventType) -> bool {
395 if !self.config.enabled {
396 return false;
397 }
398
399 match event_type {
400 AuditEventType::PermissionCheck { allowed, .. } => {
401 if !allowed && self.config.always_log_denials {
403 return true;
404 }
405 rand::random::<f64>() < self.config.sampling_rate
407 }
408 AuditEventType::TupleWrite { .. } | AuditEventType::TupleDelete { .. } => {
409 self.config.always_log_mutations
410 }
411 AuditEventType::CrossTenantAccess { .. } => self.config.always_log_cross_tenant,
412 _ => true, }
414 }
415
416 pub async fn flush(&self) -> Result<()> {
418 let mut events = self.events.write().await;
419
420 match &self.config.storage_backend {
421 AuditStorageBackend::InMemory => {
422 }
424 AuditStorageBackend::PostgreSQL { .. } => {
425 }
428 AuditStorageBackend::File { path } => {
429 let _ = path;
431 }
432 AuditStorageBackend::External { endpoint } => {
433 let _ = endpoint;
435 }
436 }
437
438 if events.len() > self.config.buffer_size * 2 {
440 let drain_count = events.len() - self.config.buffer_size;
441 events.drain(0..drain_count);
442 }
443
444 Ok(())
445 }
446
447 pub async fn query_by_resource(
449 &self,
450 resource: &str,
451 start_time: Option<i64>,
452 end_time: Option<i64>,
453 ) -> Result<Vec<AuditEvent>> {
454 let events = self.events.read().await;
455 let filtered = events
456 .iter()
457 .filter(|e| {
458 if let Some(start) = start_time {
460 if e.timestamp < start {
461 return false;
462 }
463 }
464 if let Some(end) = end_time {
465 if e.timestamp > end {
466 return false;
467 }
468 }
469
470 match &e.event_type {
472 AuditEventType::PermissionCheck { resource: res, .. } => res == resource,
473 AuditEventType::TupleWrite {
474 namespace,
475 object_id,
476 ..
477 }
478 | AuditEventType::TupleDelete {
479 namespace,
480 object_id,
481 ..
482 } => format!("{}:{}", namespace, object_id) == resource,
483 AuditEventType::CrossTenantAccess { resource: res, .. } => res == resource,
484 _ => false,
485 }
486 })
487 .cloned()
488 .collect();
489
490 Ok(filtered)
491 }
492
493 pub async fn query_by_subject(
495 &self,
496 subject: &str,
497 start_time: Option<i64>,
498 end_time: Option<i64>,
499 ) -> Result<Vec<AuditEvent>> {
500 let events = self.events.read().await;
501 let filtered = events
502 .iter()
503 .filter(|e| {
504 if let Some(start) = start_time {
506 if e.timestamp < start {
507 return false;
508 }
509 }
510 if let Some(end) = end_time {
511 if e.timestamp > end {
512 return false;
513 }
514 }
515
516 match &e.event_type {
518 AuditEventType::PermissionCheck { subject: subj, .. } => subj == subject,
519 AuditEventType::TupleWrite { subject: subj, .. }
520 | AuditEventType::TupleDelete { subject: subj, .. } => subj == subject,
521 _ => false,
522 }
523 })
524 .cloned()
525 .collect();
526
527 Ok(filtered)
528 }
529
530 pub async fn query_by_tenant(
532 &self,
533 tenant_id: &str,
534 start_time: Option<i64>,
535 end_time: Option<i64>,
536 ) -> Result<Vec<AuditEvent>> {
537 let events = self.events.read().await;
538 let filtered = events
539 .iter()
540 .filter(|e| {
541 if let Some(start) = start_time {
543 if e.timestamp < start {
544 return false;
545 }
546 }
547 if let Some(end) = end_time {
548 if e.timestamp > end {
549 return false;
550 }
551 }
552
553 e.tenant_id.as_deref() == Some(tenant_id)
555 })
556 .cloned()
557 .collect();
558
559 Ok(filtered)
560 }
561
562 pub async fn stats(&self) -> AuditStats {
564 self.stats.read().await.clone()
565 }
566
567 pub async fn compliance_report(&self, tenant_id: Option<&str>) -> Result<ComplianceReport> {
569 let events = if let Some(tid) = tenant_id {
570 self.query_by_tenant(tid, None, None).await?
571 } else {
572 self.events.read().await.clone()
573 };
574
575 let stats = self.stats().await;
576
577 let unique_users: std::collections::HashSet<_> = events
579 .iter()
580 .filter_map(|e| e.actor.as_ref())
581 .cloned()
582 .collect();
583
584 let unique_resources: std::collections::HashSet<_> = events
585 .iter()
586 .filter_map(|e| match &e.event_type {
587 AuditEventType::PermissionCheck { resource, .. } => Some(resource.clone()),
588 AuditEventType::CrossTenantAccess { resource, .. } => Some(resource.clone()),
589 _ => None,
590 })
591 .collect();
592
593 let denied_accesses: Vec<_> = events
594 .iter()
595 .filter(|e| {
596 matches!(
597 e.event_type,
598 AuditEventType::PermissionCheck { allowed: false, .. }
599 )
600 })
601 .cloned()
602 .collect();
603
604 let cross_tenant_accesses: Vec<_> = events
605 .iter()
606 .filter(|e| matches!(e.event_type, AuditEventType::CrossTenantAccess { .. }))
607 .cloned()
608 .collect();
609
610 Ok(ComplianceReport {
611 generated_at: chrono::Utc::now().timestamp(),
612 tenant_id: tenant_id.map(|s| s.to_string()),
613 total_events: events.len(),
614 unique_users: unique_users.len(),
615 unique_resources: unique_resources.len(),
616 denied_accesses: denied_accesses.len(),
617 cross_tenant_accesses: cross_tenant_accesses.len(),
618 stats,
619 sample_denied_accesses: denied_accesses.into_iter().take(10).collect(),
620 sample_cross_tenant_accesses: cross_tenant_accesses.into_iter().take(10).collect(),
621 })
622 }
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize, Default)]
627pub struct AuditStats {
628 pub total_events: usize,
629 pub permission_checks: usize,
630 pub allowed_checks: usize,
631 pub denied_checks: usize,
632 pub tuple_writes: usize,
633 pub tuple_deletes: usize,
634 pub batch_operations: usize,
635 pub policy_changes: usize,
636 pub cross_tenant_accesses: usize,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
641pub struct ComplianceReport {
642 pub generated_at: i64,
643 pub tenant_id: Option<String>,
644 pub total_events: usize,
645 pub unique_users: usize,
646 pub unique_resources: usize,
647 pub denied_accesses: usize,
648 pub cross_tenant_accesses: usize,
649 pub stats: AuditStats,
650 pub sample_denied_accesses: Vec<AuditEvent>,
651 pub sample_cross_tenant_accesses: Vec<AuditEvent>,
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[tokio::test]
659 async fn test_audit_logger_basic() {
660 let config = AuditConfig::default().with_sampling_rate(1.0); let logger = AuditLogger::new(config);
662
663 let event =
664 AuditEvent::permission_check("user:alice", "document:123", "viewer", true, None);
665 logger.log(event).await.unwrap();
666
667 let stats = logger.stats().await;
668 assert_eq!(stats.total_events, 1);
669 assert_eq!(stats.permission_checks, 1);
670 assert_eq!(stats.allowed_checks, 1);
671 }
672
673 #[tokio::test]
674 async fn test_audit_query_by_resource() {
675 let config = AuditConfig::default().with_sampling_rate(1.0);
676 let logger = AuditLogger::new(config);
677
678 logger
680 .log(AuditEvent::permission_check(
681 "user:alice",
682 "document:123",
683 "viewer",
684 true,
685 None,
686 ))
687 .await
688 .unwrap();
689
690 logger
691 .log(AuditEvent::permission_check(
692 "user:bob",
693 "document:123",
694 "editor",
695 false,
696 None,
697 ))
698 .await
699 .unwrap();
700
701 logger
702 .log(AuditEvent::permission_check(
703 "user:charlie",
704 "document:456",
705 "viewer",
706 true,
707 None,
708 ))
709 .await
710 .unwrap();
711
712 let events = logger
714 .query_by_resource("document:123", None, None)
715 .await
716 .unwrap();
717
718 assert_eq!(events.len(), 2);
719 }
720
721 #[tokio::test]
722 async fn test_integrity_hash() {
723 let mut event =
724 AuditEvent::permission_check("user:alice", "document:123", "viewer", true, None);
725 event.compute_integrity_hash();
726
727 assert!(event.integrity_hash.is_some());
728 assert!(event.verify_integrity());
729
730 event.timestamp += 1000;
732 assert!(!event.verify_integrity());
733 }
734
735 #[tokio::test]
736 async fn test_sampling() {
737 let config = AuditConfig::default()
738 .with_sampling_rate(0.0) .with_always_log_denials(true); let logger = AuditLogger::new(config);
742
743 assert!(!logger.should_log(&AuditEventType::PermissionCheck {
745 subject: "user:alice".to_string(),
746 resource: "document:123".to_string(),
747 relation: "viewer".to_string(),
748 allowed: true,
749 cached: false,
750 }));
751
752 assert!(logger.should_log(&AuditEventType::PermissionCheck {
754 subject: "user:alice".to_string(),
755 resource: "document:123".to_string(),
756 relation: "viewer".to_string(),
757 allowed: false,
758 cached: false,
759 }));
760 }
761
762 #[tokio::test]
763 async fn test_compliance_report() {
764 let config = AuditConfig::default().with_sampling_rate(1.0);
765 let logger = AuditLogger::new(config);
766
767 for i in 0..10 {
769 logger
770 .log(
771 AuditEvent::permission_check(
772 format!("user:{}", i % 3),
773 format!("document:{}", i),
774 "viewer",
775 i % 2 == 0, Some("tenant-123".to_string()),
777 )
778 .with_actor(format!("actor:{}", i % 2)),
779 )
780 .await
781 .unwrap();
782 }
783
784 let report = logger.compliance_report(Some("tenant-123")).await.unwrap();
785
786 assert_eq!(report.total_events, 10);
787 assert_eq!(report.unique_users, 2); assert_eq!(report.denied_accesses, 5); }
790}