1use std::collections::HashSet;
6use std::path::PathBuf;
7use std::time::Duration;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
11pub enum Permission {
12 QueryExecute,
14
15 CacheRead,
17
18 CacheWrite,
20
21 HttpFetch,
23
24 Crypto,
26
27 KvRead,
29
30 KvWrite,
32
33 Metrics,
35
36 ConfigRead,
38
39 Network,
41
42 FilesystemRead,
44
45 FilesystemWrite,
47
48 Custom(String),
50}
51
52impl Permission {
53 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 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 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#[derive(Debug, Clone)]
105pub struct SecurityPolicy {
106 pub allowed_hosts: Vec<String>,
108
109 pub allowed_paths: Vec<PathBuf>,
111
112 pub max_memory: usize,
114
115 pub max_execution_time: Duration,
117
118 pub allow_network: bool,
120
121 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, max_execution_time: Duration::from_millis(100),
132 allow_network: false,
133 allow_filesystem: false,
134 }
135 }
136}
137
138#[derive(Debug, Clone)]
140pub struct ResourceLimits {
141 pub max_memory: usize,
143
144 pub max_execution_time: Duration,
146
147 pub max_fuel: Option<u64>,
149
150 pub max_table_elements: u32,
152
153 pub max_instances: u32,
155}
156
157impl Default for ResourceLimits {
158 fn default() -> Self {
159 Self {
160 max_memory: 64 * 1024 * 1024, 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#[derive(Debug, Clone)]
171pub struct PluginSandbox {
172 policy: SecurityPolicy,
174
175 limits: ResourceLimits,
177
178 permissions: HashSet<Permission>,
180
181 denied_hosts: HashSet<String>,
183
184 denied_paths: HashSet<PathBuf>,
186}
187
188impl PluginSandbox {
189 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 pub fn has_permission(&self, permission: &Permission) -> bool {
206 self.permissions.contains(permission)
207 }
208
209 pub fn grant_permission(&mut self, permission: Permission) {
211 self.permissions.insert(permission);
212 }
213
214 pub fn revoke_permission(&mut self, permission: &Permission) {
216 self.permissions.remove(permission);
217 }
218
219 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 self.policy.allowed_hosts.iter().any(|allowed| {
231 if allowed.starts_with('*') {
232 let suffix = &allowed[1..];
234 host.ends_with(suffix)
235 } else {
236 host == allowed
237 }
238 })
239 }
240
241 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 self.policy.allowed_paths.iter().any(|allowed| {
256 path.starts_with(allowed)
257 })
258 }
259
260 pub fn deny_host(&mut self, host: String) {
262 self.denied_hosts.insert(host);
263 }
264
265 pub fn deny_path(&mut self, path: PathBuf) {
267 self.denied_paths.insert(path);
268 }
269
270 pub fn limits(&self) -> &ResourceLimits {
272 &self.limits
273 }
274
275 pub fn policy(&self) -> &SecurityPolicy {
277 &self.policy
278 }
279
280 pub fn permissions(&self) -> &HashSet<Permission> {
282 &self.permissions
283 }
284
285 pub fn validate_call(
287 &self,
288 function: &super::host_functions::HostFunction,
289 ) -> Result<(), SecurityError> {
290 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 pub fn validate_resources(
305 &self,
306 memory_used: usize,
307 fuel_consumed: Option<u64>,
308 ) -> Result<(), SecurityError> {
309 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 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#[derive(Debug, Clone)]
343pub enum SecurityError {
344 PermissionDenied(String),
346
347 ResourceExceeded(String),
349
350 HostNotAllowed(String),
352
353 PathNotAllowed(String),
355
356 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
374pub struct SandboxBuilder {
376 sandbox: PluginSandbox,
377}
378
379impl SandboxBuilder {
380 pub fn new() -> Self {
382 Self {
383 sandbox: PluginSandbox::default(),
384 }
385 }
386
387 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 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 pub fn fuel_limit(mut self, limit: u64) -> Self {
403 self.sandbox.limits.max_fuel = Some(limit);
404 self
405 }
406
407 pub fn grant(mut self, permission: Permission) -> Self {
409 self.sandbox.permissions.insert(permission);
410 self
411 }
412
413 pub fn allow_host(mut self, host: String) -> Self {
415 self.sandbox.policy.allowed_hosts.push(host);
416 self
417 }
418
419 pub fn allow_path(mut self, path: PathBuf) -> Self {
421 self.sandbox.policy.allowed_paths.push(path);
422 self
423 }
424
425 pub fn enable_network(mut self) -> Self {
427 self.sandbox.policy.allow_network = true;
428 self
429 }
430
431 pub fn enable_filesystem(mut self) -> Self {
433 self.sandbox.policy.allow_filesystem = true;
434 self
435 }
436
437 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) .fuel_limit(1000)
525 .build();
526
527 assert!(sandbox.validate_resources(512 * 1024, Some(500)).is_ok());
529
530 assert!(sandbox.validate_resources(2 * 1024 * 1024, Some(500)).is_err());
532
533 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}