Skip to main content

enact_core/streaming/protection/
context.rs

1//! Protection Context
2//!
3//! Context types that determine how data is protected based on its destination.
4
5use crate::kernel::TenantId;
6
7/// Data destination - where the data is going
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum DataDestination {
10    /// Storage (PostgreSQL, Blob Storage, EventStore)
11    /// Treatment: Encrypt sensitive fields
12    Storage,
13
14    /// Streaming to frontend (SSE, gRPC)
15    /// Treatment: Mask PII, never send encrypted or raw
16    Stream,
17
18    /// Structured logs (telemetry, observability)
19    /// Treatment: Hash or mask, never raw
20    Log,
21
22    /// Audit export (JSON, PDF)
23    /// Treatment: Decrypt for internal, mask for external
24    AuditExport,
25}
26
27impl DataDestination {
28    /// Returns true if this destination is frontend-visible
29    pub fn is_frontend_visible(&self) -> bool {
30        matches!(self, DataDestination::Stream)
31    }
32
33    /// Returns true if this destination requires encryption
34    pub fn requires_encryption(&self) -> bool {
35        matches!(self, DataDestination::Storage)
36    }
37}
38
39/// Protection context - information needed to make protection decisions
40#[derive(Debug, Clone)]
41pub struct ProtectionContext {
42    /// Where the data is going
43    pub destination: DataDestination,
44
45    /// Tenant context (for tenant-specific policies)
46    pub tenant_id: Option<TenantId>,
47
48    /// Whether this is an internal audit export (can decrypt)
49    pub is_internal_audit: bool,
50}
51
52impl ProtectionContext {
53    /// Create a new protection context
54    pub fn new(destination: DataDestination) -> Self {
55        Self {
56            destination,
57            tenant_id: None,
58            is_internal_audit: false,
59        }
60    }
61
62    /// Create context for streaming to frontend
63    pub fn for_stream() -> Self {
64        Self::new(DataDestination::Stream)
65    }
66
67    /// Create context for storage
68    pub fn for_storage() -> Self {
69        Self::new(DataDestination::Storage)
70    }
71
72    /// Create context for logging
73    pub fn for_log() -> Self {
74        Self::new(DataDestination::Log)
75    }
76
77    /// Create context for audit export
78    pub fn for_audit(is_internal: bool) -> Self {
79        Self {
80            destination: DataDestination::AuditExport,
81            tenant_id: None,
82            is_internal_audit: is_internal,
83        }
84    }
85
86    /// Add tenant context
87    pub fn with_tenant(mut self, tenant_id: TenantId) -> Self {
88        self.tenant_id = Some(tenant_id);
89        self
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_data_destination_frontend_visible() {
99        assert!(DataDestination::Stream.is_frontend_visible());
100        assert!(!DataDestination::Storage.is_frontend_visible());
101        assert!(!DataDestination::Log.is_frontend_visible());
102        assert!(!DataDestination::AuditExport.is_frontend_visible());
103    }
104
105    #[test]
106    fn test_data_destination_requires_encryption() {
107        assert!(DataDestination::Storage.requires_encryption());
108        assert!(!DataDestination::Stream.requires_encryption());
109        assert!(!DataDestination::Log.requires_encryption());
110        assert!(!DataDestination::AuditExport.requires_encryption());
111    }
112
113    #[test]
114    fn test_protection_context_factories() {
115        let stream_ctx = ProtectionContext::for_stream();
116        assert_eq!(stream_ctx.destination, DataDestination::Stream);
117
118        let storage_ctx = ProtectionContext::for_storage();
119        assert_eq!(storage_ctx.destination, DataDestination::Storage);
120
121        let log_ctx = ProtectionContext::for_log();
122        assert_eq!(log_ctx.destination, DataDestination::Log);
123
124        let audit_ctx = ProtectionContext::for_audit(true);
125        assert_eq!(audit_ctx.destination, DataDestination::AuditExport);
126        assert!(audit_ctx.is_internal_audit);
127    }
128
129    #[test]
130    fn test_protection_context_with_tenant() {
131        let tenant_id = TenantId::from_string("tenant_123");
132        let ctx = ProtectionContext::for_stream().with_tenant(tenant_id);
133        assert!(ctx.tenant_id.is_some());
134        assert_eq!(ctx.tenant_id.unwrap().as_str(), "tenant_123");
135    }
136}