Skip to main content

heliosdb_proxy/plugins/
sandbox.rs

1//! Security Sandbox
2//!
3//! Sandboxing and permission system for WASM plugins.
4
5use std::collections::HashSet;
6use std::path::PathBuf;
7use std::time::Duration;
8
9/// Plugin permissions
10#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum Permission {
12    /// Execute queries
13    QueryExecute,
14
15    /// Read from cache
16    CacheRead,
17
18    /// Write to cache
19    CacheWrite,
20
21    /// Make HTTP requests
22    HttpFetch,
23
24    /// Access cryptographic functions
25    Crypto,
26
27    /// Read from KV store
28    KvRead,
29
30    /// Write to KV store
31    KvWrite,
32
33    /// Record metrics
34    Metrics,
35
36    /// Read configuration
37    ConfigRead,
38
39    /// Access network
40    Network,
41
42    /// Read filesystem
43    FilesystemRead,
44
45    /// Write filesystem
46    FilesystemWrite,
47
48    /// Custom permission
49    Custom(String),
50}
51
52impl Permission {
53    /// Parse permission from string
54    #[allow(clippy::should_implement_trait)]
55    pub fn from_str(s: &str) -> Option<Self> {
56        match s.to_lowercase().as_str() {
57            "query_execute" | "query" => Some(Permission::QueryExecute),
58            "cache_read" => Some(Permission::CacheRead),
59            "cache_write" => Some(Permission::CacheWrite),
60            "http_fetch" | "http" => Some(Permission::HttpFetch),
61            "crypto" | "cryptography" => Some(Permission::Crypto),
62            "kv_read" => Some(Permission::KvRead),
63            "kv_write" => Some(Permission::KvWrite),
64            "metrics" => Some(Permission::Metrics),
65            "config_read" | "config" => Some(Permission::ConfigRead),
66            "network" => Some(Permission::Network),
67            "filesystem_read" | "fs_read" => Some(Permission::FilesystemRead),
68            "filesystem_write" | "fs_write" => Some(Permission::FilesystemWrite),
69            other => Some(Permission::Custom(other.to_string())),
70        }
71    }
72
73    /// Convert to string
74    pub fn as_str(&self) -> &str {
75        match self {
76            Permission::QueryExecute => "query_execute",
77            Permission::CacheRead => "cache_read",
78            Permission::CacheWrite => "cache_write",
79            Permission::HttpFetch => "http_fetch",
80            Permission::Crypto => "crypto",
81            Permission::KvRead => "kv_read",
82            Permission::KvWrite => "kv_write",
83            Permission::Metrics => "metrics",
84            Permission::ConfigRead => "config_read",
85            Permission::Network => "network",
86            Permission::FilesystemRead => "filesystem_read",
87            Permission::FilesystemWrite => "filesystem_write",
88            Permission::Custom(name) => name,
89        }
90    }
91
92    /// Check if this is a dangerous permission
93    pub fn is_dangerous(&self) -> bool {
94        matches!(
95            self,
96            Permission::Network
97                | Permission::FilesystemRead
98                | Permission::FilesystemWrite
99                | Permission::QueryExecute
100        )
101    }
102}
103
104/// Security policy
105#[derive(Debug, Clone)]
106pub struct SecurityPolicy {
107    /// Allowed hosts for HTTP requests
108    pub allowed_hosts: Vec<String>,
109
110    /// Allowed filesystem paths
111    pub allowed_paths: Vec<PathBuf>,
112
113    /// Maximum memory
114    pub max_memory: usize,
115
116    /// Maximum execution time
117    pub max_execution_time: Duration,
118
119    /// Allow network access
120    pub allow_network: bool,
121
122    /// Allow filesystem access
123    pub allow_filesystem: bool,
124}
125
126impl Default for SecurityPolicy {
127    fn default() -> Self {
128        Self {
129            allowed_hosts: Vec::new(),
130            allowed_paths: Vec::new(),
131            max_memory: 64 * 1024 * 1024, // 64MB
132            max_execution_time: Duration::from_millis(100),
133            allow_network: false,
134            allow_filesystem: false,
135        }
136    }
137}
138
139/// Resource limits
140#[derive(Debug, Clone)]
141pub struct ResourceLimits {
142    /// Maximum memory in bytes
143    pub max_memory: usize,
144
145    /// Maximum execution time
146    pub max_execution_time: Duration,
147
148    /// Maximum fuel (instruction count)
149    pub max_fuel: Option<u64>,
150
151    /// Maximum table elements
152    pub max_table_elements: u32,
153
154    /// Maximum instances
155    pub max_instances: u32,
156}
157
158impl Default for ResourceLimits {
159    fn default() -> Self {
160        Self {
161            max_memory: 64 * 1024 * 1024, // 64MB
162            max_execution_time: Duration::from_millis(100),
163            max_fuel: Some(1_000_000),
164            max_table_elements: 10000,
165            max_instances: 1,
166        }
167    }
168}
169
170/// Plugin sandbox
171#[derive(Debug, Clone)]
172pub struct PluginSandbox {
173    /// Security policy
174    policy: SecurityPolicy,
175
176    /// Resource limits
177    limits: ResourceLimits,
178
179    /// Granted permissions
180    permissions: HashSet<Permission>,
181
182    /// Denied hosts
183    denied_hosts: HashSet<String>,
184
185    /// Denied paths
186    denied_paths: HashSet<PathBuf>,
187}
188
189impl PluginSandbox {
190    /// Create a new sandbox with policy and permissions
191    pub fn new(
192        policy: SecurityPolicy,
193        limits: ResourceLimits,
194        permissions: Vec<Permission>,
195    ) -> Self {
196        Self {
197            policy,
198            limits,
199            permissions: permissions.into_iter().collect(),
200            denied_hosts: HashSet::new(),
201            denied_paths: HashSet::new(),
202        }
203    }
204
205    /// Check if a permission is granted
206    pub fn has_permission(&self, permission: &Permission) -> bool {
207        self.permissions.contains(permission)
208    }
209
210    /// Grant a permission
211    pub fn grant_permission(&mut self, permission: Permission) {
212        self.permissions.insert(permission);
213    }
214
215    /// Revoke a permission
216    pub fn revoke_permission(&mut self, permission: &Permission) {
217        self.permissions.remove(permission);
218    }
219
220    /// Check if a host is allowed
221    pub fn is_host_allowed(&self, host: &str) -> bool {
222        if self.denied_hosts.contains(host) {
223            return false;
224        }
225
226        if !self.policy.allow_network && !self.has_permission(&Permission::Network) {
227            return false;
228        }
229
230        // Check if host matches allowed patterns
231        self.policy.allowed_hosts.iter().any(|allowed| {
232            if let Some(suffix) = allowed.strip_prefix('*') {
233                // Wildcard matching
234                host.ends_with(suffix)
235            } else {
236                host == allowed
237            }
238        })
239    }
240
241    /// Check if a path is allowed
242    pub fn is_path_allowed(&self, path: &PathBuf) -> bool {
243        if self.denied_paths.contains(path) {
244            return false;
245        }
246
247        if !self.policy.allow_filesystem
248            && !self.has_permission(&Permission::FilesystemRead)
249            && !self.has_permission(&Permission::FilesystemWrite)
250        {
251            return false;
252        }
253
254        // Check if path is under allowed directories
255        self.policy
256            .allowed_paths
257            .iter()
258            .any(|allowed| path.starts_with(allowed))
259    }
260
261    /// Deny a host
262    pub fn deny_host(&mut self, host: String) {
263        self.denied_hosts.insert(host);
264    }
265
266    /// Deny a path
267    pub fn deny_path(&mut self, path: PathBuf) {
268        self.denied_paths.insert(path);
269    }
270
271    /// Get resource limits
272    pub fn limits(&self) -> &ResourceLimits {
273        &self.limits
274    }
275
276    /// Get security policy
277    pub fn policy(&self) -> &SecurityPolicy {
278        &self.policy
279    }
280
281    /// Get granted permissions
282    pub fn permissions(&self) -> &HashSet<Permission> {
283        &self.permissions
284    }
285
286    /// Validate a function call
287    pub fn validate_call(
288        &self,
289        function: &super::host_functions::HostFunction,
290    ) -> Result<(), SecurityError> {
291        // Check if function requires a permission
292        if let Some(required) = function.required_permission() {
293            if !self.has_permission(&required) {
294                return Err(SecurityError::PermissionDenied(format!(
295                    "Function {:?} requires permission {:?}",
296                    function, required
297                )));
298            }
299        }
300
301        Ok(())
302    }
303
304    /// Validate resource usage
305    pub fn validate_resources(
306        &self,
307        memory_used: usize,
308        fuel_consumed: Option<u64>,
309    ) -> Result<(), SecurityError> {
310        // Check memory
311        if memory_used > self.limits.max_memory {
312            return Err(SecurityError::ResourceExceeded(format!(
313                "Memory limit exceeded: {} > {}",
314                memory_used, self.limits.max_memory
315            )));
316        }
317
318        // Check fuel
319        if let (Some(consumed), Some(limit)) = (fuel_consumed, self.limits.max_fuel) {
320            if consumed > limit {
321                return Err(SecurityError::ResourceExceeded(format!(
322                    "Fuel limit exceeded: {} > {}",
323                    consumed, limit
324                )));
325            }
326        }
327
328        Ok(())
329    }
330}
331
332impl Default for PluginSandbox {
333    fn default() -> Self {
334        Self::new(
335            SecurityPolicy::default(),
336            ResourceLimits::default(),
337            Vec::new(),
338        )
339    }
340}
341
342/// Security error
343#[derive(Debug, Clone)]
344pub enum SecurityError {
345    /// Permission denied
346    PermissionDenied(String),
347
348    /// Resource exceeded
349    ResourceExceeded(String),
350
351    /// Host not allowed
352    HostNotAllowed(String),
353
354    /// Path not allowed
355    PathNotAllowed(String),
356
357    /// Operation not allowed
358    OperationNotAllowed(String),
359}
360
361impl std::fmt::Display for SecurityError {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        match self {
364            SecurityError::PermissionDenied(msg) => write!(f, "Permission denied: {}", msg),
365            SecurityError::ResourceExceeded(msg) => write!(f, "Resource exceeded: {}", msg),
366            SecurityError::HostNotAllowed(msg) => write!(f, "Host not allowed: {}", msg),
367            SecurityError::PathNotAllowed(msg) => write!(f, "Path not allowed: {}", msg),
368            SecurityError::OperationNotAllowed(msg) => write!(f, "Operation not allowed: {}", msg),
369        }
370    }
371}
372
373impl std::error::Error for SecurityError {}
374
375/// Sandbox builder
376pub struct SandboxBuilder {
377    sandbox: PluginSandbox,
378}
379
380impl SandboxBuilder {
381    /// Create a new builder
382    pub fn new() -> Self {
383        Self {
384            sandbox: PluginSandbox::default(),
385        }
386    }
387
388    /// Set memory limit
389    pub fn memory_limit(mut self, limit: usize) -> Self {
390        self.sandbox.limits.max_memory = limit;
391        self.sandbox.policy.max_memory = limit;
392        self
393    }
394
395    /// Set execution timeout
396    pub fn timeout(mut self, timeout: Duration) -> Self {
397        self.sandbox.limits.max_execution_time = timeout;
398        self.sandbox.policy.max_execution_time = timeout;
399        self
400    }
401
402    /// Set fuel limit
403    pub fn fuel_limit(mut self, limit: u64) -> Self {
404        self.sandbox.limits.max_fuel = Some(limit);
405        self
406    }
407
408    /// Grant permission
409    pub fn grant(mut self, permission: Permission) -> Self {
410        self.sandbox.permissions.insert(permission);
411        self
412    }
413
414    /// Allow host
415    pub fn allow_host(mut self, host: String) -> Self {
416        self.sandbox.policy.allowed_hosts.push(host);
417        self
418    }
419
420    /// Allow path
421    pub fn allow_path(mut self, path: PathBuf) -> Self {
422        self.sandbox.policy.allowed_paths.push(path);
423        self
424    }
425
426    /// Enable network
427    pub fn enable_network(mut self) -> Self {
428        self.sandbox.policy.allow_network = true;
429        self
430    }
431
432    /// Enable filesystem
433    pub fn enable_filesystem(mut self) -> Self {
434        self.sandbox.policy.allow_filesystem = true;
435        self
436    }
437
438    /// Build the sandbox
439    pub fn build(self) -> PluginSandbox {
440        self.sandbox
441    }
442}
443
444impl Default for SandboxBuilder {
445    fn default() -> Self {
446        Self::new()
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    #[test]
455    fn test_permission_from_str() {
456        assert_eq!(
457            Permission::from_str("http_fetch"),
458            Some(Permission::HttpFetch)
459        );
460        assert_eq!(
461            Permission::from_str("cache_read"),
462            Some(Permission::CacheRead)
463        );
464        assert_eq!(
465            Permission::from_str("unknown"),
466            Some(Permission::Custom("unknown".to_string()))
467        );
468    }
469
470    #[test]
471    fn test_permission_as_str() {
472        assert_eq!(Permission::HttpFetch.as_str(), "http_fetch");
473        assert_eq!(Permission::CacheRead.as_str(), "cache_read");
474    }
475
476    #[test]
477    fn test_permission_is_dangerous() {
478        assert!(Permission::Network.is_dangerous());
479        assert!(Permission::FilesystemRead.is_dangerous());
480        assert!(!Permission::CacheRead.is_dangerous());
481        assert!(!Permission::Metrics.is_dangerous());
482    }
483
484    #[test]
485    fn test_sandbox_default() {
486        let sandbox = PluginSandbox::default();
487        assert!(sandbox.permissions.is_empty());
488        assert_eq!(sandbox.limits.max_memory, 64 * 1024 * 1024);
489    }
490
491    #[test]
492    fn test_sandbox_permissions() {
493        let mut sandbox = PluginSandbox::default();
494
495        assert!(!sandbox.has_permission(&Permission::HttpFetch));
496
497        sandbox.grant_permission(Permission::HttpFetch);
498        assert!(sandbox.has_permission(&Permission::HttpFetch));
499
500        sandbox.revoke_permission(&Permission::HttpFetch);
501        assert!(!sandbox.has_permission(&Permission::HttpFetch));
502    }
503
504    #[test]
505    fn test_sandbox_host_check() {
506        let sandbox = SandboxBuilder::new()
507            .enable_network()
508            .grant(Permission::Network)
509            .allow_host("api.example.com".to_string())
510            .allow_host("*.internal.com".to_string())
511            .build();
512
513        assert!(sandbox.is_host_allowed("api.example.com"));
514        assert!(sandbox.is_host_allowed("service.internal.com"));
515        assert!(!sandbox.is_host_allowed("malicious.com"));
516    }
517
518    #[test]
519    fn test_sandbox_path_check() {
520        let sandbox = SandboxBuilder::new()
521            .enable_filesystem()
522            .grant(Permission::FilesystemRead)
523            .allow_path(PathBuf::from("/tmp/plugins"))
524            .build();
525
526        assert!(sandbox.is_path_allowed(&PathBuf::from("/tmp/plugins/data.txt")));
527        assert!(!sandbox.is_path_allowed(&PathBuf::from("/etc/passwd")));
528    }
529
530    #[test]
531    fn test_sandbox_validate_resources() {
532        let sandbox = SandboxBuilder::new()
533            .memory_limit(1024 * 1024) // 1MB
534            .fuel_limit(1000)
535            .build();
536
537        // Within limits
538        assert!(sandbox.validate_resources(512 * 1024, Some(500)).is_ok());
539
540        // Memory exceeded
541        assert!(sandbox
542            .validate_resources(2 * 1024 * 1024, Some(500))
543            .is_err());
544
545        // Fuel exceeded
546        assert!(sandbox.validate_resources(512 * 1024, Some(2000)).is_err());
547    }
548
549    #[test]
550    fn test_sandbox_builder() {
551        let sandbox = SandboxBuilder::new()
552            .memory_limit(32 * 1024 * 1024)
553            .timeout(Duration::from_millis(50))
554            .fuel_limit(500_000)
555            .grant(Permission::CacheRead)
556            .grant(Permission::CacheWrite)
557            .allow_host("localhost".to_string())
558            .build();
559
560        assert_eq!(sandbox.limits.max_memory, 32 * 1024 * 1024);
561        assert_eq!(sandbox.limits.max_execution_time, Duration::from_millis(50));
562        assert_eq!(sandbox.limits.max_fuel, Some(500_000));
563        assert!(sandbox.has_permission(&Permission::CacheRead));
564        assert!(sandbox.has_permission(&Permission::CacheWrite));
565    }
566
567    #[test]
568    fn test_security_error_display() {
569        let err = SecurityError::PermissionDenied("http_fetch".to_string());
570        assert!(err.to_string().contains("Permission denied"));
571
572        let err = SecurityError::ResourceExceeded("memory".to_string());
573        assert!(err.to_string().contains("Resource exceeded"));
574    }
575
576    #[test]
577    fn test_denied_hosts_and_paths() {
578        let mut sandbox = SandboxBuilder::new()
579            .enable_network()
580            .grant(Permission::Network)
581            .allow_host("*.example.com".to_string())
582            .build();
583
584        sandbox.deny_host("bad.example.com".to_string());
585
586        assert!(sandbox.is_host_allowed("good.example.com"));
587        assert!(!sandbox.is_host_allowed("bad.example.com"));
588    }
589}