use boundgraph_core::{GrantAtom, GrantSet};
use crate::pattern::{Pattern, Tri, covers};
pub fn atom_leq(inner: &GrantAtom, outer: &GrantAtom) -> Tri {
if inner.domain != outer.domain || inner.action != outer.action {
return Tri::No;
}
covers(&outer.resource, &inner.resource)
}
pub fn atom_eq(a: &GrantAtom, b: &GrantAtom) -> bool {
a.domain == b.domain
&& a.action == b.action
&& Pattern::parse(&a.resource) == Pattern::parse(&b.resource)
}
pub fn set_leq(inner: &GrantSet, outer: &GrantSet) -> Tri {
Tri::all(inner.iter().map(|i| {
Tri::any(outer.iter().map(|o| atom_leq(i, o)))
}))
}
pub fn set_eq(a: &GrantSet, b: &GrantSet) -> Tri {
set_leq(a, b).and(set_leq(b, a))
}
pub fn set_lt(inner: &GrantSet, outer: &GrantSet) -> Tri {
match set_leq(inner, outer) {
Tri::Yes => match set_leq(outer, inner) {
Tri::Yes => Tri::No, Tri::No => Tri::Yes,
Tri::Unknown => Tri::Unknown,
},
other => other,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn atom(s: &str) -> GrantAtom {
s.parse().expect("test atom should parse")
}
fn set(atoms: &[&str]) -> GrantSet {
GrantSet::parse(atoms.iter().copied()).expect("test set should parse")
}
#[test]
fn an_atom_permits_no_more_than_itself() {
for s in ["fs:read:/src/**", "time:now", "fs:read:{path}"] {
assert_eq!(atom_leq(&atom(s), &atom(s)), Tri::Yes);
}
}
#[test]
fn a_narrower_resource_is_below_a_broader_one() {
assert_eq!(
atom_leq(&atom("fs:read:/src/lib/a.rs"), &atom("fs:read:/src/**")),
Tri::Yes
);
assert_eq!(
atom_leq(&atom("fs:read:/src/**"), &atom("fs:read:/src/lib/a.rs")),
Tri::No
);
}
#[test]
fn different_domains_or_actions_are_incomparable_not_undecidable() {
assert_eq!(
atom_leq(&atom("fs:read:/src/**"), &atom("net:read:/src/**")),
Tri::No
);
assert_eq!(
atom_leq(&atom("fs:write:/src/**"), &atom("fs:read:/src/**")),
Tri::No
);
assert!(
!atom_leq(&atom("fs:write:/src/a"), &atom("fs:read:/src/**")).is_unknown(),
"a domain or action mismatch is decidable"
);
}
#[test]
fn read_does_not_imply_write_or_the_reverse() {
assert_eq!(
atom_leq(&atom("fs:write:/src/a"), &atom("fs:read:/src/**")),
Tri::No,
"holding read must never confer write"
);
}
#[test]
fn undecidable_resources_report_unknown() {
assert_eq!(
atom_leq(&atom("fs:read:/src/main.rs"), &atom("fs:read:/src/*.rs")),
Tri::Unknown
);
}
#[test]
fn atom_equality_is_over_denotations_not_spellings() {
assert!(atom_eq(&atom("fs:read:/src/"), &atom("fs:read:/src")));
assert!(!atom_eq(&atom("fs:read:/src"), &atom("fs:read:/src/**")));
}
#[test]
fn the_empty_set_is_below_everything() {
assert_eq!(
set_leq(&GrantSet::EMPTY, &set(&["fs:read:/src/**"])),
Tri::Yes
);
assert_eq!(set_leq(&GrantSet::EMPTY, &GrantSet::EMPTY), Tri::Yes);
}
#[test]
fn nothing_nonempty_is_below_the_empty_set() {
assert_eq!(
set_leq(&set(&["fs:read:/src/**"]), &GrantSet::EMPTY),
Tri::No
);
}
#[test]
fn one_broad_atom_can_cover_several_narrow_ones() {
let child = set(&["fs:read:/src/a.rs", "fs:read:/src/lib/b.rs"]);
let parent = set(&["fs:read:/src/**"]);
assert_eq!(set_leq(&child, &parent), Tri::Yes);
assert_eq!(set_leq(&parent, &child), Tri::No);
}
#[test]
fn a_single_escaping_atom_sinks_the_whole_set() {
let child = set(&["fs:read:/src/a.rs", "fs:read:/etc/passwd"]);
let parent = set(&["fs:read:/src/**"]);
assert_eq!(set_leq(&child, &parent), Tri::No);
}
#[test]
fn an_undecidable_atom_makes_the_set_comparison_unknown() {
let child = set(&["fs:read:/src/a.rs", "fs:read:{path}"]);
let parent = set(&["fs:read:/src/**"]);
assert_eq!(set_leq(&child, &parent), Tri::Unknown);
}
#[test]
fn a_definite_violation_beats_an_undecidable_sibling() {
let child = set(&["fs:read:{path}", "fs:read:/etc/passwd"]);
let parent = set(&["fs:read:/src/**"]);
assert_eq!(set_leq(&child, &parent), Tri::No);
}
#[test]
fn set_equality_and_strict_ordering() {
let a = set(&["fs:read:/src/**"]);
let b = set(&["fs:read:/src/"]);
let narrow = set(&["fs:read:/src/a.rs"]);
assert_eq!(set_eq(&a, &a), Tri::Yes);
assert_eq!(
set_lt(&a, &a),
Tri::No,
"a set is not strictly below itself"
);
assert_eq!(set_lt(&narrow, &a), Tri::Yes);
assert_eq!(set_lt(&a, &narrow), Tri::No);
assert_eq!(set_eq(&b, &b), Tri::Yes);
}
#[test]
fn the_order_is_reflexive() {
for s in [
set(&["fs:read:/src/**"]),
set(&["fs:read:/a", "net:egress:h:1"]),
GrantSet::EMPTY,
] {
assert_eq!(set_leq(&s, &s), Tri::Yes);
}
}
#[test]
fn the_order_is_transitive() {
let a = set(&["fs:read:/src/lib/a.rs"]);
let b = set(&["fs:read:/src/lib/**"]);
let c = set(&["fs:read:/src/**"]);
assert_eq!(set_leq(&a, &b), Tri::Yes);
assert_eq!(set_leq(&b, &c), Tri::Yes);
assert_eq!(set_leq(&a, &c), Tri::Yes, "⊑ must compose");
}
#[test]
fn the_order_is_antisymmetric_over_denotations() {
let a = set(&["fs:read:/src"]);
let b = set(&["fs:read:/src/"]);
assert_eq!(set_leq(&a, &b), Tri::Yes);
assert_eq!(set_leq(&b, &a), Tri::Yes);
assert_eq!(set_eq(&a, &b), Tri::Yes, "mutual ⊑ means equal denotation");
}
}