use serde::{Deserialize, Serialize};
use crate::statements::action_in_scope;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthorityUse {
pub granted: Vec<String>,
pub exercised: Vec<String>,
pub dormant: Vec<String>,
pub out_of_scope: Vec<String>,
}
impl AuthorityUse {
pub fn compute(scopes: &[String], actions: &[String]) -> Self {
let granted = sorted_unique(scopes);
let exercised = sorted_unique(actions);
let dormant = granted
.iter()
.filter(|entry| {
!exercised
.iter()
.any(|a| action_in_scope(a, std::slice::from_ref(*entry)))
})
.cloned()
.collect();
let out_of_scope = exercised
.iter()
.filter(|a| !action_in_scope(a, &granted))
.cloned()
.collect();
Self {
granted,
exercised,
dormant,
out_of_scope,
}
}
pub fn fully_exercised(&self) -> bool {
!self.granted.is_empty() && self.dormant.is_empty()
}
pub fn summary(&self) -> String {
if self.granted.is_empty() {
return "no mandate scope on this chain".to_string();
}
let mut s = format!(
"{} of {} granted capabilit{} exercised",
self.granted.len() - self.dormant.len(),
self.granted.len(),
if self.granted.len() == 1 { "y" } else { "ies" }
);
if !self.dormant.is_empty() {
s.push_str(&format!("; dormant: {}", self.dormant.join(", ")));
}
if !self.out_of_scope.is_empty() {
s.push_str(&format!("; OUT OF SCOPE: {}", self.out_of_scope.join(", ")));
}
s
}
}
fn sorted_unique(v: &[String]) -> Vec<String> {
let mut out: Vec<String> = v.to_vec();
out.sort();
out.dedup();
out
}
#[cfg(test)]
mod tests {
use super::*;
fn s(v: &[&str]) -> Vec<String> {
v.iter().map(|x| x.to_string()).collect()
}
#[test]
fn narrow_and_broad_grants_are_distinguishable() {
let narrow = AuthorityUse::compute(&s(&["deploy.staging"]), &s(&["deploy.staging"]));
let broad = AuthorityUse::compute(
&s(&["deploy.staging", "deploy.production"]),
&s(&["deploy.staging"]),
);
assert!(narrow.dormant.is_empty());
assert_eq!(broad.dormant, s(&["deploy.production"]));
assert_eq!(narrow.exercised, broad.exercised);
assert_ne!(
narrow, broad,
"identical actions under different grants must not compare equal"
);
assert!(broad.summary().contains("deploy.production"));
}
#[test]
fn a_used_glob_family_is_not_dormant() {
let u = AuthorityUse::compute(&s(&["payments.*"]), &s(&["payments.refund"]));
assert!(u.dormant.is_empty(), "{:?}", u.dormant);
assert!(u.out_of_scope.is_empty());
}
#[test]
fn an_entirely_unused_glob_family_is_dormant() {
let u = AuthorityUse::compute(&s(&["payments.*", "read.repo"]), &s(&["read.repo"]));
assert_eq!(u.dormant, s(&["payments.*"]));
}
#[test]
fn out_of_scope_matches_the_verifier_predicate() {
let u = AuthorityUse::compute(&s(&["read.*"]), &s(&["read.repo", "deploy.production"]));
assert_eq!(u.out_of_scope, s(&["deploy.production"]));
assert!(!action_in_scope("deploy.production", &s(&["read.*"])));
assert!(action_in_scope("read.repo", &s(&["read.*"])));
}
#[test]
fn bare_star_is_not_treated_as_a_wildcard() {
let u = AuthorityUse::compute(&s(&["*"]), &s(&["deploy.production"]));
assert_eq!(u.out_of_scope, s(&["deploy.production"]));
assert_eq!(u.dormant, s(&["*"]));
}
#[test]
fn duplicates_across_hops_collapse() {
let u = AuthorityUse::compute(
&s(&["read.repo", "read.repo", "edit.src"]),
&s(&["read.repo", "read.repo"]),
);
assert_eq!(u.granted, s(&["edit.src", "read.repo"]));
assert_eq!(u.exercised, s(&["read.repo"]));
assert_eq!(u.dormant, s(&["edit.src"]));
}
#[test]
fn empty_scope_authorizes_nothing() {
let u = AuthorityUse::compute(&[], &s(&["read.repo"]));
assert!(u.granted.is_empty());
assert_eq!(u.out_of_scope, s(&["read.repo"]));
assert!(!u.fully_exercised());
assert!(u.summary().contains("no mandate scope"));
}
#[test]
fn a_chain_that_used_everything_says_so() {
let u = AuthorityUse::compute(&s(&["a.one", "b.two"]), &s(&["a.one", "b.two"]));
assert!(u.fully_exercised());
assert!(!u.summary().contains("dormant"));
}
}