Skip to main content

rocketmq_auth/authorization/context/
default_authorization_context.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
15//! Default authorization context implementation.
16//!
17//! This module provides the standard authorization context used throughout RocketMQ.
18
19use std::collections::HashMap;
20use std::fmt;
21
22use rocketmq_security_api::Action;
23
24use crate::authentication::enums::subject_type::SubjectType;
25use crate::authorization::model::resource::Resource;
26
27/// A type-erased subject wrapper that implements Debug and Clone.
28pub struct SubjectWrapper {
29    subject_key: String,
30    subject_type: SubjectType,
31}
32
33impl SubjectWrapper {
34    pub fn new(subject_key: String, subject_type: SubjectType) -> Self {
35        let subject_key = normalize_subject_key(subject_key, subject_type);
36        Self {
37            subject_key,
38            subject_type,
39        }
40    }
41
42    pub fn subject_key(&self) -> &str {
43        &self.subject_key
44    }
45
46    pub fn subject_type(&self) -> SubjectType {
47        self.subject_type
48    }
49}
50
51impl fmt::Debug for SubjectWrapper {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        f.debug_struct("SubjectWrapper")
54            .field("subject_key", &self.subject_key)
55            .field("subject_type", &self.subject_type)
56            .finish()
57    }
58}
59
60impl Clone for SubjectWrapper {
61    fn clone(&self) -> Self {
62        Self {
63            subject_key: self.subject_key.clone(),
64            subject_type: self.subject_type,
65        }
66    }
67}
68
69/// Default authorization context containing all information needed for authorization decisions.
70///
71/// This struct contains:
72/// - Subject: who is performing the action (user, role, service account)
73/// - Resource: what is being accessed (topic, group, cluster)
74/// - Actions: what operations are being performed (PUB, SUB, CREATE, etc.)
75/// - Source IP: where the request originates from
76/// - Channel ID: unique identifier for the communication channel
77/// - RPC code: the RocketMQ request code
78/// - Extended info: additional context-specific metadata
79#[derive(Debug, Clone, Default)]
80pub struct DefaultAuthorizationContext {
81    /// The subject performing the operation (user, role, etc.)
82    subject: Option<SubjectWrapper>,
83
84    /// The resource being accessed (topic, group, etc.)
85    resource: Option<Resource>,
86
87    /// List of actions being performed on the resource
88    actions: Vec<Action>,
89
90    /// Source IP address of the request
91    source_ip: Option<String>,
92
93    /// Unique identifier for the communication channel (inherited from base context)
94    channel_id: Option<String>,
95
96    /// RocketMQ RPC request code (for protocol-specific authorization)
97    rpc_code: Option<String>,
98
99    /// Extended information for custom authorization logic
100    ext_info: HashMap<String, String>,
101}
102
103impl DefaultAuthorizationContext {
104    /// Create a new authorization context with basic information.
105    ///
106    /// # Arguments
107    /// * `subject_key` - The subject key performing the operation
108    /// * `subject_type` - The type of the subject
109    /// * `resource` - The resource being accessed
110    /// * `action` - The action being performed
111    /// * `source_ip` - Source IP address of the request
112    pub fn of(
113        subject_key: impl Into<String>,
114        subject_type: SubjectType,
115        resource: Resource,
116        action: Action,
117        source_ip: impl Into<String>,
118    ) -> Self {
119        Self {
120            subject: Some(SubjectWrapper::new(subject_key.into(), subject_type)),
121            resource: Some(resource),
122            actions: vec![action],
123            source_ip: Some(source_ip.into()),
124            channel_id: None,
125            rpc_code: None,
126            ext_info: HashMap::new(),
127        }
128    }
129
130    /// Create a new authorization context with multiple actions.
131    ///
132    /// # Arguments
133    /// * `subject_key` - The subject key performing the operation
134    /// * `subject_type` - The type of the subject
135    /// * `resource` - The resource being accessed
136    /// * `actions` - List of actions being performed
137    /// * `source_ip` - Source IP address of the request
138    pub fn of_multi_actions(
139        subject_key: impl Into<String>,
140        subject_type: SubjectType,
141        resource: Resource,
142        actions: Vec<Action>,
143        source_ip: impl Into<String>,
144    ) -> Self {
145        Self {
146            subject: Some(SubjectWrapper::new(subject_key.into(), subject_type)),
147            resource: Some(resource),
148            actions,
149            source_ip: Some(source_ip.into()),
150            channel_id: None,
151            rpc_code: None,
152            ext_info: HashMap::new(),
153        }
154    }
155
156    /// Create a builder for constructing authorization contexts.
157    pub fn builder() -> DefaultAuthorizationContextBuilder {
158        DefaultAuthorizationContextBuilder::default()
159    }
160
161    /// Get the subject key (unique identifier for the subject).
162    pub fn subject_key(&self) -> Option<&str> {
163        self.subject.as_ref().map(|s| s.subject_key())
164    }
165
166    /// Get the subject type.
167    pub fn subject_type(&self) -> Option<SubjectType> {
168        self.subject.as_ref().map(|s| s.subject_type())
169    }
170
171    /// Get the resource key (unique identifier for the resource).
172    pub fn resource_key(&self) -> Option<String> {
173        self.resource.as_ref().and_then(|r| r.resource_key())
174    }
175
176    // Getters
177    pub fn subject(&self) -> Option<&SubjectWrapper> {
178        self.subject.as_ref()
179    }
180
181    pub fn resource(&self) -> Option<&Resource> {
182        self.resource.as_ref()
183    }
184
185    pub fn actions(&self) -> &[Action] {
186        &self.actions
187    }
188
189    pub fn source_ip(&self) -> Option<&str> {
190        self.source_ip.as_deref()
191    }
192
193    pub fn channel_id(&self) -> Option<&str> {
194        self.channel_id.as_deref()
195    }
196
197    pub fn rpc_code(&self) -> Option<&str> {
198        self.rpc_code.as_deref()
199    }
200
201    pub fn ext_info(&self) -> &HashMap<String, String> {
202        &self.ext_info
203    }
204
205    // Setters
206    pub fn set_subject(&mut self, subject_key: impl Into<String>, subject_type: SubjectType) {
207        self.subject = Some(SubjectWrapper::new(subject_key.into(), subject_type));
208    }
209
210    pub fn set_resource(&mut self, resource: Resource) {
211        self.resource = Some(resource);
212    }
213
214    pub fn set_actions(&mut self, actions: Vec<Action>) {
215        self.actions = actions;
216    }
217
218    pub fn set_source_ip(&mut self, source_ip: impl Into<String>) {
219        self.source_ip = Some(source_ip.into());
220    }
221
222    pub fn set_channel_id(&mut self, channel_id: impl Into<String>) {
223        self.channel_id = Some(channel_id.into());
224    }
225
226    pub fn set_rpc_code(&mut self, rpc_code: impl Into<String>) {
227        self.rpc_code = Some(rpc_code.into());
228    }
229
230    pub fn set_ext_info(&mut self, ext_info: HashMap<String, String>) {
231        self.ext_info = ext_info;
232    }
233
234    pub fn add_ext_info(&mut self, key: impl Into<String>, value: impl Into<String>) {
235        self.ext_info.insert(key.into(), value.into());
236    }
237}
238
239fn normalize_subject_key(subject_key: String, subject_type: SubjectType) -> String {
240    if let Some((_, raw_name)) = subject_key.split_once(':') {
241        return format!("{}:{}", subject_type.name(), raw_name);
242    }
243    format!("{}:{}", subject_type.name(), subject_key)
244}
245
246/// Builder for constructing `DefaultAuthorizationContext` instances.
247#[derive(Default)]
248pub struct DefaultAuthorizationContextBuilder {
249    subject: Option<SubjectWrapper>,
250    resource: Option<Resource>,
251    actions: Vec<Action>,
252    source_ip: Option<String>,
253    channel_id: Option<String>,
254    rpc_code: Option<String>,
255    ext_info: HashMap<String, String>,
256}
257
258impl DefaultAuthorizationContextBuilder {
259    pub fn subject(mut self, subject_key: impl Into<String>, subject_type: SubjectType) -> Self {
260        self.subject = Some(SubjectWrapper::new(subject_key.into(), subject_type));
261        self
262    }
263
264    pub fn resource(mut self, resource: Resource) -> Self {
265        self.resource = Some(resource);
266        self
267    }
268
269    pub fn action(mut self, action: Action) -> Self {
270        self.actions.push(action);
271        self
272    }
273
274    pub fn actions(mut self, actions: Vec<Action>) -> Self {
275        self.actions = actions;
276        self
277    }
278
279    pub fn source_ip(mut self, source_ip: impl Into<String>) -> Self {
280        self.source_ip = Some(source_ip.into());
281        self
282    }
283
284    pub fn channel_id(mut self, channel_id: impl Into<String>) -> Self {
285        self.channel_id = Some(channel_id.into());
286        self
287    }
288
289    pub fn rpc_code(mut self, rpc_code: impl Into<String>) -> Self {
290        self.rpc_code = Some(rpc_code.into());
291        self
292    }
293
294    pub fn ext_info(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
295        self.ext_info.insert(key.into(), value.into());
296        self
297    }
298
299    pub fn build(self) -> DefaultAuthorizationContext {
300        DefaultAuthorizationContext {
301            subject: self.subject,
302            resource: self.resource,
303            actions: self.actions,
304            source_ip: self.source_ip,
305            channel_id: self.channel_id,
306            rpc_code: self.rpc_code,
307            ext_info: self.ext_info,
308        }
309    }
310}
311
312#[cfg(test)]
313mod tests {
314    use rocketmq_security_api::Action;
315
316    use super::*;
317
318    #[test]
319    fn test_context_creation_with_of() {
320        let subject_key = "user:alice";
321        let subject_type = SubjectType::User;
322        let resource = Resource::of_topic("test-topic");
323        let context = DefaultAuthorizationContext::of(subject_key, subject_type, resource, Action::Pub, "192.168.1.1");
324
325        assert_eq!(context.subject_key(), Some("User:alice"));
326        assert_eq!(context.subject_type(), Some(SubjectType::User));
327        assert_eq!(context.actions().len(), 1);
328        assert_eq!(context.actions()[0], Action::Pub);
329        assert_eq!(context.source_ip(), Some("192.168.1.1"));
330    }
331
332    #[test]
333    fn test_context_creation_with_multi_actions() {
334        let subject_key = "user:bob";
335        let subject_type = SubjectType::User;
336        let resource = Resource::of_topic("test-topic");
337        let actions = vec![Action::Pub, Action::Sub];
338        let context =
339            DefaultAuthorizationContext::of_multi_actions(subject_key, subject_type, resource, actions, "10.0.0.1");
340
341        assert_eq!(context.actions().len(), 2);
342        assert!(context.actions().contains(&Action::Pub));
343        assert!(context.actions().contains(&Action::Sub));
344    }
345
346    #[test]
347    fn test_context_builder() {
348        let subject_key = "user:charlie";
349        let subject_type = SubjectType::User;
350        let resource = Resource::of_group("test-group".to_string());
351
352        let context = DefaultAuthorizationContext::builder()
353            .subject(subject_key, subject_type)
354            .resource(resource)
355            .action(Action::Create)
356            .source_ip("172.16.0.1")
357            .rpc_code("310")
358            .ext_info("region", "us-west")
359            .build();
360
361        assert_eq!(context.subject_key(), Some("User:charlie"));
362        assert_eq!(context.rpc_code(), Some("310"));
363        assert_eq!(context.ext_info().get("region"), Some(&"us-west".to_string()));
364    }
365
366    #[test]
367    fn test_context_setters() {
368        let mut context = DefaultAuthorizationContext::default();
369
370        context.set_subject("user:dave", SubjectType::User);
371        context.set_resource(Resource::of_topic("new-topic"));
372        context.set_actions(vec![Action::Delete]);
373        context.set_source_ip("203.0.113.1");
374        context.set_rpc_code("500");
375        context.add_ext_info("environment", "production");
376
377        assert_eq!(context.subject_key(), Some("User:dave"));
378        assert_eq!(context.actions().len(), 1);
379        assert_eq!(context.source_ip(), Some("203.0.113.1"));
380        assert_eq!(context.rpc_code(), Some("500"));
381        assert_eq!(context.ext_info().get("environment"), Some(&"production".to_string()));
382    }
383
384    #[test]
385    fn test_resource_key_generation() {
386        let subject_key = "user:eve";
387        let subject_type = SubjectType::User;
388        let resource = Resource::of_topic("my-topic");
389        let context = DefaultAuthorizationContext::of(subject_key, subject_type, resource, Action::Get, "10.10.10.10");
390
391        let resource_key = context.resource_key();
392        assert!(resource_key.is_some());
393        let key = resource_key.unwrap();
394        assert!(key.contains("Topic"));
395        assert!(key.contains("my-topic"));
396    }
397
398    #[test]
399    fn test_subject_wrapper() {
400        let wrapper = SubjectWrapper::new("user:test".to_string(), SubjectType::User);
401        assert_eq!(wrapper.subject_key(), "User:test");
402        assert_eq!(wrapper.subject_type(), SubjectType::User);
403
404        // Test clone
405        let cloned = wrapper.clone();
406        assert_eq!(cloned.subject_key(), wrapper.subject_key());
407        assert_eq!(cloned.subject_type(), wrapper.subject_type());
408
409        // Test debug
410        let debug_str = format!("{:?}", wrapper);
411        assert!(debug_str.contains("User:test"));
412    }
413}