atd_runtime/
capability.rs1use std::collections::BTreeSet;
11
12use crate::audit::CapProvenance;
13
14#[derive(Debug, Clone, Default)]
24pub struct CapabilitySet {
25 granted: BTreeSet<String>,
26 provenance: Vec<CapProvenance>,
27}
28
29impl CapabilitySet {
30 pub fn empty() -> Self {
31 Self::default()
32 }
33
34 pub fn with_provenance<I>(granted: I, provenance: Vec<CapProvenance>) -> Self
37 where
38 I: IntoIterator<Item = String>,
39 {
40 Self {
41 granted: granted.into_iter().collect(),
42 provenance,
43 }
44 }
45
46 pub fn contains(&self, cap: &str) -> bool {
47 self.granted.contains(cap)
48 }
49
50 pub fn granted(&self) -> Vec<String> {
52 self.granted.iter().cloned().collect()
53 }
54
55 pub fn provenance(&self) -> &[CapProvenance] {
60 &self.provenance
61 }
62
63 pub fn intersect(&self, requested: &[String]) -> (Vec<String>, Vec<String>) {
67 let mut granted: Vec<String> = Vec::new();
68 let mut denied: Vec<String> = Vec::new();
69 let req_sorted: BTreeSet<&String> = requested.iter().collect();
70 for r in req_sorted {
71 if self.granted.contains(r) {
72 granted.push(r.clone());
73 } else {
74 denied.push(r.clone());
75 }
76 }
77 (granted, denied)
78 }
79
80 pub fn union(&self, other: &Self) -> Self {
85 let mut provenance = self.provenance.clone();
92 for p in &other.provenance {
93 if !provenance.contains(p) {
94 provenance.push(p.clone());
95 }
96 }
97 Self {
98 granted: self.granted.union(&other.granted).cloned().collect(),
99 provenance,
100 }
101 }
102}
103
104impl FromIterator<String> for CapabilitySet {
105 fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
106 Self {
107 granted: iter.into_iter().collect(),
108 provenance: Vec::new(),
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn empty_contains_nothing() {
119 let s = CapabilitySet::empty();
120 assert!(!s.contains("anything"));
121 assert!(s.granted().is_empty());
122 }
123
124 #[test]
125 fn from_iter_builds_set() {
126 let s = CapabilitySet::from_iter(["read".to_string(), "exec".to_string()]);
127 assert!(s.contains("read"));
128 assert!(s.contains("exec"));
129 assert!(!s.contains("admin"));
130 }
131
132 #[test]
133 fn granted_returns_sorted_deterministic_order() {
134 let s =
136 CapabilitySet::from_iter(["zeta".to_string(), "alpha".to_string(), "mu".to_string()]);
137 assert_eq!(s.granted(), vec!["alpha", "mu", "zeta"]);
138 }
139
140 #[test]
141 fn intersect_with_empty_granted_denies_all() {
142 let s = CapabilitySet::empty();
143 let (granted, denied) = s.intersect(&["read".into(), "exec".into()]);
144 assert!(granted.is_empty());
145 assert_eq!(denied, vec!["exec", "read"]);
146 }
147
148 #[test]
149 fn intersect_partial() {
150 let s = CapabilitySet::from_iter(["read".to_string(), "exec".to_string()]);
151 let (granted, denied) = s.intersect(&["read".into(), "admin".into(), "exec".into()]);
152 assert_eq!(granted, vec!["exec", "read"]);
153 assert_eq!(denied, vec!["admin"]);
154 }
155
156 #[test]
157 fn intersect_full_grants_all_requested() {
158 let s = CapabilitySet::from_iter(["a".to_string(), "b".to_string(), "c".to_string()]);
159 let (granted, denied) = s.intersect(&["a".into(), "b".into()]);
160 assert_eq!(granted, vec!["a", "b"]);
161 assert!(denied.is_empty());
162 }
163
164 #[test]
165 fn intersect_dedupes_via_sorted_input() {
166 let s = CapabilitySet::from_iter(["read".to_string()]);
168 let (granted, denied) = s.intersect(&["read".into(), "read".into(), "admin".into()]);
169 assert_eq!(granted, vec!["read"]);
170 assert_eq!(denied, vec!["admin"]);
171 }
172}