use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Rights {
Read,
Write,
Execute,
Grant,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResourceRef {
KernelDomain {
domain: String,
},
Path {
pattern: String,
},
Network {
pattern: String,
},
Named {
name: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityEntry {
pub resource: ResourceRef,
pub rights: Vec<Rights>,
#[serde(skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl CapabilityEntry {
pub fn new(resource: ResourceRef, rights: Vec<Rights>) -> Self {
Self {
resource,
rights,
label: None,
}
}
pub fn has_right(&self, right: Rights) -> bool {
self.rights.contains(&right)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CSpace {
pub agent_id: Uuid,
#[serde(default)]
pub name: String,
entries: HashMap<u32, CapabilityEntry>,
next_index: u32,
}
impl CSpace {
pub fn new(agent_id: Uuid) -> Self {
Self {
agent_id,
name: String::new(),
entries: HashMap::new(),
next_index: 1,
}
}
pub fn with_name(agent_id: Uuid, name: &str) -> Self {
Self {
agent_id,
name: name.to_string(),
entries: HashMap::new(),
next_index: 1,
}
}
pub fn insert(&mut self, entry: CapabilityEntry) -> u32 {
let idx = self.next_index;
self.next_index += 1;
self.entries.insert(idx, entry);
idx
}
pub fn remove(&mut self, index: u32) -> Option<CapabilityEntry> {
self.entries.remove(&index)
}
pub fn can(&self, resource: &ResourceRef, right: Rights) -> bool {
self.entries.values().any(|entry| {
entry.has_right(right) && resource_matches(&entry.resource, resource)
})
}
pub fn iter(&self) -> impl Iterator<Item = (&u32, &CapabilityEntry)> {
self.entries.iter()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
fn resource_matches(granted: &ResourceRef, required: &ResourceRef) -> bool {
match (granted, required) {
_ if granted == required => true,
(
ResourceRef::KernelDomain { domain: g },
ResourceRef::KernelDomain { domain: _r },
) => g == "*",
(ResourceRef::Path { pattern: g }, ResourceRef::Path { pattern: _r }) => {
if g == "*" || g == "/**" {
return true;
}
if let Some(prefix) = g.strip_suffix("/**") {
return _r.starts_with(prefix)
|| _r.starts_with(&format!("{}/", prefix.trim_end_matches('/')));
}
false
}
(ResourceRef::Network { pattern: g }, ResourceRef::Network { pattern: _r }) => g == "*",
(ResourceRef::Named { name: g }, ResourceRef::Named { name: _r }) => g == "*",
_ => false,
}
}
#[derive(Debug)]
pub struct CSpaceBuilder {
agent_id: Uuid,
name: String,
entries: Vec<CapabilityEntry>,
}
impl CSpaceBuilder {
pub fn new(agent_id: Uuid) -> Self {
Self {
agent_id,
name: String::new(),
entries: Vec::new(),
}
}
pub fn name(mut self, name: &str) -> Self {
self.name = name.to_string();
self
}
pub fn grant(mut self, resource: ResourceRef, rights: Vec<Rights>) -> Self {
self.entries.push(CapabilityEntry::new(resource, rights));
self
}
pub fn all_access(self) -> Self {
self.grant(
ResourceRef::KernelDomain { domain: "*".into() },
vec![Rights::Read, Rights::Write, Rights::Execute, Rights::Grant],
)
.grant(
ResourceRef::Path { pattern: "/**".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
)
.grant(
ResourceRef::Network { pattern: "*".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
)
}
pub fn standard(self) -> Self {
self.grant(
ResourceRef::KernelDomain { domain: "read".into() },
vec![Rights::Read, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "write".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "edit".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "bash".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "grep".into() },
vec![Rights::Read, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "find".into() },
vec![Rights::Read, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "ls".into() },
vec![Rights::Read, Rights::Execute],
)
.grant(
ResourceRef::KernelDomain { domain: "memory".into() },
vec![Rights::Read, Rights::Write],
)
.grant(
ResourceRef::Path { pattern: "/workspace/**".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
)
}
pub fn worker(self) -> Self {
self.standard()
.grant(
ResourceRef::KernelDomain { domain: "subagent".into() },
vec![Rights::Execute],
)
.grant(
ResourceRef::Network { pattern: "*".into() },
vec![Rights::Read, Rights::Write],
)
}
pub fn build(self) -> CSpace {
let mut cspace = CSpace::with_name(self.agent_id, &self.name);
for entry in self.entries {
cspace.insert(entry);
}
cspace
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cspace_insert_and_check() {
let id = Uuid::new_v4();
let mut cs = CSpace::new(id);
cs.insert(CapabilityEntry::new(
ResourceRef::KernelDomain { domain: "read".into() },
vec![Rights::Read, Rights::Execute],
));
assert!(cs.can(
&ResourceRef::KernelDomain { domain: "read".into() },
Rights::Execute
));
assert!(!cs.can(
&ResourceRef::KernelDomain { domain: "write".into() },
Rights::Execute
));
}
#[test]
fn test_cspace_wildcard() {
let id = Uuid::new_v4();
let mut cs = CSpace::new(id);
cs.insert(CapabilityEntry::new(
ResourceRef::KernelDomain { domain: "*".into() },
vec![Rights::Read, Rights::Write, Rights::Execute],
));
assert!(cs.can(
&ResourceRef::KernelDomain { domain: "anything".into() },
Rights::Execute
));
}
#[test]
fn test_cspace_path_glob() {
let id = Uuid::new_v4();
let mut cs = CSpace::new(id);
cs.insert(CapabilityEntry::new(
ResourceRef::Path { pattern: "/workspace/**".into() },
vec![Rights::Read, Rights::Write],
));
assert!(cs.can(
&ResourceRef::Path { pattern: "/workspace/src/main.rs".into() },
Rights::Read
));
assert!(!cs.can(
&ResourceRef::Path { pattern: "/etc/passwd".into() },
Rights::Read
));
}
#[test]
fn test_builder_standard() {
let id = Uuid::new_v4();
let cs = CSpaceBuilder::new(id).standard().build();
assert!(cs.can(
&ResourceRef::KernelDomain { domain: "read".into() },
Rights::Execute
));
assert!(cs.can(
&ResourceRef::KernelDomain { domain: "memory".into() },
Rights::Read
));
}
#[test]
fn test_builder_all_access() {
let id = Uuid::new_v4();
let cs = CSpaceBuilder::new(id).all_access().build();
assert!(cs.can(
&ResourceRef::KernelDomain { domain: "anything".into() },
Rights::Grant
));
assert!(cs.can(
&ResourceRef::Path { pattern: "/any/path".into() },
Rights::Write
));
}
#[test]
fn test_builder_worker() {
let id = Uuid::new_v4();
let cs = CSpaceBuilder::new(id).worker().build();
assert!(cs.can(
&ResourceRef::KernelDomain { domain: "subagent".into() },
Rights::Execute
));
assert!(cs.can(
&ResourceRef::Network { pattern: "example.com".into() },
Rights::Read
));
}
#[test]
fn test_remove_capability() {
let id = Uuid::new_v4();
let mut cs = CSpace::new(id);
let idx = cs.insert(CapabilityEntry::new(
ResourceRef::KernelDomain { domain: "read".into() },
vec![Rights::Read],
));
assert!(cs.remove(idx).is_some());
assert!(!cs.can(
&ResourceRef::KernelDomain { domain: "read".into() },
Rights::Read
));
}
}