use std::collections::BTreeSet;
use mkit_attest::grant::{RefPattern, packmap_head};
use crate::error::ServerError;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RefRule {
pub pattern: RefPattern,
pub allowed_signers: Option<BTreeSet<[u8; 32]>>,
pub fast_forward_only: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RefPolicy {
rules: Vec<RefRule>,
}
fn rule_name(ref_name: &str) -> String {
packmap_head(ref_name).unwrap_or_else(|| ref_name.to_owned())
}
impl RefPolicy {
pub fn validate_for_indexed(&self, indexed: bool) -> Result<(), ServerError> {
self.validate()?;
if self.has_fast_forward_rule() && !indexed {
return Err(ServerError::invalid_argument(
"fast-forward-only ref rules require indexed mode",
));
}
Ok(())
}
#[must_use]
pub fn new(rules: Vec<RefRule>) -> Self {
Self { rules }
}
pub(crate) fn has_fast_forward_rule(&self) -> bool {
self.rules.iter().any(|rule| rule.fast_forward_only)
}
pub(crate) fn validate(&self) -> Result<(), ServerError> {
for rule in &self.rules {
if RefPattern::parse(&rule.pattern.to_string()).as_ref() != Ok(&rule.pattern) {
return Err(ServerError::invalid_argument("invalid ref policy pattern"));
}
}
Ok(())
}
pub(crate) fn signer_allowed(&self, ref_name: &str, signer: Option<&[u8; 32]>) -> bool {
let name = rule_name(ref_name);
self.rules
.iter()
.filter(|rule| rule.pattern.matches(&name))
.filter_map(|rule| rule.allowed_signers.as_ref())
.all(|allowed| signer.is_some_and(|key| allowed.contains(key)))
}
pub(crate) fn fast_forward_only(&self, ref_name: &str) -> bool {
packmap_head(ref_name).is_none()
&& self
.rules
.iter()
.any(|rule| rule.fast_forward_only && rule.pattern.matches(ref_name))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rule(pattern: &str, signers: Option<&[[u8; 32]]>, ff: bool) -> RefRule {
RefRule {
pattern: RefPattern::parse(pattern).unwrap(),
allowed_signers: signers.map(|keys| keys.iter().copied().collect()),
fast_forward_only: ff,
}
}
#[test]
fn overlapping_signer_rules_intersect_and_packmaps_follow_their_head() {
let (a, b, c) = ([1; 32], [2; 32], [3; 32]);
let policy = RefPolicy::new(vec![
rule("refs/heads/*", Some(&[a, b]), false),
rule("refs/heads/main", Some(&[b, c]), true),
]);
assert!(policy.validate().is_ok());
assert!(policy.signer_allowed("refs/heads/main", Some(&b)));
assert!(!policy.signer_allowed("refs/heads/main", Some(&a)));
assert!(!policy.signer_allowed("refs/heads/main", Some(&c)));
assert!(!policy.signer_allowed("refs/heads/main", None));
assert!(!policy.signer_allowed("refs/mkit/packmap/main", Some(&a)));
assert!(policy.signer_allowed("refs/mkit/packmap/main", Some(&b)));
assert!(policy.signer_allowed("refs/tags/v1", None));
assert!(policy.fast_forward_only("refs/heads/main"));
assert!(!policy.fast_forward_only("refs/mkit/packmap/main"));
assert!(!policy.fast_forward_only("refs/heads/dev"));
let ff_only = RefPolicy::new(vec![rule("refs/heads/main", None, true)]);
assert!(ff_only.signer_allowed("refs/heads/main", None));
}
#[test]
fn a_pattern_naming_a_packmap_ref_is_refused_at_startup() {
let policy = RefPolicy::new(vec![RefRule {
pattern: RefPattern::Exact("refs/mkit/packmap/main".into()),
allowed_signers: None,
fast_forward_only: true,
}]);
assert!(policy.validate().is_err());
}
}