Skip to main content

rocketmq_auth/authorization/model/
environment.rs

1// Copyright 2023 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::net::IpAddr;
16use std::net::SocketAddr;
17use std::str::FromStr;
18
19use ipnetwork::IpNetwork;
20
21/// Authorization environment model.
22#[derive(Clone, Debug, Default, PartialEq, Eq)]
23pub struct Environment {
24    source_ips: Vec<String>,
25}
26
27impl Environment {
28    /// Create from a single IP string (returns None on empty input)
29    pub fn of(source_ip: &str) -> Option<Self> {
30        if source_ip.trim().is_empty() {
31            None
32        } else {
33            Some(Self {
34                source_ips: vec![source_ip.to_string()],
35            })
36        }
37    }
38
39    /// Create from a list of IP strings (returns None on empty list)
40    pub fn of_list<I, S>(source_ips: I) -> Option<Self>
41    where
42        I: IntoIterator<Item = S>,
43        S: Into<String>,
44    {
45        let v: Vec<String> = source_ips.into_iter().map(Into::into).collect();
46        if v.is_empty() {
47            None
48        } else {
49            Some(Self { source_ips: v })
50        }
51    }
52
53    /// Returns true if this environment matches `other` according to rules:
54    /// - if self.source_ips empty -> true
55    /// - if other's source_ips empty -> false
56    /// - `target = other.source_ips[0]`; check if any src pattern in self matches target
57    pub fn is_match(&self, other: &Environment) -> bool {
58        if self.source_ips.is_empty() {
59            return true;
60        }
61        if other.source_ips.is_empty() {
62            return false;
63        }
64        let target = &other.source_ips[0];
65        for pattern in &self.source_ips {
66            if source_ip_matches(pattern, target) {
67                return true;
68            }
69        }
70        false
71    }
72
73    pub fn source_ips(&self) -> &Vec<String> {
74        &self.source_ips
75    }
76
77    pub fn set_source_ips<I, S>(&mut self, ips: I)
78    where
79        I: IntoIterator<Item = S>,
80        S: Into<String>,
81    {
82        self.source_ips = ips.into_iter().map(Into::into).collect();
83    }
84}
85
86pub fn source_ip_matches(pattern: &str, target: &str) -> bool {
87    let pattern = pattern.trim();
88    let target = normalize_ip_text(target);
89
90    if target.is_empty() || pattern.is_empty() {
91        return false;
92    }
93
94    if pattern == "*" {
95        return true;
96    }
97
98    if pattern.contains('*') {
99        return wildcard_match(pattern, target.as_str());
100    }
101
102    // CIDR
103    if pattern.contains('/') {
104        if let (Ok(net), Ok(ip)) = (IpNetwork::from_str(pattern), IpAddr::from_str(target.as_str())) {
105            return net.contains(ip);
106        }
107        return false;
108    }
109
110    // Range a-b
111    if pattern.contains('-') {
112        let parts: Vec<&str> = pattern.splitn(2, '-').collect();
113        if parts.len() == 2 {
114            if let (Ok(start), Ok(end), Ok(ip)) = (
115                IpAddr::from_str(parts[0].trim()),
116                IpAddr::from_str(parts[1].trim()),
117                IpAddr::from_str(target.as_str()),
118            ) {
119                return ip_in_range(ip, start, end);
120            }
121        }
122        return false;
123    }
124
125    // exact match
126    target == pattern
127}
128
129fn normalize_ip_text(value: &str) -> String {
130    let value = value.trim();
131    if let Ok(socket_addr) = SocketAddr::from_str(value) {
132        return socket_addr.ip().to_string();
133    }
134    value.to_owned()
135}
136
137fn wildcard_match(pattern: &str, target: &str) -> bool {
138    let pattern_parts: Vec<&str> = pattern.split('.').collect();
139    let target_parts: Vec<&str> = target.split('.').collect();
140    if pattern_parts.len() != target_parts.len() {
141        return false;
142    }
143
144    pattern_parts
145        .iter()
146        .zip(target_parts.iter())
147        .all(|(pattern_part, target_part)| *pattern_part == "*" || pattern_part == target_part)
148}
149
150fn ip_to_u128(ip: IpAddr) -> u128 {
151    match ip {
152        IpAddr::V4(a) => u32::from(a) as u128,
153        IpAddr::V6(b) => {
154            let octets = b.octets();
155            let mut acc = 0u128;
156            for byte in &octets {
157                acc = (acc << 8) | (*byte as u128);
158            }
159            acc
160        }
161    }
162}
163
164fn ip_in_range(ip: IpAddr, start: IpAddr, end: IpAddr) -> bool {
165    match (ip, start, end) {
166        (IpAddr::V4(a), IpAddr::V4(b), IpAddr::V4(c)) => {
167            let a = u32::from(a);
168            let b = u32::from(b);
169            let c = u32::from(c);
170            b <= a && a <= c
171        }
172        (IpAddr::V6(a), IpAddr::V6(b), IpAddr::V6(c)) => {
173            let a = ip_to_u128(IpAddr::V6(a));
174            let b = ip_to_u128(IpAddr::V6(b));
175            let c = ip_to_u128(IpAddr::V6(c));
176            b <= a && a <= c
177        }
178        _ => false,
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_of_and_list() {
188        assert!(Environment::of("").is_none());
189        assert!(Environment::of("127.0.0.1").is_some());
190        assert!(Environment::of_list(Vec::<String>::new()).is_none());
191        assert!(Environment::of_list(vec!["1.2.3.4"]).is_some());
192    }
193
194    #[test]
195    fn test_exact_match() {
196        let a = Environment::of("1.2.3.4").unwrap();
197        let b = Environment::of("1.2.3.4").unwrap();
198        assert!(a.is_match(&b));
199        let c = Environment::of("1.2.3.5").unwrap();
200        assert!(!a.is_match(&c));
201    }
202
203    #[test]
204    fn test_cidr_match() {
205        let a = Environment::of_list(vec!["192.168.0.0/16"]).unwrap();
206        let b = Environment::of("192.168.1.5").unwrap();
207        assert!(a.is_match(&b));
208        let c = Environment::of("10.0.0.1").unwrap();
209        assert!(!a.is_match(&c));
210    }
211
212    #[test]
213    fn test_range_match() {
214        let a = Environment::of_list(vec!["192.168.0.1-192.168.0.10"]).unwrap();
215        assert!(a.is_match(&Environment::of("192.168.0.5").unwrap()));
216        assert!(!a.is_match(&Environment::of("192.168.0.11").unwrap()));
217    }
218
219    #[test]
220    fn test_java_plain_acl_wildcard_match() {
221        let a = Environment::of_list(vec!["192.168.0.*", "10.10.*.*"]).unwrap();
222        assert!(a.is_match(&Environment::of("192.168.0.5").unwrap()));
223        assert!(a.is_match(&Environment::of("10.10.1.2").unwrap()));
224        assert!(!a.is_match(&Environment::of("10.11.1.2").unwrap()));
225    }
226
227    #[test]
228    fn test_source_ip_match_normalizes_socket_address() {
229        assert!(source_ip_matches("127.0.0.1", "127.0.0.1:10911"));
230        assert!(source_ip_matches("*", "127.0.0.1:10911"));
231        assert!(!source_ip_matches("127.0.0.2", "127.0.0.1:10911"));
232    }
233
234    #[test]
235    fn test_empty_behavior() {
236        let a = Environment::default();
237        let b = Environment::of("1.2.3.4").unwrap();
238        assert!(a.is_match(&b));
239        let c = Environment::default();
240        assert!(!b.is_match(&c));
241    }
242}