mkit_server/policy/
ref_policy.rs1use std::collections::BTreeSet;
6
7use mkit_attest::grant::{RefPattern, packmap_head};
8
9use crate::error::ServerError;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct RefRule {
14 pub pattern: RefPattern,
17 pub allowed_signers: Option<BTreeSet<[u8; 32]>>,
21 pub fast_forward_only: bool,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
28pub struct RefPolicy {
29 rules: Vec<RefRule>,
30}
31
32fn rule_name(ref_name: &str) -> String {
34 packmap_head(ref_name).unwrap_or_else(|| ref_name.to_owned())
35}
36
37impl RefPolicy {
38 pub fn validate_for_indexed(&self, indexed: bool) -> Result<(), ServerError> {
42 self.validate()?;
43 if self.has_fast_forward_rule() && !indexed {
44 return Err(ServerError::invalid_argument(
45 "fast-forward-only ref rules require indexed mode",
46 ));
47 }
48 Ok(())
49 }
50 #[must_use]
52 pub fn new(rules: Vec<RefRule>) -> Self {
53 Self { rules }
54 }
55
56 pub(crate) fn has_fast_forward_rule(&self) -> bool {
58 self.rules.iter().any(|rule| rule.fast_forward_only)
59 }
60
61 pub(crate) fn validate(&self) -> Result<(), ServerError> {
64 for rule in &self.rules {
65 if RefPattern::parse(&rule.pattern.to_string()).as_ref() != Ok(&rule.pattern) {
66 return Err(ServerError::invalid_argument("invalid ref policy pattern"));
67 }
68 }
69 Ok(())
70 }
71
72 pub(crate) fn signer_allowed(&self, ref_name: &str, signer: Option<&[u8; 32]>) -> bool {
75 let name = rule_name(ref_name);
76 self.rules
77 .iter()
78 .filter(|rule| rule.pattern.matches(&name))
79 .filter_map(|rule| rule.allowed_signers.as_ref())
80 .all(|allowed| signer.is_some_and(|key| allowed.contains(key)))
81 }
82
83 pub(crate) fn fast_forward_only(&self, ref_name: &str) -> bool {
86 packmap_head(ref_name).is_none()
87 && self
88 .rules
89 .iter()
90 .any(|rule| rule.fast_forward_only && rule.pattern.matches(ref_name))
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97
98 fn rule(pattern: &str, signers: Option<&[[u8; 32]]>, ff: bool) -> RefRule {
99 RefRule {
100 pattern: RefPattern::parse(pattern).unwrap(),
101 allowed_signers: signers.map(|keys| keys.iter().copied().collect()),
102 fast_forward_only: ff,
103 }
104 }
105
106 #[test]
107 fn overlapping_signer_rules_intersect_and_packmaps_follow_their_head() {
108 let (a, b, c) = ([1; 32], [2; 32], [3; 32]);
109 let policy = RefPolicy::new(vec![
110 rule("refs/heads/*", Some(&[a, b]), false),
111 rule("refs/heads/main", Some(&[b, c]), true),
112 ]);
113 assert!(policy.validate().is_ok());
114 assert!(policy.signer_allowed("refs/heads/main", Some(&b)));
115 assert!(!policy.signer_allowed("refs/heads/main", Some(&a)));
116 assert!(!policy.signer_allowed("refs/heads/main", Some(&c)));
117 assert!(!policy.signer_allowed("refs/heads/main", None));
118 assert!(!policy.signer_allowed("refs/mkit/packmap/main", Some(&a)));
119 assert!(policy.signer_allowed("refs/mkit/packmap/main", Some(&b)));
120 assert!(policy.signer_allowed("refs/tags/v1", None));
121 assert!(policy.fast_forward_only("refs/heads/main"));
122 assert!(!policy.fast_forward_only("refs/mkit/packmap/main"));
123 assert!(!policy.fast_forward_only("refs/heads/dev"));
124 let ff_only = RefPolicy::new(vec![rule("refs/heads/main", None, true)]);
126 assert!(ff_only.signer_allowed("refs/heads/main", None));
127 }
128
129 #[test]
130 fn a_pattern_naming_a_packmap_ref_is_refused_at_startup() {
131 let policy = RefPolicy::new(vec![RefRule {
132 pattern: RefPattern::Exact("refs/mkit/packmap/main".into()),
133 allowed_signers: None,
134 fast_forward_only: true,
135 }]);
136 assert!(policy.validate().is_err());
137 }
138}