1use crate::vfs::{
2 validate_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat,
3 VirtualUtimeSpec,
4};
5use std::collections::{BTreeMap, HashMap};
6use std::error::Error;
7use std::fmt;
8use std::path::Path;
9use std::sync::Arc;
10
11pub type FsPermissionCheck = Arc<dyn Fn(&FsAccessRequest) -> PermissionDecision + Send + Sync>;
12pub type NetworkPermissionCheck =
13 Arc<dyn Fn(&NetworkAccessRequest) -> PermissionDecision + Send + Sync>;
14pub type CommandPermissionCheck =
15 Arc<dyn Fn(&CommandAccessRequest) -> PermissionDecision + Send + Sync>;
16pub type EnvironmentPermissionCheck =
17 Arc<dyn Fn(&EnvAccessRequest) -> PermissionDecision + Send + Sync>;
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct PermissionDecision {
21 pub allow: bool,
22 pub reason: Option<String>,
23}
24
25impl PermissionDecision {
26 pub fn allow() -> Self {
27 Self {
28 allow: true,
29 reason: None,
30 }
31 }
32
33 pub fn deny(reason: impl Into<String>) -> Self {
34 Self {
35 allow: false,
36 reason: Some(reason.into()),
37 }
38 }
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct PermissionError {
43 code: &'static str,
44 message: String,
45}
46
47impl PermissionError {
48 pub fn code(&self) -> &'static str {
49 self.code
50 }
51
52 fn access_denied(subject: impl Into<String>, reason: Option<&str>) -> Self {
53 let subject = subject.into();
54 let message = match reason {
55 Some(reason) => format!("permission denied, {subject}: {reason}"),
56 None => format!("permission denied, {subject}"),
57 };
58
59 Self {
60 code: "EACCES",
61 message,
62 }
63 }
64}
65
66impl fmt::Display for PermissionError {
67 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68 write!(f, "{}: {}", self.code, self.message)
69 }
70}
71
72impl Error for PermissionError {}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum FsOperation {
76 Read,
77 Write,
78 Mkdir,
79 CreateDir,
80 ReadDir,
81 Stat,
82 Remove,
83 Rename,
84 Exists,
85 Symlink,
86 ReadLink,
87 Link,
88 Chmod,
89 Chown,
90 Utimes,
91 Truncate,
92 MountSensitive,
93}
94
95impl FsOperation {
96 fn as_str(self) -> &'static str {
97 match self {
98 Self::Read => "read",
99 Self::Write => "write",
100 Self::Mkdir => "mkdir",
101 Self::CreateDir => "createDir",
102 Self::ReadDir => "readdir",
103 Self::Stat => "stat",
104 Self::Remove => "rm",
105 Self::Rename => "rename",
106 Self::Exists => "exists",
107 Self::Symlink => "symlink",
108 Self::ReadLink => "readlink",
109 Self::Link => "link",
110 Self::Chmod => "chmod",
111 Self::Chown => "chown",
112 Self::Utimes => "utimes",
113 Self::Truncate => "truncate",
114 Self::MountSensitive => "mount",
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct FsAccessRequest {
121 pub vm_id: String,
122 pub op: FsOperation,
123 pub path: String,
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum NetworkOperation {
128 Fetch,
129 Http,
130 Dns,
131 Listen,
132}
133
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub struct NetworkAccessRequest {
136 pub vm_id: String,
137 pub op: NetworkOperation,
138 pub resource: String,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct CommandAccessRequest {
143 pub vm_id: String,
144 pub command: String,
145 pub args: Vec<String>,
146 pub cwd: Option<String>,
147 pub env: BTreeMap<String, String>,
148}
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151pub enum EnvironmentOperation {
152 Read,
153 Write,
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct EnvAccessRequest {
158 pub vm_id: String,
159 pub op: EnvironmentOperation,
160 pub key: String,
161 pub value: Option<String>,
162}
163
164#[derive(Clone, Default)]
165pub struct Permissions {
166 pub filesystem: Option<FsPermissionCheck>,
167 pub network: Option<NetworkPermissionCheck>,
168 pub child_process: Option<CommandPermissionCheck>,
169 pub environment: Option<EnvironmentPermissionCheck>,
170}
171
172impl Permissions {
173 pub fn allow_all() -> Self {
174 Self {
175 filesystem: Some(Arc::new(|_: &FsAccessRequest| PermissionDecision::allow())),
176 network: Some(Arc::new(|_: &NetworkAccessRequest| {
177 PermissionDecision::allow()
178 })),
179 child_process: Some(Arc::new(|_: &CommandAccessRequest| {
180 PermissionDecision::allow()
181 })),
182 environment: Some(Arc::new(|_: &EnvAccessRequest| PermissionDecision::allow())),
183 }
184 }
185}
186
187pub fn permission_glob_matches(pattern: &str, value: &str) -> bool {
188 fn matches(
189 pattern: &[u8],
190 value: &[u8],
191 pattern_index: usize,
192 value_index: usize,
193 memo: &mut HashMap<(usize, usize), bool>,
194 ) -> bool {
195 if let Some(result) = memo.get(&(pattern_index, value_index)) {
196 return *result;
197 }
198
199 let result = if pattern_index == pattern.len() {
200 value_index == value.len()
201 } else {
202 match pattern[pattern_index] {
203 b'?' => {
204 value_index < value.len()
205 && value[value_index] != b'/'
206 && matches(pattern, value, pattern_index + 1, value_index + 1, memo)
207 }
208 b'*' => {
209 let mut next_pattern_index = pattern_index;
210 while next_pattern_index < pattern.len() && pattern[next_pattern_index] == b'*'
211 {
212 next_pattern_index += 1;
213 }
214
215 if matches(pattern, value, next_pattern_index, value_index, memo) {
216 true
217 } else {
218 let crosses_separators = next_pattern_index - pattern_index > 1;
219 let mut next_value_index = value_index;
220 while next_value_index < value.len()
221 && (crosses_separators || value[next_value_index] != b'/')
222 {
223 next_value_index += 1;
224 if matches(pattern, value, next_pattern_index, next_value_index, memo) {
225 return true;
226 }
227 }
228 false
229 }
230 }
231 expected => {
232 value_index < value.len()
233 && expected == value[value_index]
234 && matches(pattern, value, pattern_index + 1, value_index + 1, memo)
235 }
236 }
237 };
238
239 memo.insert((pattern_index, value_index), result);
240 result
241 }
242
243 matches(
244 pattern.as_bytes(),
245 value.as_bytes(),
246 0,
247 0,
248 &mut HashMap::new(),
249 )
250}
251
252pub fn filter_env(
253 vm_id: &str,
254 env: &BTreeMap<String, String>,
255 permissions: &Permissions,
256) -> BTreeMap<String, String> {
257 let Some(check) = permissions.environment.as_ref() else {
258 return BTreeMap::new();
259 };
260
261 env.iter()
262 .filter_map(|(key, value)| {
263 let request = EnvAccessRequest {
264 vm_id: vm_id.to_owned(),
265 op: EnvironmentOperation::Read,
266 key: key.clone(),
267 value: Some(value.clone()),
268 };
269 let decision = check(&request);
270 decision.allow.then(|| (key.clone(), value.clone()))
271 })
272 .collect()
273}
274
275pub fn check_command_execution(
276 vm_id: &str,
277 permissions: &Permissions,
278 command: &str,
279 args: &[String],
280 cwd: Option<&str>,
281 env: &BTreeMap<String, String>,
282) -> Result<(), PermissionError> {
283 let Some(check) = permissions.child_process.as_ref() else {
284 return Err(PermissionError::access_denied(
285 format!("spawn '{command}'"),
286 None,
287 ));
288 };
289
290 let request = CommandAccessRequest {
291 vm_id: vm_id.to_owned(),
292 command: command.to_owned(),
293 args: args.to_vec(),
294 cwd: cwd.map(ToOwned::to_owned),
295 env: env.clone(),
296 };
297 let decision = check(&request);
298 if decision.allow {
299 Ok(())
300 } else {
301 Err(PermissionError::access_denied(
302 format!("spawn '{command}'"),
303 decision.reason.as_deref(),
304 ))
305 }
306}
307
308pub fn check_network_access(
309 vm_id: &str,
310 permissions: &Permissions,
311 op: NetworkOperation,
312 resource: &str,
313) -> Result<(), PermissionError> {
314 let Some(check) = permissions.network.as_ref() else {
315 return Err(PermissionError::access_denied(resource, None));
316 };
317
318 let request = NetworkAccessRequest {
319 vm_id: vm_id.to_owned(),
320 op,
321 resource: resource.to_owned(),
322 };
323 let decision = check(&request);
324 if decision.allow {
325 Ok(())
326 } else {
327 Err(PermissionError::access_denied(
328 resource,
329 decision.reason.as_deref(),
330 ))
331 }
332}
333
334#[derive(Clone)]
335pub struct PermissionedFileSystem<F> {
336 inner: F,
337 vm_id: String,
338 permissions: Permissions,
339}
340
341impl<F> PermissionedFileSystem<F> {
342 pub fn new(inner: F, vm_id: impl Into<String>, permissions: Permissions) -> Self {
343 Self {
344 inner,
345 vm_id: vm_id.into(),
346 permissions,
347 }
348 }
349
350 pub fn into_inner(self) -> F {
351 self.inner
352 }
353
354 pub fn inner(&self) -> &F {
355 &self.inner
356 }
357
358 pub fn inner_mut(&mut self) -> &mut F {
359 &mut self.inner
360 }
361
362 pub fn set_permissions(&mut self, permissions: Permissions) {
363 self.permissions = permissions;
364 }
365
366 fn check(&self, op: FsOperation, path: &str) -> VfsResult<()> {
367 validate_path(path)?;
368 if crate::device_layer::is_standard_device_path(path) {
374 return Ok(());
375 }
376 let Some(check) = self.permissions.filesystem.as_ref() else {
377 return Err(VfsError::access_denied(op.as_str(), path, None));
378 };
379
380 let request = FsAccessRequest {
381 vm_id: self.vm_id.clone(),
382 op,
383 path: path.to_owned(),
384 };
385 let decision = check(&request);
386 if decision.allow {
387 Ok(())
388 } else {
389 Err(VfsError::access_denied(
390 op.as_str(),
391 path,
392 decision.reason.as_deref(),
393 ))
394 }
395 }
396}
397
398impl<F: VirtualFileSystem> PermissionedFileSystem<F> {
399 fn resolved_existing_path(&self, path: &str) -> VfsResult<String> {
400 self.inner.realpath(path)
401 }
402
403 fn resolved_destination_path(&self, path: &str) -> VfsResult<String> {
404 let normalized = crate::vfs::normalize_path(path);
405 if normalized == "/" {
406 return Ok(normalized);
407 }
408
409 let parent = Path::new(&normalized)
410 .parent()
411 .unwrap_or_else(|| Path::new("/"))
412 .to_string_lossy()
413 .into_owned();
414 let basename = Path::new(&normalized)
415 .file_name()
416 .map(|value| value.to_string_lossy().into_owned())
417 .unwrap_or_default();
418
419 let mut candidate = parent;
420 let mut unresolved_segments = Vec::new();
421
422 let resolved_parent = loop {
423 match self.inner.realpath(&candidate) {
424 Ok(resolved) => break resolved,
425 Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => {
426 if candidate == "/" {
427 break String::from("/");
428 }
429 let candidate_path = Path::new(&candidate);
430 if let Some(segment) = candidate_path.file_name() {
431 unresolved_segments.push(segment.to_string_lossy().into_owned());
432 }
433 candidate = candidate_path
434 .parent()
435 .unwrap_or_else(|| Path::new("/"))
436 .to_string_lossy()
437 .into_owned();
438 }
439 Err(error) => return Err(error),
440 }
441 };
442
443 let mut resolved = resolved_parent;
444 for segment in unresolved_segments.iter().rev() {
445 if resolved == "/" {
446 resolved = format!("/{segment}");
447 } else {
448 resolved = format!("{resolved}/{segment}");
449 }
450 }
451
452 if resolved == "/" {
453 Ok(format!("/{basename}"))
454 } else {
455 Ok(format!("{resolved}/{basename}"))
456 }
457 }
458
459 fn permission_subject(&self, op: FsOperation, path: &str) -> VfsResult<String> {
460 validate_path(path)?;
461 match op {
462 FsOperation::Read
463 | FsOperation::ReadDir
464 | FsOperation::Stat
465 | FsOperation::ReadLink
466 | FsOperation::Chmod
467 | FsOperation::Chown
468 | FsOperation::Utimes
469 | FsOperation::Truncate => self.resolved_existing_path(path),
470 FsOperation::Exists | FsOperation::Write => self
471 .resolved_existing_path(path)
472 .or_else(|_| self.resolved_destination_path(path)),
473 FsOperation::Mkdir
474 | FsOperation::CreateDir
475 | FsOperation::Rename
476 | FsOperation::Symlink
477 | FsOperation::Link
478 | FsOperation::MountSensitive
479 | FsOperation::Remove => self.resolved_destination_path(path),
480 }
481 }
482
483 fn check_subject(&self, op: FsOperation, path: &str) -> VfsResult<()> {
484 let subject = self.permission_subject(op, path)?;
485 self.check(op, &subject)
486 }
487
488 fn check_existing_subject(&self, op: FsOperation, path: &str) -> VfsResult<()> {
489 validate_path(path)?;
490 let subject = self.resolved_existing_path(path)?;
491 self.check(op, &subject)
492 }
493
494 fn check_destination_subject(&self, op: FsOperation, path: &str) -> VfsResult<()> {
495 validate_path(path)?;
496 let subject = self.resolved_destination_path(path)?;
497 self.check(op, &subject)
498 }
499
500 pub fn check_path(&self, op: FsOperation, path: &str) -> VfsResult<()> {
501 self.check_subject(op, path)
502 }
503
504 pub fn check_virtual_path(&self, op: FsOperation, path: &str) -> VfsResult<()> {
505 self.check(op, path)
506 }
507
508 pub fn exists(&self, path: &str) -> VfsResult<bool> {
509 if let Err(error) = self.check_subject(FsOperation::Exists, path) {
510 if matches!(error.code(), "EACCES" | "ENOENT" | "ENOTDIR" | "ELOOP") {
511 return Ok(false);
512 }
513 return Err(error);
514 }
515 Ok(self.inner.exists(path))
516 }
517}
518
519impl<F: VirtualFileSystem> VirtualFileSystem for PermissionedFileSystem<F> {
520 fn read_file(&mut self, path: &str) -> VfsResult<Vec<u8>> {
521 self.check_subject(FsOperation::Read, path)?;
522 self.inner.read_file(path)
523 }
524
525 fn read_dir(&mut self, path: &str) -> VfsResult<Vec<String>> {
526 self.check_subject(FsOperation::ReadDir, path)?;
527 self.inner.read_dir(path)
528 }
529
530 fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult<Vec<String>> {
531 self.check_subject(FsOperation::ReadDir, path)?;
532 self.inner.read_dir_limited(path, max_entries)
533 }
534
535 fn read_dir_with_types(&mut self, path: &str) -> VfsResult<Vec<VirtualDirEntry>> {
536 self.check_subject(FsOperation::ReadDir, path)?;
537 self.inner.read_dir_with_types(path)
538 }
539
540 fn write_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
541 self.check_subject(FsOperation::Write, path)?;
542 self.inner.write_file(path, content)
543 }
544
545 fn create_file_exclusive(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<()> {
546 self.check_subject(FsOperation::Write, path)?;
547 self.inner.create_file_exclusive(path, content)
548 }
549
550 fn append_file(&mut self, path: &str, content: impl Into<Vec<u8>>) -> VfsResult<u64> {
551 self.check_subject(FsOperation::Write, path)?;
552 self.inner.append_file(path, content)
553 }
554
555 fn create_dir(&mut self, path: &str) -> VfsResult<()> {
556 self.check_subject(FsOperation::CreateDir, path)?;
557 self.inner.create_dir(path)
558 }
559
560 fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> {
561 self.check_subject(FsOperation::Mkdir, path)?;
562 self.inner.mkdir(path, recursive)
563 }
564
565 fn exists(&self, path: &str) -> bool {
566 PermissionedFileSystem::exists(self, path).unwrap_or(false)
567 }
568
569 fn stat(&mut self, path: &str) -> VfsResult<VirtualStat> {
570 self.check_subject(FsOperation::Stat, path)?;
571 self.inner.stat(path)
572 }
573
574 fn remove_file(&mut self, path: &str) -> VfsResult<()> {
575 self.check_subject(FsOperation::Remove, path)?;
576 self.inner.remove_file(path)
577 }
578
579 fn remove_dir(&mut self, path: &str) -> VfsResult<()> {
580 self.check_subject(FsOperation::Remove, path)?;
581 self.inner.remove_dir(path)
582 }
583
584 fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
585 self.check_subject(FsOperation::Rename, old_path)?;
586 self.check_subject(FsOperation::Rename, new_path)?;
587 self.inner.rename(old_path, new_path)
588 }
589
590 fn realpath(&self, path: &str) -> VfsResult<String> {
591 self.check_subject(FsOperation::Read, path)?;
592 self.inner.realpath(path)
593 }
594
595 fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> {
596 self.check_subject(FsOperation::Symlink, link_path)?;
597 self.inner.symlink(target, link_path)
598 }
599
600 fn read_link(&self, path: &str) -> VfsResult<String> {
601 validate_path(path)?;
606 let subject = self.resolved_destination_path(path)?;
607 self.check(FsOperation::ReadLink, &subject)?;
608 self.inner.read_link(path)
609 }
610
611 fn lstat(&self, path: &str) -> VfsResult<VirtualStat> {
612 validate_path(path)?;
616 let subject = self.resolved_destination_path(path)?;
617 self.check(FsOperation::Stat, &subject)?;
618 self.inner.lstat(path)
619 }
620
621 fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> {
622 self.check_existing_subject(FsOperation::Link, old_path)?;
623 self.check_destination_subject(FsOperation::Link, new_path)?;
624 self.inner.link(old_path, new_path)
625 }
626
627 fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> {
628 self.check_subject(FsOperation::Chmod, path)?;
629 self.inner.chmod(path, mode)
630 }
631
632 fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> {
633 self.check_subject(FsOperation::Chown, path)?;
634 self.inner.chown(path, uid, gid)
635 }
636
637 fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> {
638 self.check_subject(FsOperation::Utimes, path)?;
639 self.inner.utimes(path, atime_ms, mtime_ms)
640 }
641
642 fn utimes_spec(
643 &mut self,
644 path: &str,
645 atime: VirtualUtimeSpec,
646 mtime: VirtualUtimeSpec,
647 follow_symlinks: bool,
648 ) -> VfsResult<()> {
649 self.check_subject(FsOperation::Utimes, path)?;
650 self.inner.utimes_spec(path, atime, mtime, follow_symlinks)
651 }
652
653 fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> {
654 self.check_subject(FsOperation::Truncate, path)?;
655 self.inner.truncate(path, length)
656 }
657
658 fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult<Vec<u8>> {
659 self.check_subject(FsOperation::Read, path)?;
660 self.inner.pread(path, offset, length)
661 }
662}