rocketmq_auth/acl/
white_list.rs1use std::collections::HashMap;
16
17use crate::authorization::model::environment::source_ip_matches;
18use crate::migration::alc::acl_config::AclConfig;
19
20#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct WhiteList {
22 global_patterns: Vec<String>,
23 account_patterns: HashMap<String, Vec<String>>,
24}
25
26impl WhiteList {
27 pub fn from_acl_config(acl_config: &AclConfig) -> Self {
28 let global_patterns = acl_config
29 .global_white_addrs()
30 .unwrap_or_default()
31 .iter()
32 .flat_map(|value| normalize_patterns(value.as_str()))
33 .collect();
34
35 let account_patterns = acl_config
36 .plain_access_configs()
37 .unwrap_or_default()
38 .iter()
39 .filter_map(|account| {
40 let access_key = account.access_key()?.as_str().trim();
41 let white_remote_address = account.white_remote_address()?.as_str();
42 if access_key.is_empty() {
43 return None;
44 }
45
46 let patterns = normalize_patterns(white_remote_address);
47 if patterns.is_empty() {
48 return None;
49 }
50 Some((access_key.to_owned(), patterns))
51 })
52 .collect();
53
54 Self {
55 global_patterns,
56 account_patterns,
57 }
58 }
59
60 pub fn from_global_patterns<I, S>(patterns: I) -> Self
61 where
62 I: IntoIterator<Item = S>,
63 S: AsRef<str>,
64 {
65 Self {
66 global_patterns: patterns
67 .into_iter()
68 .flat_map(|pattern| normalize_patterns(pattern.as_ref()))
69 .collect(),
70 account_patterns: HashMap::new(),
71 }
72 }
73
74 pub fn with_global_patterns<I, S>(&self, patterns: I) -> Self
75 where
76 I: IntoIterator<Item = S>,
77 S: AsRef<str>,
78 {
79 Self {
80 global_patterns: patterns
81 .into_iter()
82 .flat_map(|pattern| normalize_patterns(pattern.as_ref()))
83 .collect(),
84 account_patterns: self.account_patterns.clone(),
85 }
86 }
87
88 pub fn matches(&self, access_key: Option<&str>, source_ip: Option<&str>) -> bool {
89 let Some(source_ip) = valid_source_ip(source_ip) else {
90 return false;
91 };
92
93 if self
94 .global_patterns
95 .iter()
96 .any(|pattern| source_ip_matches(pattern, source_ip))
97 {
98 return true;
99 }
100
101 let Some(access_key) = access_key.map(str::trim).filter(|value| !value.is_empty()) else {
102 return false;
103 };
104
105 self.account_patterns
106 .get(access_key)
107 .is_some_and(|patterns| patterns.iter().any(|pattern| source_ip_matches(pattern, source_ip)))
108 }
109}
110
111fn normalize_patterns(value: &str) -> Vec<String> {
112 value
113 .split(';')
114 .map(str::trim)
115 .filter(|pattern| !pattern.is_empty())
116 .flat_map(expand_brace_pattern)
117 .collect()
118}
119
120fn expand_brace_pattern(pattern: &str) -> Vec<String> {
121 let Some(open) = pattern.find('{') else {
122 return vec![pattern.to_owned()];
123 };
124 let Some(close_offset) = pattern[open + 1..].find('}') else {
125 return vec![pattern.to_owned()];
126 };
127 let close = open + 1 + close_offset;
128 let prefix = &pattern[..open];
129 let suffix = &pattern[close + 1..];
130 let choices = &pattern[open + 1..close];
131 let expanded: Vec<String> = choices
132 .split(',')
133 .map(str::trim)
134 .filter(|choice| !choice.is_empty())
135 .map(|choice| format!("{prefix}{choice}{suffix}"))
136 .collect();
137
138 if expanded.is_empty() {
139 vec![pattern.to_owned()]
140 } else {
141 expanded
142 }
143}
144
145fn valid_source_ip(source_ip: Option<&str>) -> Option<&str> {
146 source_ip
147 .map(str::trim)
148 .filter(|value| !value.is_empty() && !value.eq_ignore_ascii_case("unknown"))
149}
150
151#[cfg(test)]
152mod tests {
153 use cheetah_string::CheetahString;
154
155 use super::*;
156 use crate::migration::alc::acl_config::AclConfig;
157 use crate::migration::alc::plain_access_config::PlainAccessConfig;
158
159 #[test]
160 fn white_list_matches_global_and_account_patterns() {
161 let mut account = PlainAccessConfig::default();
162 account.set_access_key(CheetahString::from_static_str("alice"));
163 account.set_white_remote_address(CheetahString::from_static_str("192.168.0.*;172.16.1.1"));
164
165 let mut config = AclConfig::new();
166 config.set_global_white_addrs(vec![CheetahString::from_static_str("10.10.*.*")]);
167 config.set_plain_access_configs(vec![account]);
168
169 let white_list = WhiteList::from_acl_config(&config);
170
171 assert!(white_list.matches(None, Some("10.10.1.2")));
172 assert!(white_list.matches(Some("alice"), Some("192.168.0.7")));
173 assert!(white_list.matches(Some("alice"), Some("172.16.1.1")));
174 assert!(!white_list.matches(Some("alice"), Some("192.168.1.7")));
175 assert!(!white_list.matches(Some("bob"), Some("192.168.0.7")));
176 assert!(!white_list.matches(Some("alice"), None));
177 assert!(!white_list.matches(Some("alice"), Some("unknown")));
178 }
179
180 #[test]
181 fn white_list_expands_java_brace_address_patterns() {
182 let white_list = WhiteList::from_global_patterns(vec!["1.1.1.{1,2}", "2001:db8::{1,2}"]);
183
184 assert!(white_list.matches(None, Some("1.1.1.1")));
185 assert!(white_list.matches(None, Some("1.1.1.2")));
186 assert!(!white_list.matches(None, Some("1.1.1.3")));
187 assert!(white_list.matches(None, Some("2001:db8::1")));
188 assert!(white_list.matches(None, Some("2001:db8::2")));
189 assert!(!white_list.matches(None, Some("2001:db8::3")));
190 }
191
192 #[test]
193 fn replacing_global_patterns_preserves_account_patterns() {
194 let mut account = PlainAccessConfig::default();
195 account.set_access_key(CheetahString::from_static_str("alice"));
196 account.set_white_remote_address(CheetahString::from_static_str("192.168.0.*"));
197
198 let mut config = AclConfig::new();
199 config.set_global_white_addrs(vec![CheetahString::from_static_str("10.10.*.*")]);
200 config.set_plain_access_configs(vec![account]);
201 let white_list = WhiteList::from_acl_config(&config);
202
203 let white_list = white_list.with_global_patterns(vec!["172.16.*.*"]);
204
205 assert!(!white_list.matches(None, Some("10.10.1.2")));
206 assert!(white_list.matches(None, Some("172.16.1.2")));
207 assert!(white_list.matches(Some("alice"), Some("192.168.0.7")));
208 }
209}