Skip to main content

boundgraph_grant/
order.rs

1//! The partial order `⊑` — "permits no more than".
2//!
3//! This is the relation Law I is stated in, and the whole attenuation check
4//! (US-0403) reduces to it: a flow is legal when every child's grant is `⊑` its
5//! parent's. Getting it wrong makes the security guarantee wrong, so the order
6//! is verified as a partial order — reflexive, antisymmetric, transitive — by
7//! property tests rather than assumed.
8//!
9//! Every comparison is three-valued. An `Unknown` is a refusal: the compiler
10//! rejects rather than assume (US-0206).
11
12use boundgraph_core::{GrantAtom, GrantSet};
13
14use crate::pattern::{Pattern, Tri, covers};
15
16/// Whether `inner` permits no more than `outer` — that is, `inner ⊑ outer`.
17///
18/// Atoms in different domains or with different actions are **incomparable**,
19/// which is a definite [`Tri::No`], not an `Unknown`: they are unrelated, and
20/// that is a decided fact rather than a gap in the solver.
21pub 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
28/// Whether two atoms denote exactly the same permission.
29///
30/// Distinct spellings of one pattern (`/src` and `/src/`) are equal here, which
31/// is what makes the order antisymmetric over denotations rather than strings.
32pub 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
38/// Whether `inner ⊑ outer` for grant sets.
39///
40/// Every atom of `inner` must be permitted by *some* atom of `outer`. A single
41/// broad atom can therefore cover many narrow ones, which is what makes
42/// attenuation checks cheap in the common case.
43///
44/// The empty set is below everything — a step that declares nothing is always
45/// within its parent's authority.
46pub fn set_leq(inner: &GrantSet, outer: &GrantSet) -> Tri {
47    Tri::all(inner.iter().map(|i| {
48        // Does anything in `outer` permit this atom?
49        Tri::any(outer.iter().map(|o| atom_leq(i, o)))
50    }))
51}
52
53/// Whether two grant sets permit exactly the same thing.
54pub fn set_eq(a: &GrantSet, b: &GrantSet) -> Tri {
55    set_leq(a, b).and(set_leq(b, a))
56}
57
58/// Whether `inner ⊑ outer` **and** the two are not equal — a strict narrowing.
59pub 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, // equal, so not strictly below
63            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    // ── atoms ───────────────────────────────────────────────────────────────
83
84    #[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        // This distinction matters: incomparable is a decided fact, so the
106        // compiler rejects cleanly instead of reporting a solver gap.
107        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    // ── sets ────────────────────────────────────────────────────────────────
145
146    #[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        // One atom is undecidable, another definitely escapes. The definite
188        // violation must win, so the compiler reports the real problem.
189        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    // ── the partial-order laws, on worked examples ──────────────────────────
212    // Exhaustive generative coverage is US-0207; these pin the shape.
213
214    #[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}