use boundgraph_core::{GrantAtom, GrantSet};
use crate::normalize::normalize;
use crate::pattern::intersect;
pub fn join(a: &GrantSet, b: &GrantSet) -> GrantSet {
normalize(&GrantSet::new(a.iter().chain(b.iter()).cloned()))
}
pub fn join_all<'a>(sets: impl IntoIterator<Item = &'a GrantSet>) -> GrantSet {
sets.into_iter()
.fold(GrantSet::EMPTY, |acc, s| join(&acc, s))
}
pub fn meet(a: &GrantSet, b: &GrantSet) -> GrantSet {
let pairs = a
.iter()
.flat_map(|x| b.iter().filter_map(move |y| meet_atom(x, y)));
normalize(&GrantSet::new(pairs))
}
pub fn meet_all<'a>(sets: impl IntoIterator<Item = &'a GrantSet>) -> GrantSet {
let mut iter = sets.into_iter();
let Some(first) = iter.next() else {
return GrantSet::EMPTY;
};
iter.fold(first.clone(), |acc, s| meet(&acc, s))
}
fn meet_atom(x: &GrantAtom, y: &GrantAtom) -> Option<GrantAtom> {
if x.domain != y.domain || x.action != y.action {
return None;
}
intersect(&x.resource, &y.resource)
.map(|resource| GrantAtom::new(&x.domain, &x.action, resource))
}
pub fn clamp(resolved: &GrantSet, envelope: &GrantSet) -> GrantSet {
meet(resolved, envelope)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::normalize::is_normal;
use crate::order::{set_eq, set_leq};
use crate::pattern::Tri;
fn set(atoms: &[&str]) -> GrantSet {
GrantSet::parse(atoms.iter().copied()).expect("test set should parse")
}
#[test]
fn join_is_an_upper_bound_of_both_sides() {
let a = set(&["fs:read:/src/**"]);
let b = set(&["fs:write:/build/**"]);
let j = join(&a, &b);
assert_eq!(set_leq(&a, &j), Tri::Yes);
assert_eq!(set_leq(&b, &j), Tri::Yes);
}
#[test]
fn join_is_the_least_upper_bound() {
let a = set(&["fs:read:/src/lib/**"]);
let b = set(&["fs:read:/src/bin/**"]);
let j = join(&a, &b);
let above = set(&["fs:read:/src/**"]);
assert_eq!(set_leq(&j, &above), Tri::Yes);
}
#[test]
fn join_collapses_a_contained_side() {
let broad = set(&["fs:read:/src/**"]);
let narrow = set(&["fs:read:/src/lib/a.rs"]);
assert_eq!(join(&broad, &narrow), broad);
}
#[test]
fn join_with_empty_is_the_identity() {
let a = set(&["fs:read:/src/**"]);
assert_eq!(join(&a, &GrantSet::EMPTY), a);
assert_eq!(join(&GrantSet::EMPTY, &a), a);
}
#[test]
fn join_all_folds_an_envelope() {
let arms = [
set(&["fs:read:/a/**"]),
set(&["fs:read:/b/**"]),
set(&["net:egress:api.example.com:443"]),
];
let envelope = join_all(&arms);
for arm in &arms {
assert_eq!(set_leq(arm, &envelope), Tri::Yes);
}
assert_eq!(join_all(std::iter::empty()), GrantSet::EMPTY);
}
#[test]
fn meet_is_a_lower_bound_of_both_sides() {
let a = set(&["fs:read:/src/**", "fs:write:/build/**"]);
let b = set(&["fs:read:/src/lib/**"]);
let m = meet(&a, &b);
assert_eq!(set_leq(&m, &a), Tri::Yes);
assert_eq!(set_leq(&m, &b), Tri::Yes);
}
#[test]
fn meet_keeps_the_narrower_of_two_comparable_atoms() {
let broad = set(&["fs:read:/src/**"]);
let narrow = set(&["fs:read:/src/lib/a.rs"]);
assert_eq!(meet(&broad, &narrow), narrow);
}
#[test]
fn meet_of_disjoint_sets_is_empty() {
let a = set(&["fs:read:/src/**"]);
let b = set(&["fs:read:/etc/**"]);
assert!(meet(&a, &b).is_empty());
}
#[test]
fn meet_with_empty_is_empty() {
let a = set(&["fs:read:/src/**"]);
assert!(meet(&a, &GrantSet::EMPTY).is_empty());
assert!(meet(&GrantSet::EMPTY, &a).is_empty());
}
#[test]
fn meet_finds_an_overlap_between_incomparable_patterns() {
let a = set(&["fs:read:/a/*"]);
let b = set(&["fs:read:/*/b"]);
assert_eq!(meet(&a, &b), set(&["fs:read:/a/b"]));
}
#[test]
fn meet_of_a_single_glob_and_a_recursive_one_is_their_shared_root() {
let a = set(&["secret:read:/*"]);
let b = set(&["secret:read:/a/**"]);
assert_eq!(meet(&a, &b), set(&["secret:read:/a"]));
}
#[test]
fn meet_stays_a_sound_lower_bound() {
let a = set(&["fs:read:/a/*"]);
let b = set(&["fs:read:/*/b"]);
let m = meet(&a, &b);
assert_eq!(set_leq(&m, &a), Tri::Yes);
assert_eq!(set_leq(&m, &b), Tri::Yes);
}
#[test]
fn meet_drops_undecidable_pairs_rather_than_guessing() {
let a = set(&["fs:read:/src/*.rs"]);
let b = set(&["fs:read:/src/main.rs"]);
assert!(
meet(&a, &b).is_empty(),
"an undecided pair must not contribute to a lower bound"
);
}
fn samples() -> Vec<GrantSet> {
vec![
GrantSet::EMPTY,
set(&["fs:read:/src/**"]),
set(&["fs:read:/src/lib/a.rs"]),
set(&["fs:write:/build/**"]),
set(&["fs:read:/src/**", "net:egress:api.example.com:443"]),
]
}
#[test]
fn both_operations_are_idempotent() {
for a in samples() {
assert_eq!(join(&a, &a), normalize(&a));
assert_eq!(meet(&a, &a), normalize(&a));
}
}
#[test]
fn both_operations_are_commutative() {
for a in samples() {
for b in samples() {
assert_eq!(join(&a, &b), join(&b, &a));
assert_eq!(meet(&a, &b), meet(&b, &a));
}
}
}
#[test]
fn both_operations_are_associative() {
for a in samples() {
for b in samples() {
for c in samples() {
assert_eq!(join(&join(&a, &b), &c), join(&a, &join(&b, &c)));
assert_eq!(meet(&meet(&a, &b), &c), meet(&a, &meet(&b, &c)));
}
}
}
}
#[test]
fn absorption_holds() {
for a in samples() {
for b in samples() {
assert_eq!(set_eq(&meet(&a, &join(&a, &b)), &a), Tri::Yes);
assert_eq!(set_eq(&join(&a, &meet(&a, &b)), &a), Tri::Yes);
}
}
}
#[test]
fn results_are_always_normalised() {
for a in samples() {
for b in samples() {
assert!(is_normal(&join(&a, &b)));
assert!(is_normal(&meet(&a, &b)));
}
}
}
#[test]
fn a_resolved_template_can_only_narrow_its_envelope() {
let envelope = set(&["fs:read:/src/**"]);
let inside = set(&["fs:read:/src/lib/a.rs"]);
assert_eq!(clamp(&inside, &envelope), inside);
let outside = set(&["fs:read:/etc/passwd"]);
assert!(
clamp(&outside, &envelope).is_empty(),
"a template must never resolve to authority outside its envelope"
);
let wider = set(&["fs:read:/**"]);
assert_eq!(clamp(&wider, &envelope), envelope);
}
#[test]
fn clamping_never_exceeds_the_envelope() {
let envelope = set(&["fs:read:/src/**"]);
for resolved in samples() {
let clamped = clamp(&resolved, &envelope);
assert_eq!(
set_leq(&clamped, &envelope),
Tri::Yes,
"clamped grant escaped its envelope"
);
}
}
#[test]
fn meet_all_of_nothing_grants_nothing() {
assert_eq!(meet_all(std::iter::empty()), GrantSet::EMPTY);
}
}