zeroclaw_channels/
allowlist.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Match {
22 Sensitive,
24 CaseInsensitive,
26}
27
28#[must_use]
35pub fn is_user_allowed(allowed: &[String], user: &str, mode: Match) -> bool {
36 if allowed.iter().any(|u| u == "*") {
37 return true;
38 }
39 match mode {
40 Match::Sensitive => allowed.iter().any(|u| u == user),
41 Match::CaseInsensitive => allowed.iter().any(|u| u.eq_ignore_ascii_case(user)),
42 }
43}
44
45#[must_use]
58pub fn is_user_allowed_by(
59 allowed: &[String],
60 user: &str,
61 match_fn: impl Fn(&str, &str) -> bool,
62) -> bool {
63 if allowed.iter().any(|u| u == "*") {
64 return true;
65 }
66 allowed.iter().any(|entry| match_fn(entry, user))
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn wildcard_allows_anyone() {
75 let list = vec!["*".to_string()];
76 assert!(is_user_allowed(&list, "alice", Match::Sensitive));
77 assert!(is_user_allowed(&list, "ALICE", Match::Sensitive));
78 }
79
80 #[test]
81 fn empty_list_denies_everyone() {
82 assert!(!is_user_allowed(&[], "alice", Match::Sensitive));
83 assert!(!is_user_allowed(&[], "alice", Match::CaseInsensitive));
84 }
85
86 #[test]
87 fn exact_match_case_sensitive() {
88 let list = vec!["alice".to_string()];
89 assert!(is_user_allowed(&list, "alice", Match::Sensitive));
90 assert!(!is_user_allowed(&list, "Alice", Match::Sensitive));
91 }
92
93 #[test]
94 fn exact_match_case_insensitive() {
95 let list = vec!["Alice".to_string()];
96 assert!(is_user_allowed(&list, "alice", Match::CaseInsensitive));
97 assert!(is_user_allowed(&list, "ALICE", Match::CaseInsensitive));
98 }
99
100 #[test]
103 fn by_empty_denies_and_wildcard_admits() {
104 let eq = |e: &str, u: &str| e == u;
105 assert!(!is_user_allowed_by(&[], "alice", eq));
106 assert!(is_user_allowed_by(&["*".to_string()], "anyone", eq));
107 }
108
109 #[test]
110 fn by_email_domain_class() {
111 let matcher = |allowed: &str, email: &str| -> bool {
114 let email_lower = email.to_lowercase();
115 if allowed.starts_with('@') {
116 email_lower.ends_with(&allowed.to_lowercase())
117 } else if allowed.contains('@') {
118 allowed.eq_ignore_ascii_case(email)
119 } else {
120 email_lower.ends_with(&format!("@{}", allowed.to_lowercase()))
121 }
122 };
123 let list = vec!["@example.com".to_string(), "boss@corp.io".to_string()];
124 assert!(is_user_allowed_by(&list, "anyone@Example.com", matcher));
125 assert!(is_user_allowed_by(&list, "BOSS@corp.io", matcher));
126 assert!(!is_user_allowed_by(&list, "user@evil.com", matcher));
127 }
128
129 #[test]
130 fn by_phone_e164_normalized() {
131 let norm = |s: &str| -> String {
133 let mut out = String::new();
134 let mut chars = s.chars();
135 if let Some('+') = chars.clone().next() {
136 out.push('+');
137 chars.next();
138 }
139 out.extend(chars.filter(|c| c.is_ascii_digit()));
140 out
141 };
142 let matcher = |entry: &str, phone: &str| norm(entry) == norm(phone);
143 let list = vec!["+1-555-0100".to_string()];
144 assert!(is_user_allowed_by(&list, "+1 555 0100", matcher));
145 assert!(!is_user_allowed_by(&list, "+15550101", matcher));
146 }
147}