Skip to main content

rocketmq_auth/acl/
white_list.rs

1// Copyright 2026 The RocketMQ Rust Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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}