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