mkit_server/policy/
namespace.rs1use std::collections::BTreeSet;
2
3use mkit_core::repo_identity::Namespace;
4
5#[derive(Debug, Clone, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum NamespacePolicy {
10 Allowlist(BTreeSet<Namespace>),
12 Any {
15 unsafe_without_admission: bool,
17 },
18}
19
20impl Default for NamespacePolicy {
21 fn default() -> Self {
22 Self::Allowlist(BTreeSet::new())
23 }
24}
25
26pub fn parse_namespace_allowlist(text: &str) -> Result<BTreeSet<Namespace>, String> {
38 let mut namespaces = BTreeSet::new();
39 for (line, raw) in text.lines().enumerate() {
40 let uncommented = raw.split('#').next().unwrap_or_default();
41 for entry in uncommented.split(',') {
42 let entry = entry.trim();
43 if entry.is_empty() {
44 continue;
45 }
46 let namespace =
47 Namespace::parse(entry).map_err(|e| format!("line {}: {e}", line + 1))?;
48 if !namespaces.insert(namespace) {
49 return Err(format!("line {}: duplicate namespace {entry}", line + 1));
50 }
51 }
52 }
53 if namespaces.is_empty() {
54 return Err("namespace allowlist contains no namespaces".to_owned());
55 }
56 Ok(namespaces)
57}
58
59#[cfg(test)]
60mod tests {
61 use super::*;
62
63 fn ns(byte: u8) -> String {
64 format!("ed25519-{}", "a".repeat(62) + &format!("{byte:02x}"))
65 }
66
67 #[test]
68 fn parses_newlines_commas_comments_and_blanks() {
69 let text = format!(
70 "# deployment owners\n\n {}\n{},{} # the third one\n",
71 ns(1),
72 ns(2),
73 ns(3)
74 );
75 let set = parse_namespace_allowlist(&text).unwrap();
76 assert_eq!(set.len(), 3);
77 for byte in 1..=3 {
78 assert!(set.contains(&Namespace::parse(&ns(byte)).unwrap()));
79 }
80 }
81
82 #[test]
83 fn tolerates_crlf_and_trailing_commas() {
84 let set = parse_namespace_allowlist(&format!("{},\r\n{}\r\n", ns(1), ns(2))).unwrap();
85 assert_eq!(set.len(), 2);
86 }
87
88 #[test]
89 fn accepts_0x_namespaces() {
90 let address = format!("0x{}", "b".repeat(40));
91 let set = parse_namespace_allowlist(&format!("{},{address}", ns(1))).unwrap();
92 assert!(set.contains(&Namespace::Address([0xbb; 20])));
93 }
94
95 #[test]
96 fn refuses_uppercase_bare_and_garbage() {
97 for bad in [
98 format!("ed25519-{}", "A".repeat(64)),
99 "default".to_owned(),
100 format!("0X{}", "b".repeat(40)),
101 "not a namespace".to_owned(),
102 format!("ed25519-{}/repo", "a".repeat(64)),
103 ] {
104 let err = parse_namespace_allowlist(&format!("{}\n{bad}", ns(1))).unwrap_err();
105 assert!(err.starts_with("line 2:"), "{bad}: {err}");
106 }
107 }
108
109 #[test]
110 fn refuses_duplicates() {
111 let err = parse_namespace_allowlist(&format!("{0},{0}", ns(1))).unwrap_err();
112 assert_eq!(err, format!("line 1: duplicate namespace {}", ns(1)));
113 }
114
115 #[test]
116 fn refuses_an_empty_allowlist() {
117 for text in ["", "\n", "# only a comment\n", " , \n"] {
118 assert_eq!(
119 parse_namespace_allowlist(text).unwrap_err(),
120 "namespace allowlist contains no namespaces"
121 );
122 }
123 }
124}