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 #[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 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 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#[derive(Debug, Clone)]
106pub struct SecurityPolicy {
107 pub allowed_hosts: Vec<String>,
109
110 pub allowed_paths: Vec<PathBuf>,
112
113 pub max_memory: usize,
115
116 pub max_execution_time: Duration,
118
119 pub allow_network: bool,
121
122 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, max_execution_time: Duration::from_millis(100),
133 allow_network: false,
134 allow_filesystem: false,
135 }
136 }
137}
138
139#[derive(Debug, Clone)]
141pub struct ResourceLimits {
142 pub max_memory: usize,
144
145 pub max_execution_time: Duration,
147
148 pub max_fuel: Option<u64>,
150
151 pub max_table_elements: u32,
153
154 pub max_instances: u32,
156}
157
158impl Default for ResourceLimits {
159 fn default() -> Self {
160 Self {
161 max_memory: 64 * 1024 * 1024, 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#[derive(Debug, Clone)]
172pub struct PluginSandbox {
173 policy: SecurityPolicy,
175
176 limits: ResourceLimits,
178
179 permissions: HashSet<Permission>,
181
182 denied_hosts: HashSet<String>,
184
185 denied_paths: HashSet<PathBuf>,
187}
188
189impl PluginSandbox {
190 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 pub fn has_permission(&self, permission: &Permission) -> bool {
207 self.permissions.contains(permission)
208 }
209
210 pub fn grant_permission(&mut self, permission: Permission) {
212 self.permissions.insert(permission);
213 }
214
215 pub fn revoke_permission(&mut self, permission: &Permission) {
217 self.permissions.remove(permission);
218 }
219
220 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 self.policy.allowed_hosts.iter().any(|allowed| {
232 if let Some(suffix) = allowed.strip_prefix('*') {
233 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
256 .allowed_paths
257 .iter()
258 .any(|allowed| path.starts_with(allowed))
259 }
260
261 pub fn deny_host(&mut self, host: String) {
263 self.denied_hosts.insert(host);
264 }
265
266 pub fn deny_path(&mut self, path: PathBuf) {
268 self.denied_paths.insert(path);
269 }
270
271 pub fn limits(&self) -> &ResourceLimits {
273 &self.limits
274 }
275
276 pub fn policy(&self) -> &SecurityPolicy {
278 &self.policy
279 }
280
281 pub fn permissions(&self) -> &HashSet<Permission> {
283 &self.permissions
284 }
285
286 pub fn validate_call(
288 &self,
289 function: &super::host_functions::HostFunction,
290 ) -> Result<(), SecurityError> {
291 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 pub fn validate_resources(
306 &self,
307 memory_used: usize,
308 fuel_consumed: Option<u64>,
309 ) -> Result<(), SecurityError> {
310 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 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#[derive(Debug, Clone)]
344pub enum SecurityError {
345 PermissionDenied(String),
347
348 ResourceExceeded(String),
350
351 HostNotAllowed(String),
353
354 PathNotAllowed(String),
356
357 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
375pub struct SandboxBuilder {
377 sandbox: PluginSandbox,
378}
379
380impl SandboxBuilder {
381 pub fn new() -> Self {
383 Self {
384 sandbox: PluginSandbox::default(),
385 }
386 }
387
388 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 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 pub fn fuel_limit(mut self, limit: u64) -> Self {
404 self.sandbox.limits.max_fuel = Some(limit);
405 self
406 }
407
408 pub fn grant(mut self, permission: Permission) -> Self {
410 self.sandbox.permissions.insert(permission);
411 self
412 }
413
414 pub fn allow_host(mut self, host: String) -> Self {
416 self.sandbox.policy.allowed_hosts.push(host);
417 self
418 }
419
420 pub fn allow_path(mut self, path: PathBuf) -> Self {
422 self.sandbox.policy.allowed_paths.push(path);
423 self
424 }
425
426 pub fn enable_network(mut self) -> Self {
428 self.sandbox.policy.allow_network = true;
429 self
430 }
431
432 pub fn enable_filesystem(mut self) -> Self {
434 self.sandbox.policy.allow_filesystem = true;
435 self
436 }
437
438 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) .fuel_limit(1000)
535 .build();
536
537 assert!(sandbox.validate_resources(512 * 1024, Some(500)).is_ok());
539
540 assert!(sandbox
542 .validate_resources(2 * 1024 * 1024, Some(500))
543 .is_err());
544
545 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}