Skip to main content

rocketmq_auth/authorization/model/
resource.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::fmt;
16
17use rocketmq_model::common::mix_all;
18use rocketmq_protocol::protocol::namespace_util::NamespaceUtil;
19use rocketmq_security_api::ResourcePattern;
20use rocketmq_security_api::ResourceType;
21
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct Resource {
24    pub resource_type: ResourceType,
25    pub resource_name: Option<String>,
26    pub resource_pattern: ResourcePattern,
27}
28
29impl Resource {
30    pub fn of_cluster(cluster_name: &str) -> Self {
31        Self::of(
32            ResourceType::Cluster,
33            Some(cluster_name.to_string()),
34            ResourcePattern::Literal,
35        )
36    }
37
38    pub fn of_topic(topic_name: &str) -> Self {
39        Self::of(
40            ResourceType::Topic,
41            Some(topic_name.to_string()),
42            ResourcePattern::Literal,
43        )
44    }
45
46    pub fn of_group(mut group_name: String) -> Self {
47        if NamespaceUtil::is_retry_topic(&group_name) {
48            let prefix = mix_all::RETRY_GROUP_TOPIC_PREFIX;
49            if group_name.starts_with(prefix) {
50                group_name = group_name[prefix.len()..].to_string();
51            }
52        } else if NamespaceUtil::is_dlq_topic(&group_name) {
53            let prefix = mix_all::DLQ_GROUP_TOPIC_PREFIX;
54            if group_name.starts_with(prefix) {
55                group_name = group_name[prefix.len()..].to_string();
56            }
57        }
58        Self::of(ResourceType::Group, Some(group_name), ResourcePattern::Literal)
59    }
60
61    pub fn of(resource_type: ResourceType, resource_name: Option<String>, resource_pattern: ResourcePattern) -> Self {
62        Resource {
63            resource_type,
64            resource_name,
65            resource_pattern,
66        }
67    }
68
69    pub fn of_list<I, S>(resource_keys: I) -> Option<Vec<Self>>
70    where
71        I: IntoIterator<Item = S>,
72        S: AsRef<str>,
73    {
74        let mut out = Vec::new();
75        for s in resource_keys.into_iter() {
76            let sref = s.as_ref();
77            if let Some(r) = Resource::of_str(sref) {
78                out.push(r);
79            }
80        }
81        if out.is_empty() {
82            None
83        } else {
84            Some(out)
85        }
86    }
87
88    pub fn of_str(resource_key: &str) -> Option<Self> {
89        let key = resource_key.trim();
90        if key.is_empty() {
91            return None;
92        }
93        if key == "*" {
94            return Some(Self::of(ResourceType::Any, None, ResourcePattern::Any));
95        }
96        let (t, name) = match key.split_once(':') {
97            Some((a, b)) => (a, b),
98            None => (key, ""),
99        };
100        let resource_type = ResourceType::get_by_name(t)?;
101        let mut resource_name = if name.is_empty() { None } else { Some(name.to_string()) };
102        let mut resource_pattern = ResourcePattern::Literal;
103
104        if let Some(ref rn) = resource_name {
105            if rn == "*" {
106                resource_name = None;
107                resource_pattern = ResourcePattern::Any;
108            } else if rn.ends_with('*') {
109                let trimmed = rn.trim_end_matches('*').to_string();
110                resource_name = Some(trimmed);
111                resource_pattern = ResourcePattern::Prefixed;
112            }
113        }
114
115        Some(Self::of(resource_type, resource_name, resource_pattern))
116    }
117
118    /// Serialized resource key for matching and storage.
119    pub fn resource_key(&self) -> Option<String> {
120        if self.resource_type == ResourceType::Any {
121            return Some("*".to_string());
122        }
123        let type_name = self.resource_type.name();
124        match self.resource_pattern {
125            ResourcePattern::Any => Some(format!("{}:{}", type_name, "*")),
126            ResourcePattern::Literal => Some(format!("{}:{}", type_name, self.resource_name.as_deref().unwrap_or(""))),
127            ResourcePattern::Prefixed => Some(format!(
128                "{}:{}*",
129                type_name,
130                self.resource_name.as_deref().unwrap_or("")
131            )),
132        }
133    }
134
135    /// Check whether `self` (the rule) matches the given `resource`.
136    pub fn is_match(&self, resource: &Resource) -> bool {
137        if self.resource_type == ResourceType::Any {
138            return true;
139        }
140        if self.resource_type != resource.resource_type {
141            return false;
142        }
143        match self.resource_pattern {
144            ResourcePattern::Any => true,
145            ResourcePattern::Literal => resource.resource_name.as_deref() == self.resource_name.as_deref(),
146            ResourcePattern::Prefixed => match (&resource.resource_name, &self.resource_name) {
147                (Some(target), Some(prefix)) => target.starts_with(prefix),
148                _ => false,
149            },
150        }
151    }
152
153    // getters/setters
154    pub fn resource_type(&self) -> ResourceType {
155        self.resource_type
156    }
157
158    pub fn resource_name(&self) -> Option<&str> {
159        self.resource_name.as_deref()
160    }
161
162    pub fn resource_pattern(&self) -> ResourcePattern {
163        self.resource_pattern
164    }
165
166    pub fn set_resource_type(&mut self, t: ResourceType) {
167        self.resource_type = t;
168    }
169
170    pub fn set_resource_name<S: Into<String>>(&mut self, name: Option<S>) {
171        self.resource_name = name.map(Into::into);
172    }
173
174    pub fn set_resource_pattern(&mut self, p: ResourcePattern) {
175        self.resource_pattern = p;
176    }
177}
178
179impl fmt::Display for Resource {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self.resource_key() {
182            Some(s) => write!(f, "{}", s),
183            None => write!(f, ""),
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_of_and_serialization() {
194        let r = Resource::of(ResourceType::Topic, Some("foo".to_string()), ResourcePattern::Literal);
195        assert_eq!(r.resource_key(), Some("Topic:foo".to_string()));
196
197        let any = Resource::of_str("*").unwrap();
198        assert_eq!(any.resource_key(), Some("*".to_string()));
199    }
200
201    #[test]
202    fn test_of_parsing_patterns() {
203        assert_eq!(
204            Resource::of_str("Topic:*").unwrap().resource_pattern,
205            ResourcePattern::Any
206        );
207        assert_eq!(
208            Resource::of_str("Topic:pref*").unwrap().resource_pattern,
209            ResourcePattern::Prefixed
210        );
211        assert_eq!(
212            Resource::of_str("Topic:foo").unwrap().resource_pattern,
213            ResourcePattern::Literal
214        );
215    }
216
217    #[test]
218    fn test_group_retry_strip() {
219        let g = format!("{}{}", mix_all::RETRY_GROUP_TOPIC_PREFIX, "mygroup");
220        let r = Resource::of_group(g);
221        assert_eq!(r.resource_name(), Some("mygroup"));
222    }
223
224    #[test]
225    fn test_group_dlq_strip() {
226        let g = format!("{}{}", mix_all::DLQ_GROUP_TOPIC_PREFIX, "mygroup");
227        let r = Resource::of_group(g);
228        assert_eq!(r.resource_name(), Some("mygroup"));
229    }
230
231    #[test]
232    fn test_is_match() {
233        let rule_any = Resource::of(ResourceType::Any, None, ResourcePattern::Any);
234        let res = Resource::of(ResourceType::Topic, Some("a".to_string()), ResourcePattern::Literal);
235        assert!(rule_any.is_match(&res));
236
237        let rule_pref = Resource::of(ResourceType::Topic, Some("pre".to_string()), ResourcePattern::Prefixed);
238        let res2 = Resource::of(
239            ResourceType::Topic,
240            Some("prefix_val".to_string()),
241            ResourcePattern::Literal,
242        );
243        assert!(rule_pref.is_match(&res2));
244
245        let rule_lit = Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal);
246        assert!(rule_lit.is_match(&Resource::of(
247            ResourceType::Topic,
248            Some("t1".to_string()),
249            ResourcePattern::Literal
250        )));
251        assert!(!rule_lit.is_match(&Resource::of(
252            ResourceType::Topic,
253            Some("t2".to_string()),
254            ResourcePattern::Literal
255        )));
256    }
257
258    #[test]
259    fn test_of_list() {
260        let keys = vec!["Topic:foo".to_string(), "Topic:bar".to_string()];
261        let v = Resource::of_list(keys).unwrap();
262        assert_eq!(v.len(), 2);
263
264        let empty: Vec<String> = vec![];
265        assert!(Resource::of_list(empty).is_none());
266    }
267}