1use std::borrow::Cow;
37use std::fmt;
38use std::net::IpAddr;
39use std::path::{Component, Path, PathBuf};
40use std::sync::Arc;
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
45#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
46pub enum Kind {
47 Read,
49 Write,
52 Net,
54 Env,
56 Sys,
59}
60
61impl Kind {
62 #[must_use]
63 pub fn as_str(self) -> &'static str {
64 match self {
65 Self::Read => "read",
66 Self::Write => "write",
67 Self::Net => "net",
68 Self::Env => "env",
69 Self::Sys => "sys",
70 }
71 }
72}
73
74impl fmt::Display for Kind {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(self.as_str())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Default)]
82pub enum Allow<T> {
83 #[default]
85 None,
86 All,
88 Only(Vec<T>),
90}
91
92impl<T> Allow<T> {
93 #[must_use]
94 pub fn is_all(&self) -> bool {
95 matches!(self, Self::All)
96 }
97
98 #[must_use]
99 pub fn is_none(&self) -> bool {
100 matches!(self, Self::None)
101 }
102
103 #[must_use]
105 pub fn entries(&self) -> &[T] {
106 match self {
107 Self::Only(list) => list,
108 _ => &[],
109 }
110 }
111
112 fn map<U>(self, f: impl FnMut(T) -> U) -> Allow<U> {
113 match self {
114 Self::None => Allow::None,
115 Self::All => Allow::All,
116 Self::Only(list) => Allow::Only(list.into_iter().map(f).collect()),
117 }
118 }
119
120 fn intersect_with(&self, other: &Self, subsumes: impl Fn(&T, &T) -> bool) -> Self
126 where
127 T: Clone + PartialEq,
128 {
129 match (self, other) {
130 (Self::None, _) | (_, Self::None) => Self::None,
131 (Self::All, o) => o.clone(),
132 (s, Self::All) => s.clone(),
133 (Self::Only(mine), Self::Only(theirs)) => {
134 let mut kept: Vec<T> = mine
135 .iter()
136 .filter(|e| theirs.iter().any(|t| subsumes(t, e)))
137 .cloned()
138 .collect();
139 for e in theirs {
140 if mine.iter().any(|m| subsumes(m, e)) && !kept.contains(e) {
141 kept.push(e.clone());
142 }
143 }
144 Self::Only(kept)
145 },
146 }
147 }
148}
149
150#[cfg(feature = "serde")]
151impl<T: serde::Serialize> serde::Serialize for Allow<T> {
152 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
153 match self {
154 Self::None => s.serialize_bool(false),
155 Self::All => s.serialize_bool(true),
156 Self::Only(list) => list.serialize(s),
157 }
158 }
159}
160
161#[cfg(feature = "serde")]
162impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Allow<T> {
163 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
164 #[derive(serde::Deserialize)]
165 #[serde(untagged)]
166 enum Repr<T> {
167 Flag(bool),
168 List(Vec<T>),
169 }
170 Ok(match Repr::<T>::deserialize(d)? {
171 Repr::Flag(true) => Self::All,
172 Repr::Flag(false) => Self::None,
173 Repr::List(list) => Self::Only(list),
174 })
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct PathRule {
182 lexical: PathBuf,
184 resolved: PathBuf,
187}
188
189impl PathRule {
190 #[must_use]
194 pub fn new(root: impl AsRef<Path>) -> Self {
195 let lexical = normalize(&absolute(root.as_ref()));
196 let resolved = resolve_existing(&lexical);
197 Self { lexical, resolved }
198 }
199
200 #[must_use]
201 pub fn root(&self) -> &Path {
202 &self.lexical
203 }
204
205 fn covers(&self, candidate: &CheckedPath) -> bool {
206 candidate.lexical.starts_with(&self.lexical) && candidate.resolved.starts_with(&self.resolved)
207 }
208}
209
210impl<P: AsRef<Path>> From<P> for PathRule {
211 fn from(p: P) -> Self {
212 Self::new(p)
213 }
214}
215
216#[cfg(feature = "serde")]
217impl serde::Serialize for PathRule {
218 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
219 self.lexical.serialize(s)
220 }
221}
222
223#[cfg(feature = "serde")]
224impl<'de> serde::Deserialize<'de> for PathRule {
225 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
226 PathBuf::deserialize(d).map(Self::new)
227 }
228}
229
230struct CheckedPath {
232 lexical: PathBuf,
233 resolved: PathBuf,
234}
235
236impl CheckedPath {
237 fn new(path: &Path) -> Self {
238 let lexical = normalize(&absolute(path));
239 let resolved = resolve_existing(&lexical);
240 Self { lexical, resolved }
241 }
242}
243
244fn absolute(path: &Path) -> PathBuf {
245 if path.is_absolute() {
246 path.to_path_buf()
247 } else {
248 std::env::current_dir().map_or_else(|_| path.to_path_buf(), |cwd| cwd.join(path))
249 }
250}
251
252fn normalize(path: &Path) -> PathBuf {
256 let mut out = PathBuf::new();
257 for component in path.components() {
258 match component {
259 Component::CurDir => {},
260 Component::ParentDir => {
261 if !matches!(
262 out.components().next_back(),
263 Some(Component::RootDir | Component::Prefix(_)) | None
264 ) {
265 out.pop();
266 }
267 },
268 other => out.push(other.as_os_str()),
269 }
270 }
271 out
272}
273
274fn resolve_existing(path: &Path) -> PathBuf {
278 let mut existing = path.to_path_buf();
279 let mut rest: Vec<std::ffi::OsString> = Vec::new();
280 loop {
281 if let Ok(canonical) = std::fs::canonicalize(&existing) {
282 let mut out = canonical;
283 for part in rest.iter().rev() {
284 out.push(part);
285 }
286 return out;
287 }
288 match existing.file_name() {
289 Some(name) => {
290 rest.push(name.to_os_string());
291 if !existing.pop() {
292 break;
293 }
294 },
295 None => break,
296 }
297 }
298 path.to_path_buf()
299}
300
301#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct NetRule {
308 host: String,
309 port: Option<u16>,
310}
311
312impl NetRule {
313 pub fn parse(entry: &str) -> Result<Self, String> {
320 let entry = entry.trim();
321 if entry.is_empty() {
322 return Err("empty network rule".to_string());
323 }
324 let (host, port) = if let Some(rest) = entry.strip_prefix('[') {
325 let (addr, after) = rest
327 .split_once(']')
328 .ok_or_else(|| format!("network rule `{entry}`: unterminated IPv6 literal"))?;
329 let port = match after.strip_prefix(':') {
330 Some(p) => Some(parse_port(entry, p)?),
331 None if after.is_empty() => None,
332 None => return Err(format!("network rule `{entry}`: unexpected `{after}` after address")),
333 };
334 (addr.to_string(), port)
335 } else if entry.matches(':').count() > 1 {
336 (entry.to_string(), None)
338 } else if let Some((host, port)) = entry.rsplit_once(':') {
339 (host.to_string(), Some(parse_port(entry, port)?))
340 } else {
341 (entry.to_string(), None)
342 };
343 let host = host.to_ascii_lowercase();
344 if host.is_empty() || host == "*." {
345 return Err(format!("network rule `{entry}`: empty host"));
346 }
347 Ok(Self { host, port })
348 }
349
350 #[must_use]
351 pub fn host(&self) -> &str {
352 &self.host
353 }
354
355 #[must_use]
356 pub fn port(&self) -> Option<u16> {
357 self.port
358 }
359
360 fn covers(&self, host: &str, port: Option<u16>) -> bool {
361 let host_ok = if self.host == host {
362 true
363 } else if let Some(suffix) = self.host.strip_prefix("*.") {
364 host == suffix || host.strip_suffix(suffix).is_some_and(|prefix| prefix.ends_with('.'))
365 } else {
366 false
367 };
368 host_ok && self.port.is_none_or(|p| port == Some(p))
369 }
370
371 fn subsumes(&self, other: &Self) -> bool {
374 let host_ok = if self.host == other.host {
375 true
376 } else if let Some(suffix) = self.host.strip_prefix("*.") {
377 other.host == suffix
378 || other
379 .host
380 .strip_suffix(suffix)
381 .is_some_and(|prefix| prefix.ends_with('.'))
382 || other.host.strip_prefix("*.").is_some_and(|other_suffix| {
383 other_suffix == suffix
384 || other_suffix
385 .strip_suffix(suffix)
386 .is_some_and(|prefix| prefix.ends_with('.'))
387 })
388 } else {
389 false
390 };
391 host_ok && (self.port.is_none() || self.port == other.port)
392 }
393}
394
395fn parse_port(entry: &str, port: &str) -> Result<u16, String> {
396 port
397 .parse::<u16>()
398 .map_err(|_| format!("network rule `{entry}`: `{port}` is not a port"))
399}
400
401impl fmt::Display for NetRule {
402 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
403 let bracket = self.host.contains(':');
404 match (bracket, self.port) {
405 (true, Some(p)) => write!(f, "[{}]:{p}", self.host),
406 (true, None) => write!(f, "{}", self.host),
407 (false, Some(p)) => write!(f, "{}:{p}", self.host),
408 (false, None) => f.write_str(&self.host),
409 }
410 }
411}
412
413impl std::str::FromStr for NetRule {
414 type Err = String;
415
416 fn from_str(s: &str) -> Result<Self, Self::Err> {
417 Self::parse(s)
418 }
419}
420
421#[cfg(feature = "serde")]
422impl serde::Serialize for NetRule {
423 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
424 s.collect_str(self)
425 }
426}
427
428#[cfg(feature = "serde")]
429impl<'de> serde::Deserialize<'de> for NetRule {
430 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
431 let s = String::deserialize(d)?;
432 Self::parse(&s).map_err(serde::de::Error::custom)
433 }
434}
435
436#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
438#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
439#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
440pub enum SysInfo {
441 Hostname,
442 OsRelease,
443 OsUptime,
444 LoadAvg,
445 NetworkInterfaces,
446 SystemMemory,
447 Uid,
448 Gid,
449 Username,
450 Cpus,
451 HomeDir,
452 Priority,
454}
455
456impl SysInfo {
457 #[must_use]
458 pub fn as_str(self) -> &'static str {
459 match self {
460 Self::Hostname => "hostname",
461 Self::OsRelease => "osRelease",
462 Self::OsUptime => "osUptime",
463 Self::LoadAvg => "loadavg",
464 Self::NetworkInterfaces => "networkInterfaces",
465 Self::SystemMemory => "systemMemoryInfo",
466 Self::Uid => "uid",
467 Self::Gid => "gid",
468 Self::Username => "username",
469 Self::Cpus => "cpus",
470 Self::HomeDir => "homedir",
471 Self::Priority => "priority",
472 }
473 }
474}
475
476impl fmt::Display for SysInfo {
477 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
478 f.write_str(self.as_str())
479 }
480}
481
482#[derive(Debug, Clone, PartialEq, Eq, Default)]
486#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
487#[cfg_attr(feature = "serde", serde(default, rename_all = "camelCase"))]
488pub struct Deny {
489 pub read: Vec<PathRule>,
490 pub write: Vec<PathRule>,
491 pub net: Vec<NetRule>,
492 pub env: Vec<String>,
493 pub sys: Vec<SysInfo>,
494}
495
496impl Deny {
497 #[must_use]
498 pub fn is_empty(&self) -> bool {
499 self.read.is_empty() && self.write.is_empty() && self.net.is_empty() && self.env.is_empty() && self.sys.is_empty()
500 }
501
502 fn merged(&self, other: &Self) -> Self {
503 fn union<T: Clone + PartialEq>(a: &[T], b: &[T]) -> Vec<T> {
504 let mut out = a.to_vec();
505 for item in b {
506 if !out.contains(item) {
507 out.push(item.clone());
508 }
509 }
510 out
511 }
512 Self {
513 read: union(&self.read, &other.read),
514 write: union(&self.write, &other.write),
515 net: union(&self.net, &other.net),
516 env: union(&self.env, &other.env),
517 sys: union(&self.sys, &other.sys),
518 }
519 }
520}
521
522#[derive(Debug, Clone, PartialEq, Eq, Default)]
527#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
528#[cfg_attr(feature = "serde", serde(default, rename_all = "camelCase"))]
529pub struct Permissions {
530 pub read: Allow<PathRule>,
531 pub write: Allow<PathRule>,
532 pub net: Allow<NetRule>,
533 pub env: Allow<String>,
534 pub sys: Allow<SysInfo>,
535 #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Deny::is_empty"))]
536 pub deny: Deny,
537}
538
539impl Permissions {
540 #[must_use]
542 pub fn none() -> Self {
543 Self::default()
544 }
545
546 #[must_use]
548 pub fn all() -> Self {
549 Self {
550 read: Allow::All,
551 write: Allow::All,
552 net: Allow::All,
553 env: Allow::All,
554 sys: Allow::All,
555 deny: Deny::default(),
556 }
557 }
558
559 #[must_use]
560 pub fn allow_read<I, P>(mut self, roots: I) -> Self
561 where
562 I: IntoIterator<Item = P>,
563 P: AsRef<Path>,
564 {
565 self.read = Allow::Only(roots.into_iter().map(PathRule::new).collect());
566 self
567 }
568
569 #[must_use]
570 pub fn allow_write<I, P>(mut self, roots: I) -> Self
571 where
572 I: IntoIterator<Item = P>,
573 P: AsRef<Path>,
574 {
575 self.write = Allow::Only(roots.into_iter().map(PathRule::new).collect());
576 self
577 }
578
579 pub fn allow_net<I, S>(mut self, hosts: I) -> Result<Self, String>
587 where
588 I: IntoIterator<Item = S>,
589 S: AsRef<str>,
590 {
591 let rules = hosts
592 .into_iter()
593 .map(|h| NetRule::parse(h.as_ref()))
594 .collect::<Result<Vec<_>, _>>()?;
595 self.net = Allow::Only(rules);
596 Ok(self)
597 }
598
599 #[must_use]
600 pub fn allow_env<I, S>(mut self, names: I) -> Self
601 where
602 I: IntoIterator<Item = S>,
603 S: Into<String>,
604 {
605 self.env = Allow::Only(names.into_iter().map(Into::into).collect());
606 self
607 }
608
609 #[must_use]
610 pub fn allow_sys<I>(mut self, items: I) -> Self
611 where
612 I: IntoIterator<Item = SysInfo>,
613 {
614 self.sys = Allow::Only(items.into_iter().collect());
615 self
616 }
617
618 #[must_use]
619 pub fn allow_all_read(mut self) -> Self {
620 self.read = Allow::All;
621 self
622 }
623
624 #[must_use]
625 pub fn allow_all_write(mut self) -> Self {
626 self.write = Allow::All;
627 self
628 }
629
630 #[must_use]
631 pub fn allow_all_net(mut self) -> Self {
632 self.net = Allow::All;
633 self
634 }
635
636 #[must_use]
637 pub fn allow_all_env(mut self) -> Self {
638 self.env = Allow::All;
639 self
640 }
641
642 #[must_use]
643 pub fn allow_all_sys(mut self) -> Self {
644 self.sys = Allow::All;
645 self
646 }
647
648 #[must_use]
649 pub fn deny_read<I, P>(mut self, roots: I) -> Self
650 where
651 I: IntoIterator<Item = P>,
652 P: AsRef<Path>,
653 {
654 self.deny.read.extend(roots.into_iter().map(PathRule::new));
655 self
656 }
657
658 #[must_use]
659 pub fn deny_write<I, P>(mut self, roots: I) -> Self
660 where
661 I: IntoIterator<Item = P>,
662 P: AsRef<Path>,
663 {
664 self.deny.write.extend(roots.into_iter().map(PathRule::new));
665 self
666 }
667
668 pub fn deny_net<I, S>(mut self, hosts: I) -> Result<Self, String>
672 where
673 I: IntoIterator<Item = S>,
674 S: AsRef<str>,
675 {
676 for host in hosts {
677 self.deny.net.push(NetRule::parse(host.as_ref())?);
678 }
679 Ok(self)
680 }
681
682 #[must_use]
683 pub fn deny_env<I, S>(mut self, names: I) -> Self
684 where
685 I: IntoIterator<Item = S>,
686 S: Into<String>,
687 {
688 self.deny.env.extend(names.into_iter().map(Into::into));
689 self
690 }
691
692 #[must_use]
693 pub fn deny_sys<I>(mut self, items: I) -> Self
694 where
695 I: IntoIterator<Item = SysInfo>,
696 {
697 self.deny.sys.extend(items);
698 self
699 }
700
701 #[must_use]
703 pub fn describe(&self, kind: Kind) -> String {
704 fn render<T: fmt::Display>(allow: &Allow<T>) -> String {
705 match allow {
706 Allow::None => "none".to_string(),
707 Allow::All => "all".to_string(),
708 Allow::Only(list) => list.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
709 }
710 }
711 match kind {
712 Kind::Read => render(&self.read.clone().map(|r| r.lexical.display().to_string())),
713 Kind::Write => render(&self.write.clone().map(|r| r.lexical.display().to_string())),
714 Kind::Net => render(&self.net),
715 Kind::Env => render(&self.env),
716 Kind::Sys => render(&self.sys),
717 }
718 }
719
720 #[must_use]
722 pub fn intersect(&self, other: &Self) -> Self {
723 let path_subsumes =
724 |a: &PathRule, b: &PathRule| b.lexical.starts_with(&a.lexical) && b.resolved.starts_with(&a.resolved);
725 Self {
726 read: self.read.intersect_with(&other.read, path_subsumes),
727 write: self.write.intersect_with(&other.write, path_subsumes),
728 net: self.net.intersect_with(&other.net, NetRule::subsumes),
729 env: self.env.intersect_with(&other.env, |a, b| a == b),
730 sys: self.sys.intersect_with(&other.sys, |a, b| a == b),
731 deny: self.deny.merged(&other.deny),
732 }
733 }
734
735 pub fn check_read(&self, path: &Path) -> Result<(), Denied> {
739 check_path(Kind::Read, &self.read, &self.deny.read, path)
740 }
741
742 pub fn check_write(&self, path: &Path) -> Result<(), Denied> {
746 check_path(Kind::Write, &self.write, &self.deny.write, path)
747 }
748
749 pub fn check_net(&self, host: &str, port: Option<u16>) -> Result<(), Denied> {
756 let host = host.to_ascii_lowercase();
757 let host = host.trim_start_matches('[').trim_end_matches(']');
758 let denied = self.deny.net.iter().any(|r| r.covers(host, port));
759 let granted = !denied
760 && match &self.net {
761 Allow::None => false,
762 Allow::All => true,
763 Allow::Only(rules) => rules.iter().any(|r| r.covers(host, port)),
764 };
765 if granted {
766 Ok(())
767 } else {
768 let resource = match port {
769 Some(p) if host.contains(':') => format!("[{host}]:{p}"),
770 Some(p) => format!("{host}:{p}"),
771 None => host.to_string(),
772 };
773 Err(Denied::new(Kind::Net, resource))
774 }
775 }
776
777 pub fn check_env(&self, name: &str) -> Result<(), Denied> {
781 let granted = !self.deny.env.iter().any(|n| n == name)
782 && match &self.env {
783 Allow::None => false,
784 Allow::All => true,
785 Allow::Only(names) => names.iter().any(|n| n == name),
786 };
787 if granted {
788 Ok(())
789 } else {
790 Err(Denied::new(Kind::Env, name))
791 }
792 }
793
794 pub fn check_sys(&self, item: SysInfo) -> Result<(), Denied> {
798 let granted = !self.deny.sys.contains(&item)
799 && match &self.sys {
800 Allow::None => false,
801 Allow::All => true,
802 Allow::Only(items) => items.contains(&item),
803 };
804 if granted {
805 Ok(())
806 } else {
807 Err(Denied::new(Kind::Sys, item.as_str()))
808 }
809 }
810
811 #[must_use]
814 pub fn env_snapshot(&self) -> Vec<(String, String)> {
815 let mut out: Vec<(String, String)> = match &self.env {
816 Allow::None => Vec::new(),
817 Allow::All => std::env::vars().collect(),
818 Allow::Only(names) => names
819 .iter()
820 .filter_map(|n| std::env::var(n).ok().map(|v| (n.clone(), v)))
821 .collect(),
822 };
823 out.retain(|(name, _)| !self.deny.env.contains(name));
824 out.sort();
825 out.dedup_by(|a, b| a.0 == b.0);
826 out
827 }
828}
829
830fn check_path(kind: Kind, allow: &Allow<PathRule>, deny: &[PathRule], path: &Path) -> Result<(), Denied> {
831 let granted = match allow {
832 Allow::None => false,
833 Allow::All if deny.is_empty() => true,
834 _ => {
835 let candidate = CheckedPath::new(path);
836 let denied = deny.iter().any(|r| r.covers(&candidate));
837 !denied
838 && match allow {
839 Allow::All => true,
840 Allow::Only(rules) => rules.iter().any(|r| r.covers(&candidate)),
841 Allow::None => false,
842 }
843 },
844 };
845 if granted {
846 Ok(())
847 } else {
848 Err(Denied::new(kind, path.display().to_string()))
849 }
850}
851
852#[derive(Debug, Clone, PartialEq, Eq)]
856pub struct Denied {
857 pub kind: Kind,
858 pub resource: String,
859}
860
861impl Denied {
862 #[must_use]
863 pub fn new(kind: Kind, resource: impl Into<String>) -> Self {
864 Self {
865 kind,
866 resource: resource.into(),
867 }
868 }
869
870 pub const CODE: &'static str = "ERR_ACCESS_DENIED";
872
873 pub const NAME: &'static str = "PermissionDeniedError";
875}
876
877impl fmt::Display for Denied {
878 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
879 let what = match self.kind {
880 Kind::Read => "read access to",
881 Kind::Write => "write access to",
882 Kind::Net => "network access to",
883 Kind::Env => "access to environment variable",
884 Kind::Sys => "access to host information",
885 };
886 write!(
887 f,
888 "permission denied: {what} \"{}\" (grant `{}` to allow it)",
889 self.resource, self.kind
890 )
891 }
892}
893
894impl std::error::Error for Denied {}
895
896#[derive(Debug, Clone, PartialEq, Eq)]
898pub struct Request<'a> {
899 pub kind: Kind,
900 pub resource: Cow<'a, str>,
901}
902
903#[derive(Debug, Clone, Copy, PartialEq, Eq)]
905pub enum Decision {
906 Allow,
907 Deny,
908}
909
910pub trait Hook: Send + Sync {
915 fn decide(&self, request: &Request<'_>) -> Decision;
916}
917
918impl<F> Hook for F
919where
920 F: Fn(&Request<'_>) -> Decision + Send + Sync,
921{
922 fn decide(&self, request: &Request<'_>) -> Decision {
923 self(request)
924 }
925}
926
927pub trait Audit: Send + Sync {
929 fn record(&self, request: &Request<'_>, granted: bool);
930}
931
932impl<F> Audit for F
933where
934 F: Fn(&Request<'_>, bool) + Send + Sync,
935{
936 fn record(&self, request: &Request<'_>, granted: bool) {
937 self(request, granted);
938 }
939}
940
941pub struct Container {
947 policy: std::sync::RwLock<Arc<Permissions>>,
948 hook: Option<Arc<dyn Hook>>,
949 audit: Option<Arc<dyn Audit>>,
950}
951
952impl fmt::Debug for Container {
953 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
954 f.debug_struct("Container")
955 .field("policy", &self.permissions())
956 .field("hook", &self.hook.as_ref().map(|_| "..."))
957 .field("audit", &self.audit.as_ref().map(|_| "..."))
958 .finish()
959 }
960}
961
962impl Container {
963 #[must_use]
964 pub fn new(policy: Permissions) -> Self {
965 Self {
966 policy: std::sync::RwLock::new(Arc::new(policy)),
967 hook: None,
968 audit: None,
969 }
970 }
971
972 #[must_use]
973 pub fn with_hook(mut self, hook: Arc<dyn Hook>) -> Self {
974 self.hook = Some(hook);
975 self
976 }
977
978 #[must_use]
979 pub fn with_audit(mut self, audit: Arc<dyn Audit>) -> Self {
980 self.audit = Some(audit);
981 self
982 }
983
984 #[must_use]
987 pub fn permissions(&self) -> Arc<Permissions> {
988 Arc::clone(&self.policy.read().unwrap_or_else(std::sync::PoisonError::into_inner))
989 }
990
991 pub fn revoke(&self, remaining: &Permissions) {
994 let mut guard = self.policy.write().unwrap_or_else(std::sync::PoisonError::into_inner);
995 *guard = Arc::new(guard.intersect(remaining));
996 }
997
998 pub fn deny(&self, kind: Kind, resource: &str) -> Result<(), String> {
1006 let mut guard = self.policy.write().unwrap_or_else(std::sync::PoisonError::into_inner);
1007 let mut next = (**guard).clone();
1008 match kind {
1009 Kind::Read => next.deny.read.push(PathRule::new(resource)),
1010 Kind::Write => next.deny.write.push(PathRule::new(resource)),
1011 Kind::Net => next.deny.net.push(NetRule::parse(resource)?),
1012 Kind::Env => next.deny.env.push(resource.to_string()),
1013 Kind::Sys => next.deny.sys.push(sys_info_from_str(resource)?),
1014 }
1015 *guard = Arc::new(next);
1016 Ok(())
1017 }
1018
1019 fn decide(&self, kind: Kind, resource: &str, statically: Result<(), Denied>) -> Result<(), Denied> {
1020 let request = Request {
1021 kind,
1022 resource: Cow::Borrowed(resource),
1023 };
1024 let outcome = match statically {
1025 Ok(()) => Ok(()),
1026 Err(denied) => match &self.hook {
1027 Some(hook) if hook.decide(&request) == Decision::Allow => Ok(()),
1028 _ => Err(denied),
1029 },
1030 };
1031 if let Some(audit) = &self.audit {
1032 audit.record(&request, outcome.is_ok());
1033 }
1034 outcome
1035 }
1036
1037 pub fn check_read(&self, path: &Path) -> Result<(), Denied> {
1041 let result = self.permissions().check_read(path);
1042 self.decide(Kind::Read, &path.to_string_lossy(), result)
1043 }
1044
1045 pub fn check_write(&self, path: &Path) -> Result<(), Denied> {
1049 let result = self.permissions().check_write(path);
1050 self.decide(Kind::Write, &path.to_string_lossy(), result)
1051 }
1052
1053 pub fn check_net(&self, host: &str, port: Option<u16>) -> Result<(), Denied> {
1057 let result = self.permissions().check_net(host, port);
1058 let resource = match port {
1059 Some(p) => format!("{host}:{p}"),
1060 None => host.to_string(),
1061 };
1062 self.decide(Kind::Net, &resource, result)
1063 }
1064
1065 pub fn check_env(&self, name: &str) -> Result<(), Denied> {
1069 let result = self.permissions().check_env(name);
1070 self.decide(Kind::Env, name, result)
1071 }
1072
1073 pub fn check_sys(&self, item: SysInfo) -> Result<(), Denied> {
1077 let result = self.permissions().check_sys(item);
1078 self.decide(Kind::Sys, item.as_str(), result)
1079 }
1080
1081 pub fn has(&self, kind: Kind, resource: Option<&str>) -> Result<bool, String> {
1089 let policy = self.permissions();
1090 Ok(match (kind, resource) {
1091 (Kind::Read, None) => policy.read.is_all() && policy.deny.read.is_empty(),
1092 (Kind::Write, None) => policy.write.is_all() && policy.deny.write.is_empty(),
1093 (Kind::Net, None) => policy.net.is_all() && policy.deny.net.is_empty(),
1094 (Kind::Env, None) => policy.env.is_all() && policy.deny.env.is_empty(),
1095 (Kind::Sys, None) => policy.sys.is_all() && policy.deny.sys.is_empty(),
1096 (Kind::Read, Some(r)) => policy.check_read(Path::new(r)).is_ok(),
1097 (Kind::Write, Some(r)) => policy.check_write(Path::new(r)).is_ok(),
1098 (Kind::Net, Some(r)) => {
1099 let rule = NetRule::parse(r)?;
1100 policy.check_net(rule.host(), rule.port()).is_ok()
1101 },
1102 (Kind::Env, Some(r)) => policy.check_env(r).is_ok(),
1103 (Kind::Sys, Some(r)) => policy.check_sys(sys_info_from_str(r)?).is_ok(),
1104 })
1105 }
1106}
1107
1108fn sys_info_from_str(name: &str) -> Result<SysInfo, String> {
1109 [
1110 SysInfo::Hostname,
1111 SysInfo::OsRelease,
1112 SysInfo::OsUptime,
1113 SysInfo::LoadAvg,
1114 SysInfo::NetworkInterfaces,
1115 SysInfo::SystemMemory,
1116 SysInfo::Uid,
1117 SysInfo::Gid,
1118 SysInfo::Username,
1119 SysInfo::Cpus,
1120 SysInfo::HomeDir,
1121 SysInfo::Priority,
1122 ]
1123 .into_iter()
1124 .find(|item| item.as_str() == name)
1125 .ok_or_else(|| format!("`{name}` is not a sys permission"))
1126}
1127
1128impl std::str::FromStr for Kind {
1129 type Err = String;
1130
1131 fn from_str(s: &str) -> Result<Self, Self::Err> {
1132 match s {
1133 "read" => Ok(Self::Read),
1134 "write" => Ok(Self::Write),
1135 "net" => Ok(Self::Net),
1136 "env" => Ok(Self::Env),
1137 "sys" => Ok(Self::Sys),
1138 other => Err(format!(
1139 "`{other}` is not a permission kind (read, write, net, env, sys)"
1140 )),
1141 }
1142 }
1143}
1144
1145impl std::str::FromStr for SysInfo {
1146 type Err = String;
1147
1148 fn from_str(s: &str) -> Result<Self, Self::Err> {
1149 sys_info_from_str(s)
1150 }
1151}
1152
1153#[must_use]
1157pub fn is_metadata_ip(ip: IpAddr) -> bool {
1158 match canon_ip(ip) {
1159 IpAddr::V4(v4) => v4 == std::net::Ipv4Addr::new(169, 254, 169, 254),
1160 IpAddr::V6(v6) => v6 == std::net::Ipv6Addr::new(0xfd00, 0x0ec2, 0, 0, 0, 0, 0, 0x0254),
1161 }
1162}
1163
1164#[must_use]
1168pub fn is_private_ip(ip: IpAddr) -> bool {
1169 match canon_ip(ip) {
1170 IpAddr::V4(v4) => {
1171 v4.is_loopback()
1172 || v4.is_private()
1173 || v4.is_link_local()
1174 || v4.is_unspecified()
1175 || v4.is_broadcast()
1176 || v4.octets()[0] == 0
1177 || (v4.octets()[0] == 100 && (64..=127).contains(&v4.octets()[1]))
1178 },
1179 IpAddr::V6(v6) => {
1180 v6.is_loopback()
1181 || v6.is_unspecified()
1182 || (v6.segments()[0] & 0xfe00) == 0xfc00
1183 || (v6.segments()[0] & 0xffc0) == 0xfe80
1184 },
1185 }
1186}
1187
1188fn canon_ip(ip: IpAddr) -> IpAddr {
1191 match ip {
1192 IpAddr::V6(v6) => v6.to_ipv4_mapped().map_or(IpAddr::V6(v6), IpAddr::V4),
1193 v4 @ IpAddr::V4(_) => v4,
1194 }
1195}
1196
1197#[cfg(test)]
1198mod tests {
1199 use super::*;
1200
1201 #[test]
1202 fn default_denies_everything() {
1203 let p = Permissions::none();
1204 assert!(p.check_read(Path::new("/etc/hosts")).is_err());
1205 assert!(p.check_write(Path::new("/tmp/x")).is_err());
1206 assert!(p.check_net("example.com", Some(443)).is_err());
1207 assert!(p.check_env("HOME").is_err());
1208 assert!(p.check_sys(SysInfo::Hostname).is_err());
1209 }
1210
1211 #[test]
1212 fn all_grants_everything() {
1213 let p = Permissions::all();
1214 assert!(p.check_read(Path::new("/etc/hosts")).is_ok());
1215 assert!(p.check_net("example.com", None).is_ok());
1216 assert!(p.check_env("HOME").is_ok());
1217 assert!(p.check_sys(SysInfo::Uid).is_ok());
1218 }
1219
1220 #[test]
1221 fn read_is_scoped_to_the_granted_root() {
1222 let dir = tempfile::tempdir().unwrap();
1223 let root = dir.path().join("data");
1224 std::fs::create_dir_all(&root).unwrap();
1225 let p = Permissions::none().allow_read([&root]);
1226 assert!(p.check_read(&root.join("a.txt")).is_ok());
1227 assert!(p.check_read(&root.join("sub/deep/b.txt")).is_ok());
1228 assert!(p.check_read(&root.join("../escape.txt")).is_err());
1229 assert!(p.check_read(dir.path()).is_err());
1230 assert!(p.check_read(&dir.path().join("data-other/x")).is_err());
1232 }
1233
1234 #[test]
1235 fn a_relative_path_resolves_against_cwd() {
1236 let cwd = std::env::current_dir().unwrap();
1237 let p = Permissions::none().allow_read([&cwd]);
1238 assert!(p.check_read(Path::new("Cargo.toml")).is_ok());
1239 assert!(p.check_read(Path::new("../../../../../../etc/passwd")).is_err());
1240 }
1241
1242 #[cfg(unix)]
1243 #[test]
1244 fn a_symlink_inside_the_root_cannot_point_out_of_it() {
1245 let dir = tempfile::tempdir().unwrap();
1246 let root = dir.path().join("root");
1247 let outside = dir.path().join("outside");
1248 std::fs::create_dir_all(&root).unwrap();
1249 std::fs::create_dir_all(&outside).unwrap();
1250 std::fs::write(outside.join("secret"), b"x").unwrap();
1251 std::os::unix::fs::symlink(&outside, root.join("link")).unwrap();
1252 let p = Permissions::none().allow_read([&root]);
1253 assert!(p.check_read(&root.join("link/secret")).is_err());
1255 assert!(p.check_read(&root.join("link/new-file")).is_err());
1257 assert!(p.check_read(&root.join("plain")).is_ok());
1258 }
1259
1260 #[cfg(unix)]
1261 #[test]
1262 fn a_root_that_is_itself_a_symlink_grants_its_target() {
1263 let dir = tempfile::tempdir().unwrap();
1264 let real = dir.path().join("real");
1265 std::fs::create_dir_all(&real).unwrap();
1266 let link = dir.path().join("link");
1267 std::os::unix::fs::symlink(&real, &link).unwrap();
1268 let p = Permissions::none().allow_read([&link]);
1269 assert!(p.check_read(&link.join("f")).is_ok());
1270 }
1271
1272 #[test]
1273 fn net_rules_match_host_wildcard_and_port() {
1274 let p = Permissions::none()
1275 .allow_net(["api.acme.com", "*.cdn.com", "127.0.0.1:8080", "[::1]:9"])
1276 .unwrap();
1277 assert!(p.check_net("api.acme.com", Some(443)).is_ok());
1278 assert!(p.check_net("API.ACME.COM", None).is_ok());
1279 assert!(p.check_net("cdn.com", Some(80)).is_ok());
1280 assert!(p.check_net("a.b.cdn.com", Some(80)).is_ok());
1281 assert!(p.check_net("evilcdn.com", Some(80)).is_err());
1282 assert!(p.check_net("acme.com", Some(443)).is_err());
1283 assert!(p.check_net("127.0.0.1", Some(8080)).is_ok());
1284 assert!(p.check_net("127.0.0.1", Some(8081)).is_err());
1285 assert!(p.check_net("::1", Some(9)).is_ok());
1286 assert!(p.check_net("[::1]", Some(9)).is_ok());
1287 assert!(p.check_net("::1", Some(10)).is_err());
1288 }
1289
1290 #[test]
1291 fn net_rule_parse_rejects_garbage() {
1292 assert!(NetRule::parse("").is_err());
1293 assert!(NetRule::parse("host:notaport").is_err());
1294 assert!(NetRule::parse("*.").is_err());
1295 assert!(NetRule::parse("[::1").is_err());
1296 assert_eq!(NetRule::parse("[::1]:80").unwrap().to_string(), "[::1]:80");
1297 assert_eq!(NetRule::parse("Example.COM").unwrap().to_string(), "example.com");
1298 }
1299
1300 #[test]
1301 fn intersect_never_widens() {
1302 let base = Permissions::none()
1303 .allow_net(["*.acme.com", "cdn.com:443"])
1304 .unwrap()
1305 .allow_env(["HOME", "USER"])
1306 .allow_all_read();
1307 let declared = Permissions::none()
1308 .allow_net(["api.acme.com", "evil.com", "cdn.com"])
1309 .unwrap()
1310 .allow_env(["USER", "SECRET"])
1311 .allow_read(["/srv"]);
1312 let narrowed = base.intersect(&declared);
1313 assert!(narrowed.check_net("api.acme.com", Some(443)).is_ok());
1314 assert!(narrowed.check_net("evil.com", Some(443)).is_err());
1315 assert!(narrowed.check_net("cdn.com", Some(443)).is_ok());
1317 assert!(narrowed.check_net("cdn.com", Some(80)).is_err());
1318 assert!(narrowed.check_env("USER").is_ok());
1319 assert!(narrowed.check_env("HOME").is_err());
1320 assert!(narrowed.check_env("SECRET").is_err());
1321 assert!(narrowed.check_read(Path::new("/srv/x")).is_ok());
1322 assert!(narrowed.check_read(Path::new("/etc/x")).is_err());
1323 assert!(Permissions::all().intersect(&Permissions::all()).write.is_all());
1325 assert!(Permissions::all().intersect(&Permissions::none()).write.is_none());
1326 }
1327
1328 #[test]
1329 fn deny_overrides_allow() {
1330 let p = Permissions::all()
1331 .deny_read(["/etc"])
1332 .deny_net(["*.internal"])
1333 .unwrap()
1334 .deny_env(["SECRET"])
1335 .deny_sys([SysInfo::Username]);
1336 assert!(p.check_read(Path::new("/etc/passwd")).is_err());
1337 assert!(p.check_read(Path::new("/var/log")).is_ok());
1338 assert!(p.check_net("db.internal", Some(5432)).is_err());
1339 assert!(p.check_net("example.com", Some(443)).is_ok());
1340 assert!(p.check_env("SECRET").is_err());
1341 assert!(p.check_env("HOME").is_ok());
1342 assert!(p.check_sys(SysInfo::Username).is_err());
1343 assert!(p.check_sys(SysInfo::Hostname).is_ok());
1344 assert!(!p.env_snapshot().iter().any(|(k, _)| k == "SECRET"));
1345 }
1346
1347 #[test]
1348 fn a_container_only_narrows() {
1349 let c = Container::new(Permissions::all());
1350 assert!(c.check_env("ANY").is_ok());
1351 assert_eq!(c.has(Kind::Env, None), Ok(true));
1352 c.revoke(&Permissions::all().allow_env(["ONLY"]));
1353 assert!(c.check_env("ONLY").is_ok());
1354 assert!(c.check_env("ANY").is_err());
1355 assert_eq!(c.has(Kind::Env, None), Ok(false));
1356 assert_eq!(c.has(Kind::Env, Some("ONLY")), Ok(true));
1357 c.revoke(&Permissions::all());
1359 assert!(c.check_env("ANY").is_err());
1360 c.deny(Kind::Env, "ONLY").unwrap();
1361 assert!(c.check_env("ONLY").is_err());
1362 c.deny(Kind::Net, "*.internal").unwrap();
1363 assert!(c.check_net("x.internal", Some(80)).is_err());
1364 assert!(c.check_net("example.com", Some(80)).is_ok());
1365 assert_eq!(c.has(Kind::Net, Some("example.com:80")), Ok(true));
1366 assert!(c.deny(Kind::Sys, "nope").is_err());
1367 }
1368
1369 #[test]
1370 fn hook_can_grant_and_audit_sees_both() {
1371 use std::sync::atomic::{AtomicUsize, Ordering};
1372 let seen = Arc::new(AtomicUsize::new(0));
1373 let seen2 = Arc::clone(&seen);
1374 let c = Container::new(Permissions::none())
1375 .with_hook(Arc::new(|req: &Request<'_>| {
1376 if req.kind == Kind::Env && req.resource == "PROMPTED" {
1377 Decision::Allow
1378 } else {
1379 Decision::Deny
1380 }
1381 }))
1382 .with_audit(Arc::new(move |_req: &Request<'_>, _granted: bool| {
1383 seen2.fetch_add(1, Ordering::Relaxed);
1384 }));
1385 assert!(c.check_env("PROMPTED").is_ok());
1386 assert!(c.check_env("OTHER").is_err());
1387 assert_eq!(seen.load(Ordering::Relaxed), 2);
1388 }
1389
1390 #[test]
1391 fn denied_renders_kind_and_resource() {
1392 let d = Denied::new(Kind::Net, "evil.com:443");
1393 assert_eq!(
1394 d.to_string(),
1395 "permission denied: network access to \"evil.com:443\" (grant `net` to allow it)"
1396 );
1397 }
1398
1399 #[test]
1400 fn metadata_and_private_ranges() {
1401 assert!(is_metadata_ip("169.254.169.254".parse().unwrap()));
1402 assert!(is_metadata_ip("::ffff:169.254.169.254".parse().unwrap()));
1403 assert!(is_metadata_ip("fd00:ec2::254".parse().unwrap()));
1404 assert!(!is_metadata_ip("93.184.216.34".parse().unwrap()));
1405 for ip in [
1406 "127.0.0.1",
1407 "10.0.0.1",
1408 "192.168.1.1",
1409 "172.16.0.1",
1410 "100.64.0.1",
1411 "::1",
1412 "fe80::1",
1413 "fc00::1",
1414 ] {
1415 assert!(is_private_ip(ip.parse().unwrap()), "{ip}");
1416 }
1417 assert!(!is_private_ip("8.8.8.8".parse().unwrap()));
1418 }
1419
1420 #[test]
1421 fn env_snapshot_filters_to_the_grant() {
1422 let p = Permissions::none().allow_env(["PATH", "FERRIJS_SURELY_UNSET_VAR"]);
1423 let snap = p.env_snapshot();
1424 assert!(snap.iter().any(|(k, _)| k == "PATH"));
1425 assert!(!snap.iter().any(|(k, _)| k == "FERRIJS_SURELY_UNSET_VAR"));
1426 assert!(Permissions::none().env_snapshot().is_empty());
1427 }
1428
1429 #[cfg(feature = "serde")]
1430 #[test]
1431 fn serde_shape_is_bool_or_list() {
1432 let doc = r#"{"read": true, "write": ["/srv/out"], "net": ["*.acme.com:443"], "env": false, "sys": ["hostname"], "deny": {"read": ["/etc"]}}"#;
1433 let p: Permissions = serde_json::from_str(doc).unwrap();
1434 assert!(p.read.is_all());
1435 assert_eq!(p.write.entries().len(), 1);
1436 assert!(p.env.is_none());
1437 assert_eq!(p.sys.entries(), &[SysInfo::Hostname]);
1438 assert_eq!(p.deny.read.len(), 1);
1439 assert!(p.check_read(Path::new("/etc/hosts")).is_err());
1440 let back = serde_json::to_value(&p).unwrap();
1441 assert_eq!(back["read"], serde_json::Value::Bool(true));
1442 assert_eq!(back["net"][0], "*.acme.com:443");
1443 assert_eq!(back["deny"]["read"][0], "/etc");
1444 let plain: Permissions = serde_json::from_str(r#"{"read": true}"#).unwrap();
1445 assert!(serde_json::to_value(&plain).unwrap().get("deny").is_none());
1446 }
1447}