use std::collections::HashSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum Capability {
FileRead = 0,
FileWrite = 1,
Execute = 2,
Network = 3,
Environment = 4,
Settings = 5,
Secrets = 6,
Process = 7,
Ipc = 8,
System = 255,
}
impl Capability {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"file-read" | "fs:read" => Some(Self::FileRead),
"file-write" | "fs:write" => Some(Self::FileWrite),
"execute" | "exec" => Some(Self::Execute),
"network" | "net" => Some(Self::Network),
"environment" | "env" => Some(Self::Environment),
"settings" | "config" => Some(Self::Settings),
"secrets" | "credentials" => Some(Self::Secrets),
"process" | "spawn" => Some(Self::Process),
"ipc" => Some(Self::Ipc),
"system" | "all" => Some(Self::System),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::FileRead => "fs:read",
Self::FileWrite => "fs:write",
Self::Execute => "execute",
Self::Network => "network",
Self::Environment => "environment",
Self::Settings => "settings",
Self::Secrets => "secrets",
Self::Process => "process",
Self::Ipc => "ipc",
Self::System => "system",
}
}
pub fn implies(&self, other: &Capability) -> bool {
if *self == Self::System {
return true;
}
if *self == Self::FileWrite && *other == Self::FileRead {
return true;
}
if *self == Self::Process && *other == Self::Execute {
return true;
}
*self == *other
}
}
#[derive(Debug, Clone, Default)]
pub struct CapabilityManifest {
required: HashSet<Capability>,
optional: HashSet<Capability>,
denied: HashSet<Capability>,
restrictions: Vec<CapabilityRestriction>,
}
#[derive(Debug, Clone)]
pub struct CapabilityRestriction {
pub capability: Capability,
pub restriction: RestrictionType,
pub allowed: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RestrictionType {
Path,
Domain,
Command,
EnvVar,
}
impl CapabilityManifest {
pub fn new() -> Self {
Self::default()
}
pub fn safe() -> Self {
let mut manifest = Self::new();
manifest.require(Capability::FileRead);
manifest
}
pub fn unrestricted() -> Self {
let mut manifest = Self::new();
manifest.require(Capability::System);
manifest
}
pub fn require(&mut self, cap: Capability) {
self.required.insert(cap);
self.optional.remove(&cap);
self.denied.remove(&cap);
}
pub fn request(&mut self, cap: Capability) {
if !self.required.contains(&cap) && !self.denied.contains(&cap) {
self.optional.insert(cap);
}
}
pub fn deny(&mut self, cap: Capability) {
self.required.remove(&cap);
self.optional.remove(&cap);
self.denied.insert(cap);
}
pub fn restrict(&mut self, restriction: CapabilityRestriction) {
self.restrictions.push(restriction);
}
pub fn requires(&self, cap: Capability) -> bool {
self.required.iter().any(|r| r.implies(&cap))
}
pub fn allows(&self, cap: Capability) -> bool {
if self.denied.iter().any(|d| d.implies(&cap)) {
return false;
}
self.required.iter().any(|r| r.implies(&cap))
|| self.optional.iter().any(|o| o.implies(&cap))
}
pub fn denies(&self, cap: Capability) -> bool {
self.denied.iter().any(|d| d.implies(&cap))
}
pub fn required_capabilities(&self) -> &HashSet<Capability> {
&self.required
}
pub fn optional_capabilities(&self) -> &HashSet<Capability> {
&self.optional
}
pub fn restrictions_for(
&self,
cap: Capability,
) -> impl Iterator<Item = &CapabilityRestriction> {
self.restrictions
.iter()
.filter(move |r| r.capability == cap)
}
pub fn is_path_allowed(&self, path: &str, cap: Capability) -> bool {
if !self.allows(cap) {
return false;
}
let restrictions: Vec<_> = self
.restrictions_for(cap)
.filter(|r| r.restriction == RestrictionType::Path)
.collect();
if restrictions.is_empty() {
return true;
}
restrictions.iter().any(|r| {
r.allowed.iter().any(|allowed| {
path.starts_with(allowed)
|| globset::Glob::new(allowed)
.ok()
.and_then(|g| g.compile_matcher().is_match(path).then_some(()))
.is_some()
})
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut output = Vec::new();
output.push(self.required.len() as u8);
for cap in &self.required {
output.push(*cap as u8);
}
output.push(self.optional.len() as u8);
for cap in &self.optional {
output.push(*cap as u8);
}
output.push(self.denied.len() as u8);
for cap in &self.denied {
output.push(*cap as u8);
}
output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capability_implies() {
assert!(Capability::System.implies(&Capability::FileRead));
assert!(Capability::FileWrite.implies(&Capability::FileRead));
assert!(!Capability::FileRead.implies(&Capability::FileWrite));
}
#[test]
fn test_manifest_require() {
let mut manifest = CapabilityManifest::new();
manifest.require(Capability::FileRead);
assert!(manifest.requires(Capability::FileRead));
assert!(manifest.allows(Capability::FileRead));
assert!(!manifest.denies(Capability::FileRead));
}
#[test]
fn test_manifest_deny() {
let mut manifest = CapabilityManifest::new();
manifest.deny(Capability::Network);
assert!(manifest.denies(Capability::Network));
assert!(!manifest.allows(Capability::Network));
}
#[test]
fn test_path_restrictions() {
let mut manifest = CapabilityManifest::new();
manifest.require(Capability::FileRead);
manifest.restrict(CapabilityRestriction {
capability: Capability::FileRead,
restriction: RestrictionType::Path,
allowed: vec!["/home/user/project".to_string()],
});
assert!(manifest.is_path_allowed("/home/user/project/src/main.rs", Capability::FileRead));
assert!(!manifest.is_path_allowed("/etc/passwd", Capability::FileRead));
}
}