glyph_runtime/
capability.rs

1//! Capability-based security system for Glyph runtime
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::fmt;
6
7/// Represents a capability that can be granted to code
8#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
9pub enum Capability {
10    // Audio capabilities
11    AudioSpeak,
12    AudioPlay,
13    AudioRecord,
14
15    // Display capabilities
16    DisplayChart,
17    DisplayText,
18    DisplayImage,
19    DisplayVideo,
20
21    // Network capabilities
22    NetworkFetch(String),     // URL pattern
23    NetworkWebSocket(String), // URL pattern
24
25    // File system capabilities (future)
26    FileRead(String),  // Path pattern
27    FileWrite(String), // Path pattern
28
29    // System capabilities
30    SystemTime,
31    SystemRandom,
32    SystemEnvironment(String), // Environment variable name
33
34    // Compute capabilities
35    ComputeUnlimited,
36    ComputeLimited(u64), // Max operations
37
38    // Memory capabilities
39    MemoryUnlimited,
40    MemoryLimited(usize), // Max bytes
41}
42
43impl fmt::Display for Capability {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Capability::AudioSpeak => write!(f, "audio.speak"),
47            Capability::AudioPlay => write!(f, "audio.play"),
48            Capability::AudioRecord => write!(f, "audio.record"),
49            Capability::DisplayChart => write!(f, "display.chart"),
50            Capability::DisplayText => write!(f, "display.text"),
51            Capability::DisplayImage => write!(f, "display.image"),
52            Capability::DisplayVideo => write!(f, "display.video"),
53            Capability::NetworkFetch(pattern) => write!(f, "network.fetch[{pattern}]"),
54            Capability::NetworkWebSocket(pattern) => write!(f, "network.websocket[{pattern}]"),
55            Capability::FileRead(pattern) => write!(f, "file.read[{pattern}]"),
56            Capability::FileWrite(pattern) => write!(f, "file.write[{pattern}]"),
57            Capability::SystemTime => write!(f, "system.time"),
58            Capability::SystemRandom => write!(f, "system.random"),
59            Capability::SystemEnvironment(var) => write!(f, "system.env[{var}]"),
60            Capability::ComputeUnlimited => write!(f, "compute.unlimited"),
61            Capability::ComputeLimited(ops) => write!(f, "compute.limited[{ops}]"),
62            Capability::MemoryUnlimited => write!(f, "memory.unlimited"),
63            Capability::MemoryLimited(bytes) => write!(f, "memory.limited[{bytes}]"),
64        }
65    }
66}
67
68/// A set of capabilities granted to a piece of code
69#[derive(Debug, Clone, Default)]
70pub struct CapabilitySet {
71    capabilities: HashSet<Capability>,
72}
73
74impl CapabilitySet {
75    /// Create a new empty capability set
76    pub fn new() -> Self {
77        CapabilitySet {
78            capabilities: HashSet::new(),
79        }
80    }
81
82    /// Create a capability set with the given capabilities
83    pub fn from_capabilities<I>(caps: I) -> Self
84    where
85        I: IntoIterator<Item = Capability>,
86    {
87        CapabilitySet {
88            capabilities: caps.into_iter().collect(),
89        }
90    }
91
92    /// Add a capability to the set
93    pub fn grant(&mut self, cap: Capability) {
94        self.capabilities.insert(cap);
95    }
96
97    /// Remove a capability from the set
98    pub fn revoke(&mut self, cap: &Capability) -> bool {
99        self.capabilities.remove(cap)
100    }
101
102    /// Check if a capability is granted
103    pub fn has(&self, cap: &Capability) -> bool {
104        self.capabilities.contains(cap)
105    }
106
107    /// Check if a capability is granted by name
108    pub fn has_by_name(&self, name: &str) -> bool {
109        self.capabilities.iter().any(|cap| {
110            cap.to_string() == name || cap.to_string().starts_with(&format!("{name}["))
111        })
112    }
113
114    /// Check if a capability is granted, with pattern matching for parameterized capabilities
115    pub fn has_matching(&self, requested: &Capability) -> bool {
116        match requested {
117            Capability::NetworkFetch(url) => self.capabilities.iter().any(|cap| match cap {
118                Capability::NetworkFetch(pattern) => url_matches_pattern(url, pattern),
119                _ => false,
120            }),
121            Capability::NetworkWebSocket(url) => self.capabilities.iter().any(|cap| match cap {
122                Capability::NetworkWebSocket(pattern) => url_matches_pattern(url, pattern),
123                _ => false,
124            }),
125            Capability::FileRead(path) => self.capabilities.iter().any(|cap| match cap {
126                Capability::FileRead(pattern) => path_matches_pattern(path, pattern),
127                _ => false,
128            }),
129            Capability::FileWrite(path) => self.capabilities.iter().any(|cap| match cap {
130                Capability::FileWrite(pattern) => path_matches_pattern(path, pattern),
131                _ => false,
132            }),
133            _ => self.has(requested),
134        }
135    }
136
137    /// Create an intersection of two capability sets
138    pub fn intersection(&self, other: &CapabilitySet) -> CapabilitySet {
139        CapabilitySet {
140            capabilities: self
141                .capabilities
142                .intersection(&other.capabilities)
143                .cloned()
144                .collect(),
145        }
146    }
147
148    /// Create a union of two capability sets
149    pub fn union(&self, other: &CapabilitySet) -> CapabilitySet {
150        CapabilitySet {
151            capabilities: self
152                .capabilities
153                .union(&other.capabilities)
154                .cloned()
155                .collect(),
156        }
157    }
158
159    /// Check if this set is a subset of another
160    pub fn is_subset(&self, other: &CapabilitySet) -> bool {
161        self.capabilities.is_subset(&other.capabilities)
162    }
163
164    /// Get an iterator over the capabilities
165    pub fn iter(&self) -> impl Iterator<Item = &Capability> {
166        self.capabilities.iter()
167    }
168
169    /// Get the number of capabilities
170    pub fn len(&self) -> usize {
171        self.capabilities.len()
172    }
173
174    /// Check if the set is empty
175    pub fn is_empty(&self) -> bool {
176        self.capabilities.is_empty()
177    }
178}
179
180/// Simple pattern matching for URLs (supports * wildcard)
181fn url_matches_pattern(url: &str, pattern: &str) -> bool {
182    if pattern == "*" {
183        return true;
184    }
185
186    if let Some(prefix) = pattern.strip_suffix("*") {
187        url.starts_with(prefix)
188    } else {
189        url == pattern
190    }
191}
192
193/// Simple pattern matching for paths (supports * wildcard)
194fn path_matches_pattern(path: &str, pattern: &str) -> bool {
195    if pattern == "*" {
196        return true;
197    }
198
199    if let Some(prefix) = pattern.strip_suffix("/*") {
200        path.starts_with(prefix)
201    } else if let Some(prefix) = pattern.strip_suffix("*") {
202        path.starts_with(prefix)
203    } else {
204        path == pattern
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_capability_set() {
214        let mut caps = CapabilitySet::new();
215
216        caps.grant(Capability::AudioSpeak);
217        caps.grant(Capability::DisplayText);
218
219        assert!(caps.has(&Capability::AudioSpeak));
220        assert!(caps.has(&Capability::DisplayText));
221        assert!(!caps.has(&Capability::AudioPlay));
222
223        assert!(caps.revoke(&Capability::AudioSpeak));
224        assert!(!caps.has(&Capability::AudioSpeak));
225    }
226
227    #[test]
228    fn test_pattern_matching() {
229        let mut caps = CapabilitySet::new();
230
231        caps.grant(Capability::NetworkFetch(
232            "https://api.example.com/*".to_string(),
233        ));
234        caps.grant(Capability::FileRead("/home/user/data/*".to_string()));
235
236        assert!(caps.has_matching(&Capability::NetworkFetch(
237            "https://api.example.com/users".to_string()
238        )));
239        assert!(!caps.has_matching(&Capability::NetworkFetch("https://evil.com/".to_string())));
240
241        assert!(caps.has_matching(&Capability::FileRead(
242            "/home/user/data/file.txt".to_string()
243        )));
244        assert!(!caps.has_matching(&Capability::FileRead("/etc/passwd".to_string())));
245    }
246
247    #[test]
248    fn test_set_operations() {
249        let caps1 = CapabilitySet::from_capabilities(vec![Capability::AudioSpeak, Capability::DisplayText]);
250
251        let caps2 = CapabilitySet::from_capabilities(vec![Capability::DisplayText, Capability::SystemTime]);
252
253        let intersection = caps1.intersection(&caps2);
254        assert!(intersection.has(&Capability::DisplayText));
255        assert!(!intersection.has(&Capability::AudioSpeak));
256        assert!(!intersection.has(&Capability::SystemTime));
257
258        let union = caps1.union(&caps2);
259        assert!(union.has(&Capability::AudioSpeak));
260        assert!(union.has(&Capability::DisplayText));
261        assert!(union.has(&Capability::SystemTime));
262    }
263}