1use std::collections::BTreeSet;
38
39use serde::{Deserialize, Serialize};
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "snake_case")]
49pub enum Scope<T: Ord + Clone> {
50 All,
52 Only(BTreeSet<T>),
54}
55
56impl<T: Ord + Clone> Scope<T> {
57 #[must_use]
59 pub fn top() -> Self {
60 Self::All
61 }
62
63 #[must_use]
65 pub fn none() -> Self {
66 Self::Only(BTreeSet::new())
67 }
68
69 pub fn only<I: IntoIterator<Item = T>>(items: I) -> Self {
71 Self::Only(items.into_iter().collect())
72 }
73
74 #[must_use]
76 pub fn leq(&self, other: &Self) -> bool {
77 match (self, other) {
78 (_, Self::All) => true,
79 (Self::All, Self::Only(_)) => false,
80 (Self::Only(a), Self::Only(b)) => a.is_subset(b),
81 }
82 }
83
84 #[must_use]
87 pub fn meet(&self, other: &Self) -> Self {
88 match (self, other) {
89 (Self::All, x) | (x, Self::All) => x.clone(),
90 (Self::Only(a), Self::Only(b)) => Self::Only(a.intersection(b).cloned().collect()),
91 }
92 }
93
94 #[must_use]
99 pub fn permits(&self, item: &T) -> bool {
100 match self {
101 Self::All => true,
102 Self::Only(set) => set.contains(item),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113pub enum CountBound {
114 Unlimited,
116 AtMost(u64),
118}
119
120impl CountBound {
121 #[must_use]
123 pub fn top() -> Self {
124 Self::Unlimited
125 }
126
127 #[must_use]
129 pub fn leq(&self, other: &Self) -> bool {
130 match (self, other) {
131 (_, Self::Unlimited) => true,
132 (Self::Unlimited, Self::AtMost(_)) => false,
133 (Self::AtMost(a), Self::AtMost(b)) => a <= b,
134 }
135 }
136
137 #[must_use]
139 pub fn meet(&self, other: &Self) -> Self {
140 match (self, other) {
141 (Self::Unlimited, x) | (x, Self::Unlimited) => *x,
142 (Self::AtMost(a), Self::AtMost(b)) => Self::AtMost((*a).min(*b)),
143 }
144 }
145
146 #[must_use]
151 pub fn permits_one_more(&self, used_so_far: u64) -> bool {
152 match self {
153 Self::Unlimited => true,
154 Self::AtMost(n) => used_so_far < *n,
155 }
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct Caveats {
163 pub fs_read: Scope<String>,
165 pub fs_write: Scope<String>,
167 pub exec: Scope<String>,
169 pub net: Scope<String>,
171 pub max_calls: CountBound,
173 pub valid_for_generation: Scope<u64>,
176}
177
178impl Caveats {
179 #[must_use]
183 pub fn top() -> Self {
184 Self {
185 fs_read: Scope::top(),
186 fs_write: Scope::top(),
187 exec: Scope::top(),
188 net: Scope::top(),
189 max_calls: CountBound::top(),
190 valid_for_generation: Scope::top(),
191 }
192 }
193
194 #[must_use]
197 pub fn leq(&self, other: &Self) -> bool {
198 self.fs_read.leq(&other.fs_read)
199 && self.fs_write.leq(&other.fs_write)
200 && self.exec.leq(&other.exec)
201 && self.net.leq(&other.net)
202 && self.max_calls.leq(&other.max_calls)
203 && self.valid_for_generation.leq(&other.valid_for_generation)
204 }
205
206 #[must_use]
208 pub fn meet(&self, other: &Self) -> Self {
209 Self {
210 fs_read: self.fs_read.meet(&other.fs_read),
211 fs_write: self.fs_write.meet(&other.fs_write),
212 exec: self.exec.meet(&other.exec),
213 net: self.net.meet(&other.net),
214 max_calls: self.max_calls.meet(&other.max_calls),
215 valid_for_generation: self.valid_for_generation.meet(&other.valid_for_generation),
216 }
217 }
218
219 #[must_use]
225 pub fn permits_fs_read(&self, path: &str) -> bool {
226 self.fs_read.permits(&path.to_string())
227 }
228
229 #[must_use]
231 pub fn permits_fs_write(&self, path: &str) -> bool {
232 self.fs_write.permits(&path.to_string())
233 }
234
235 #[must_use]
237 pub fn permits_exec(&self, cmd: &str) -> bool {
238 self.exec.permits(&cmd.to_string())
239 }
240
241 #[must_use]
243 pub fn permits_net(&self, host: &str) -> bool {
244 self.net.permits(&host.to_string())
245 }
246}
247
248impl Default for Caveats {
249 fn default() -> Self {
253 Self::top()
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
262 fn scope_all_permits_everything() {
263 let s: Scope<String> = Scope::All;
264 assert!(s.permits(&"anything".to_string()));
265 assert!(s.permits(&"".to_string()));
266 }
267
268 #[test]
269 fn scope_only_permits_exact_members() {
270 let s = Scope::only(["a".to_string(), "b".to_string()]);
271 assert!(s.permits(&"a".to_string()));
272 assert!(s.permits(&"b".to_string()));
273 assert!(!s.permits(&"c".to_string()));
274 assert!(!s.permits(&"".to_string()));
275 }
276
277 #[test]
278 fn scope_none_permits_nothing() {
279 let s: Scope<String> = Scope::none();
280 assert!(!s.permits(&"a".to_string()));
281 }
282
283 #[test]
284 fn scope_lattice_order() {
285 let small = Scope::only(["a".to_string()]);
286 let big = Scope::only(["a".to_string(), "b".to_string()]);
287 assert!(small.leq(&big));
288 assert!(small.leq(&Scope::All));
289 assert!(!Scope::<String>::All.leq(&small));
290 assert!(!big.leq(&small));
291 }
292
293 #[test]
294 fn scope_meet_intersects() {
295 let a = Scope::only(["a".to_string(), "b".to_string()]);
296 let b = Scope::only(["b".to_string(), "c".to_string()]);
297 assert_eq!(a.meet(&b), Scope::only(["b".to_string()]));
298 assert_eq!(a.meet(&Scope::All), a);
299 }
300
301 #[test]
302 fn count_bound_permits_one_more() {
303 assert!(CountBound::Unlimited.permits_one_more(0));
304 assert!(CountBound::Unlimited.permits_one_more(99_999));
305 assert!(CountBound::AtMost(3).permits_one_more(0));
306 assert!(CountBound::AtMost(3).permits_one_more(2));
307 assert!(!CountBound::AtMost(3).permits_one_more(3));
308 assert!(!CountBound::AtMost(3).permits_one_more(99));
309 assert!(!CountBound::AtMost(0).permits_one_more(0));
311 }
312
313 #[test]
314 fn count_bound_meet_is_tighter() {
315 assert_eq!(
316 CountBound::AtMost(5).meet(&CountBound::AtMost(3)),
317 CountBound::AtMost(3)
318 );
319 assert_eq!(
320 CountBound::Unlimited.meet(&CountBound::AtMost(7)),
321 CountBound::AtMost(7)
322 );
323 }
324
325 #[test]
326 fn caveats_top_permits_everything() {
327 let c = Caveats::top();
328 assert!(c.permits_fs_read("/anywhere"));
329 assert!(c.permits_fs_write("/anywhere"));
330 assert!(c.permits_exec("rm"));
331 assert!(c.permits_net("evil.example.com"));
332 assert!(c.max_calls.permits_one_more(1_000_000));
333 }
334
335 #[test]
336 fn caveats_default_is_top() {
337 assert_eq!(Caveats::default(), Caveats::top());
338 }
339
340 #[test]
341 fn caveats_attenuated_denies_outside_scope() {
342 let c = Caveats {
343 fs_write: Scope::only(["allowed.rs".to_string()]),
344 net: Scope::only(["allowed.example.com".to_string()]),
345 max_calls: CountBound::AtMost(2),
346 ..Caveats::top()
347 };
348 assert!(c.permits_fs_write("allowed.rs"));
349 assert!(!c.permits_fs_write("forbidden.rs"));
350 assert!(c.permits_net("allowed.example.com"));
351 assert!(!c.permits_net("evil.example.com"));
352 assert!(c.max_calls.permits_one_more(1));
353 assert!(!c.max_calls.permits_one_more(2));
354 }
355
356 #[test]
357 fn caveats_leq_per_axis() {
358 let restricted = Caveats {
359 fs_write: Scope::only(["a.rs".to_string()]),
360 ..Caveats::top()
361 };
362 assert!(restricted.leq(&Caveats::top()));
363 assert!(!Caveats::top().leq(&restricted));
364 }
365
366 #[test]
367 fn caveats_meet_attenuates_each_axis() {
368 let a = Caveats {
369 fs_read: Scope::only(["/repo".to_string(), "/tmp".to_string()]),
370 max_calls: CountBound::AtMost(10),
371 ..Caveats::top()
372 };
373 let b = Caveats {
374 fs_read: Scope::only(["/repo".to_string()]),
375 max_calls: CountBound::AtMost(4),
376 ..Caveats::top()
377 };
378 let m = a.meet(&b);
379 assert_eq!(m.fs_read, Scope::only(["/repo".to_string()]));
380 assert_eq!(m.max_calls, CountBound::AtMost(4));
381 assert!(m.leq(&a) && m.leq(&b));
382 }
383
384 #[test]
385 fn caveats_serde_roundtrip() {
386 let c = Caveats {
387 exec: Scope::only(["git".to_string(), "cargo".to_string()]),
388 max_calls: CountBound::AtMost(3),
389 valid_for_generation: Scope::only([42u64]),
390 ..Caveats::top()
391 };
392 let json = serde_json::to_string(&c).unwrap();
393 let back: Caveats = serde_json::from_str(&json).unwrap();
394 assert_eq!(c, back);
395 }
396}