use std::collections::BTreeSet;
use crate::audit::CapProvenance;
#[derive(Debug, Clone, Default)]
pub struct CapabilitySet {
granted: BTreeSet<String>,
provenance: Vec<CapProvenance>,
}
impl CapabilitySet {
pub fn empty() -> Self {
Self::default()
}
pub fn with_provenance<I>(granted: I, provenance: Vec<CapProvenance>) -> Self
where
I: IntoIterator<Item = String>,
{
Self {
granted: granted.into_iter().collect(),
provenance,
}
}
pub fn contains(&self, cap: &str) -> bool {
self.granted.contains(cap)
}
pub fn granted(&self) -> Vec<String> {
self.granted.iter().cloned().collect()
}
pub fn provenance(&self) -> &[CapProvenance] {
&self.provenance
}
pub fn intersect(&self, requested: &[String]) -> (Vec<String>, Vec<String>) {
let mut granted: Vec<String> = Vec::new();
let mut denied: Vec<String> = Vec::new();
let req_sorted: BTreeSet<&String> = requested.iter().collect();
for r in req_sorted {
if self.granted.contains(r) {
granted.push(r.clone());
} else {
denied.push(r.clone());
}
}
(granted, denied)
}
pub fn union(&self, other: &Self) -> Self {
let mut provenance = self.provenance.clone();
for p in &other.provenance {
if !provenance.contains(p) {
provenance.push(p.clone());
}
}
Self {
granted: self.granted.union(&other.granted).cloned().collect(),
provenance,
}
}
}
impl FromIterator<String> for CapabilitySet {
fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
Self {
granted: iter.into_iter().collect(),
provenance: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_contains_nothing() {
let s = CapabilitySet::empty();
assert!(!s.contains("anything"));
assert!(s.granted().is_empty());
}
#[test]
fn from_iter_builds_set() {
let s = CapabilitySet::from_iter(["read".to_string(), "exec".to_string()]);
assert!(s.contains("read"));
assert!(s.contains("exec"));
assert!(!s.contains("admin"));
}
#[test]
fn granted_returns_sorted_deterministic_order() {
let s =
CapabilitySet::from_iter(["zeta".to_string(), "alpha".to_string(), "mu".to_string()]);
assert_eq!(s.granted(), vec!["alpha", "mu", "zeta"]);
}
#[test]
fn intersect_with_empty_granted_denies_all() {
let s = CapabilitySet::empty();
let (granted, denied) = s.intersect(&["read".into(), "exec".into()]);
assert!(granted.is_empty());
assert_eq!(denied, vec!["exec", "read"]);
}
#[test]
fn intersect_partial() {
let s = CapabilitySet::from_iter(["read".to_string(), "exec".to_string()]);
let (granted, denied) = s.intersect(&["read".into(), "admin".into(), "exec".into()]);
assert_eq!(granted, vec!["exec", "read"]);
assert_eq!(denied, vec!["admin"]);
}
#[test]
fn intersect_full_grants_all_requested() {
let s = CapabilitySet::from_iter(["a".to_string(), "b".to_string(), "c".to_string()]);
let (granted, denied) = s.intersect(&["a".into(), "b".into()]);
assert_eq!(granted, vec!["a", "b"]);
assert!(denied.is_empty());
}
#[test]
fn intersect_dedupes_via_sorted_input() {
let s = CapabilitySet::from_iter(["read".to_string()]);
let (granted, denied) = s.intersect(&["read".into(), "read".into(), "admin".into()]);
assert_eq!(granted, vec!["read"]);
assert_eq!(denied, vec!["admin"]);
}
}