1use boundgraph_core::{GrantAtom, GrantSet};
13
14use crate::pattern::{Pattern, Tri, covers};
15
16pub fn atom_leq(inner: &GrantAtom, outer: &GrantAtom) -> Tri {
22 if inner.domain != outer.domain || inner.action != outer.action {
23 return Tri::No;
24 }
25 covers(&outer.resource, &inner.resource)
26}
27
28pub fn atom_eq(a: &GrantAtom, b: &GrantAtom) -> bool {
33 a.domain == b.domain
34 && a.action == b.action
35 && Pattern::parse(&a.resource) == Pattern::parse(&b.resource)
36}
37
38pub fn set_leq(inner: &GrantSet, outer: &GrantSet) -> Tri {
47 Tri::all(inner.iter().map(|i| {
48 Tri::any(outer.iter().map(|o| atom_leq(i, o)))
50 }))
51}
52
53pub fn set_eq(a: &GrantSet, b: &GrantSet) -> Tri {
55 set_leq(a, b).and(set_leq(b, a))
56}
57
58pub fn set_lt(inner: &GrantSet, outer: &GrantSet) -> Tri {
60 match set_leq(inner, outer) {
61 Tri::Yes => match set_leq(outer, inner) {
62 Tri::Yes => Tri::No, Tri::No => Tri::Yes,
64 Tri::Unknown => Tri::Unknown,
65 },
66 other => other,
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73
74 fn atom(s: &str) -> GrantAtom {
75 s.parse().expect("test atom should parse")
76 }
77
78 fn set(atoms: &[&str]) -> GrantSet {
79 GrantSet::parse(atoms.iter().copied()).expect("test set should parse")
80 }
81
82 #[test]
85 fn an_atom_permits_no_more_than_itself() {
86 for s in ["fs:read:/src/**", "time:now", "fs:read:{path}"] {
87 assert_eq!(atom_leq(&atom(s), &atom(s)), Tri::Yes);
88 }
89 }
90
91 #[test]
92 fn a_narrower_resource_is_below_a_broader_one() {
93 assert_eq!(
94 atom_leq(&atom("fs:read:/src/lib/a.rs"), &atom("fs:read:/src/**")),
95 Tri::Yes
96 );
97 assert_eq!(
98 atom_leq(&atom("fs:read:/src/**"), &atom("fs:read:/src/lib/a.rs")),
99 Tri::No
100 );
101 }
102
103 #[test]
104 fn different_domains_or_actions_are_incomparable_not_undecidable() {
105 assert_eq!(
108 atom_leq(&atom("fs:read:/src/**"), &atom("net:read:/src/**")),
109 Tri::No
110 );
111 assert_eq!(
112 atom_leq(&atom("fs:write:/src/**"), &atom("fs:read:/src/**")),
113 Tri::No
114 );
115 assert!(
116 !atom_leq(&atom("fs:write:/src/a"), &atom("fs:read:/src/**")).is_unknown(),
117 "a domain or action mismatch is decidable"
118 );
119 }
120
121 #[test]
122 fn read_does_not_imply_write_or_the_reverse() {
123 assert_eq!(
124 atom_leq(&atom("fs:write:/src/a"), &atom("fs:read:/src/**")),
125 Tri::No,
126 "holding read must never confer write"
127 );
128 }
129
130 #[test]
131 fn undecidable_resources_report_unknown() {
132 assert_eq!(
133 atom_leq(&atom("fs:read:/src/main.rs"), &atom("fs:read:/src/*.rs")),
134 Tri::Unknown
135 );
136 }
137
138 #[test]
139 fn atom_equality_is_over_denotations_not_spellings() {
140 assert!(atom_eq(&atom("fs:read:/src/"), &atom("fs:read:/src")));
141 assert!(!atom_eq(&atom("fs:read:/src"), &atom("fs:read:/src/**")));
142 }
143
144 #[test]
147 fn the_empty_set_is_below_everything() {
148 assert_eq!(
149 set_leq(&GrantSet::EMPTY, &set(&["fs:read:/src/**"])),
150 Tri::Yes
151 );
152 assert_eq!(set_leq(&GrantSet::EMPTY, &GrantSet::EMPTY), Tri::Yes);
153 }
154
155 #[test]
156 fn nothing_nonempty_is_below_the_empty_set() {
157 assert_eq!(
158 set_leq(&set(&["fs:read:/src/**"]), &GrantSet::EMPTY),
159 Tri::No
160 );
161 }
162
163 #[test]
164 fn one_broad_atom_can_cover_several_narrow_ones() {
165 let child = set(&["fs:read:/src/a.rs", "fs:read:/src/lib/b.rs"]);
166 let parent = set(&["fs:read:/src/**"]);
167 assert_eq!(set_leq(&child, &parent), Tri::Yes);
168 assert_eq!(set_leq(&parent, &child), Tri::No);
169 }
170
171 #[test]
172 fn a_single_escaping_atom_sinks_the_whole_set() {
173 let child = set(&["fs:read:/src/a.rs", "fs:read:/etc/passwd"]);
174 let parent = set(&["fs:read:/src/**"]);
175 assert_eq!(set_leq(&child, &parent), Tri::No);
176 }
177
178 #[test]
179 fn an_undecidable_atom_makes_the_set_comparison_unknown() {
180 let child = set(&["fs:read:/src/a.rs", "fs:read:{path}"]);
181 let parent = set(&["fs:read:/src/**"]);
182 assert_eq!(set_leq(&child, &parent), Tri::Unknown);
183 }
184
185 #[test]
186 fn a_definite_violation_beats_an_undecidable_sibling() {
187 let child = set(&["fs:read:{path}", "fs:read:/etc/passwd"]);
190 let parent = set(&["fs:read:/src/**"]);
191 assert_eq!(set_leq(&child, &parent), Tri::No);
192 }
193
194 #[test]
195 fn set_equality_and_strict_ordering() {
196 let a = set(&["fs:read:/src/**"]);
197 let b = set(&["fs:read:/src/"]);
198 let narrow = set(&["fs:read:/src/a.rs"]);
199
200 assert_eq!(set_eq(&a, &a), Tri::Yes);
201 assert_eq!(
202 set_lt(&a, &a),
203 Tri::No,
204 "a set is not strictly below itself"
205 );
206 assert_eq!(set_lt(&narrow, &a), Tri::Yes);
207 assert_eq!(set_lt(&a, &narrow), Tri::No);
208 assert_eq!(set_eq(&b, &b), Tri::Yes);
209 }
210
211 #[test]
215 fn the_order_is_reflexive() {
216 for s in [
217 set(&["fs:read:/src/**"]),
218 set(&["fs:read:/a", "net:egress:h:1"]),
219 GrantSet::EMPTY,
220 ] {
221 assert_eq!(set_leq(&s, &s), Tri::Yes);
222 }
223 }
224
225 #[test]
226 fn the_order_is_transitive() {
227 let a = set(&["fs:read:/src/lib/a.rs"]);
228 let b = set(&["fs:read:/src/lib/**"]);
229 let c = set(&["fs:read:/src/**"]);
230 assert_eq!(set_leq(&a, &b), Tri::Yes);
231 assert_eq!(set_leq(&b, &c), Tri::Yes);
232 assert_eq!(set_leq(&a, &c), Tri::Yes, "⊑ must compose");
233 }
234
235 #[test]
236 fn the_order_is_antisymmetric_over_denotations() {
237 let a = set(&["fs:read:/src"]);
238 let b = set(&["fs:read:/src/"]);
239 assert_eq!(set_leq(&a, &b), Tri::Yes);
240 assert_eq!(set_leq(&b, &a), Tri::Yes);
241 assert_eq!(set_eq(&a, &b), Tri::Yes, "mutual ⊑ means equal denotation");
242 }
243}