Skip to main content

rocketmq_auth/authorization/chain/
acl_authorization_handler.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//! ACL-based authorization handler implementation.
16//!
17//! This module implements Access Control List (ACL) authorization as part of the
18//! authorization chain. It evaluates incoming requests against stored ACL policies
19//! and makes allow/deny decisions based on:
20//! - Resource matching (topic, group, cluster)
21//! - Action matching (PUB, SUB, CREATE, etc.)
22//! - Environment matching (source IP, etc.)
23//! - Policy priority (CUSTOM > DEFAULT, DENY > ALLOW, LITERAL > PREFIXED)
24
25use std::cmp::Ordering;
26use std::future::Future;
27use std::pin::Pin;
28use std::sync::Arc;
29
30use rocketmq_security_api::AuthorizationDecision;
31use rocketmq_security_api::AuthorizationDenial;
32use rocketmq_security_api::ResourcePattern;
33use rocketmq_security_api::ResourceType;
34
35use super::handler::AuthorizationHandler;
36use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
37use crate::authorization::enums::decision::Decision;
38use crate::authorization::enums::policy_type::PolicyType;
39#[cfg(test)]
40use crate::authorization::metadata_provider::AuthorizationMetadataProvider;
41use crate::authorization::model::acl::Acl;
42use crate::authorization::model::environment::Environment;
43use crate::authorization::model::policy::Policy;
44use crate::authorization::model::policy_entry::PolicyEntry;
45use crate::AclMetadataRead;
46use crate::AuthServiceError;
47use crate::AuthServiceResult;
48use crate::SubjectKey;
49
50/// ACL Authorization Handler.
51///
52/// This handler performs ACL-based authorization by:
53/// 1. Loading the subject's ACL from the metadata provider
54/// 2. Matching policy entries against the request context
55/// 3. Evaluating the decision (ALLOW/DENY) based on priority
56///
57/// # Priority Rules
58///
59/// Policies are evaluated in this priority order:
60/// 1. **Resource Type**: Specific types (Topic, Group) > ANY
61/// 2. **Resource Pattern**: LITERAL > PREFIXED > ANY
62/// 3. **Resource Name Length**: Longer prefixes > shorter (for PREFIXED pattern)
63/// 4. **Decision**: DENY > ALLOW (deny takes precedence)
64pub struct AclAuthorizationHandler<P: AclMetadataRead + ?Sized> {
65    /// Provider for fetching ACL metadata
66    metadata_provider: Arc<P>,
67}
68
69impl<P: AclMetadataRead + ?Sized> AclAuthorizationHandler<P> {
70    /// Create a new ACL authorization handler.
71    ///
72    /// # Arguments
73    ///
74    /// * `metadata_provider` - Provider for retrieving ACL data
75    ///
76    /// # Example
77    ///
78    /// ```rust,ignore
79    /// use rocketmq_auth::AclAuthorizationHandler;
80    /// use rocketmq_auth::LocalAuthorizationMetadataProvider;
81    /// use std::sync::Arc;
82    ///
83    /// let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
84    /// let handler = AclAuthorizationHandler::new(provider);
85    /// ```
86    pub fn new(metadata_provider: Arc<P>) -> Self {
87        Self { metadata_provider }
88    }
89
90    /// Match policy entries from an ACL against the authorization context.
91    ///
92    /// This method:
93    /// 1. Tries CUSTOM policies first
94    /// 2. Falls back to DEFAULT policies if no CUSTOM match
95    /// 3. Sorts matched entries by priority
96    /// 4. Returns the highest-priority entry
97    async fn match_policy_entries(&self, context: &DefaultAuthorizationContext, acl: &Acl) -> Option<PolicyEntry> {
98        let mut matched_entries = Vec::new();
99
100        // Step 1: Try CUSTOM policies first. ACLs normally contain one policy
101        // per type, but admin or provider inputs may contain duplicates; all
102        // matching entries must be considered before DENY/ALLOW priority sorting.
103        for policy in acl
104            .policies()
105            .iter()
106            .filter(|policy| policy.policy_type() == PolicyType::Custom)
107        {
108            if let Some(mut entries) = self.match_policy_entries_from_policy(context, policy) {
109                matched_entries.append(&mut entries);
110            }
111        }
112
113        // Step 2: If no CUSTOM matches, try DEFAULT policies
114        if matched_entries.is_empty() {
115            for policy in acl
116                .policies()
117                .iter()
118                .filter(|policy| policy.policy_type() == PolicyType::Default)
119            {
120                if let Some(mut entries) = self.match_policy_entries_from_policy(context, policy) {
121                    matched_entries.append(&mut entries);
122                }
123            }
124        }
125
126        // Step 3: No matches found
127        if matched_entries.is_empty() {
128            return None;
129        }
130
131        // Step 4: Sort by priority and return the first (highest priority)
132        matched_entries.sort_by(|a, b| self.compare_policy_entries(a, b));
133
134        // Return first element (highest priority)
135        matched_entries.into_iter().next()
136    }
137
138    /// Match policy entries from a specific policy against the context.
139    ///
140    /// Filters entries that match:
141    /// - Resource (type, name, pattern)
142    /// - Actions (must include at least one requested action)
143    /// - Environment (source IP, etc.)
144    ///
145    /// # Java Mapping
146    ///
147    /// Maps to `matchPolicyEntries(DefaultAuthorizationContext, List<PolicyEntry>)`
148    fn match_policy_entries_from_policy(
149        &self,
150        context: &DefaultAuthorizationContext,
151        policy: &Policy,
152    ) -> Option<Vec<PolicyEntry>> {
153        let entries = policy.entries();
154        if entries.is_empty() {
155            return None;
156        }
157
158        let resource_binding = context.resource();
159        let resource = resource_binding.as_ref()?;
160        let actions = context.actions();
161        let source_ip_binding = context.source_ip();
162        let source_ip = source_ip_binding.unwrap_or("");
163        let environment = Environment::of(source_ip).unwrap_or_default();
164
165        let matched: Vec<PolicyEntry> = entries
166            .iter()
167            .filter(|entry| entry.is_match_resource(resource))
168            .filter(|entry| entry.is_match_action(actions))
169            .filter(|entry| entry.is_match_environment(&environment))
170            .cloned()
171            .collect();
172
173        if matched.is_empty() {
174            None
175        } else {
176            Some(matched)
177        }
178    }
179
180    /// Compare two policy entries for priority sorting.
181    ///
182    /// Priority rules (in order):
183    /// 1. Resource Type: Specific (Topic, Group, Cluster) > ANY
184    /// 2. Resource Pattern: LITERAL > PREFIXED > ANY
185    /// 3. For PREFIXED: Longer prefix > Shorter prefix
186    /// 4. Decision: DENY > ALLOW
187    ///
188    /// Returns:
189    /// - `Ordering::Less` if o1 has higher priority
190    /// - `Ordering::Greater` if o2 has higher priority
191    /// - `Ordering::Equal` if same priority
192    fn compare_policy_entries(&self, o1: &PolicyEntry, o2: &PolicyEntry) -> Ordering {
193        let r1 = o1.resource();
194        let r2 = o2.resource();
195
196        // Step 1: Compare resource types
197        if r1.resource_type != r2.resource_type {
198            // ANY has lowest priority
199            if r1.resource_type == ResourceType::Any {
200                return Ordering::Greater; // o1 has lower priority
201            }
202            if r2.resource_type == ResourceType::Any {
203                return Ordering::Less; // o1 has higher priority
204            }
205        }
206
207        // Step 2: Compare resource patterns
208        if r1.resource_pattern != r2.resource_pattern {
209            return match (r1.resource_pattern, r2.resource_pattern) {
210                (ResourcePattern::Literal, _) => Ordering::Less, // LITERAL has highest priority
211                (_, ResourcePattern::Literal) => Ordering::Greater,
212                (ResourcePattern::Prefixed, ResourcePattern::Any) => Ordering::Less,
213                (ResourcePattern::Any, ResourcePattern::Prefixed) => Ordering::Greater,
214                _ => Ordering::Equal,
215            };
216        }
217
218        // Step 3: For PREFIXED pattern, compare prefix lengths (longer = higher priority)
219        if r1.resource_pattern == ResourcePattern::Prefixed {
220            if let (Some(name1), Some(name2)) = (&r1.resource_name, &r2.resource_name) {
221                let cmp = name2.len().cmp(&name1.len()); // Reverse: longer name has higher priority
222                if cmp != Ordering::Equal {
223                    return cmp;
224                }
225            }
226        }
227
228        // Step 4: Compare decisions (DENY > ALLOW)
229        let d1 = o1.decision();
230        let d2 = o2.decision();
231        if d1 != d2 {
232            return if d1 == Decision::Deny {
233                Ordering::Less // o1 (DENY) has higher priority
234            } else {
235                Ordering::Greater // o2 (DENY) has higher priority
236            };
237        }
238
239        Ordering::Equal
240    }
241}
242
243impl<P: AclMetadataRead + ?Sized + 'static> AuthorizationHandler for AclAuthorizationHandler<P> {
244    fn handle<'a>(
245        &'a self,
246        context: &'a DefaultAuthorizationContext,
247    ) -> Pin<Box<dyn Future<Output = AuthServiceResult<AuthorizationDecision>> + Send + 'a>> {
248        Box::pin(async move {
249            // Step 1: Extract subject from context
250            let subject_binding = context.subject();
251            let subject_wrapper = subject_binding
252                .as_ref()
253                .ok_or_else(|| AuthServiceError::invalid_context("subject is missing".to_owned()))?;
254
255            let subject = SubjectKey::new(subject_wrapper.subject_type(), subject_wrapper.subject_key());
256
257            // Step 2: Fetch ACL from metadata provider
258            let acl = self.metadata_provider.lookup_acl(&subject).await?;
259            let Some(acl) = acl else {
260                return Ok(AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown));
261            };
262
263            // Step 3: Match policy entries
264            let Some(matched_entry) = self.match_policy_entries(context, &acl).await else {
265                return Ok(AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable));
266            };
267
268            // Step 4: Check decision
269            if matched_entry.decision() == Decision::Deny {
270                return Ok(AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied));
271            }
272
273            // Step 5: Authorization granted
274            Ok(AuthorizationDecision::Allow)
275        })
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use std::sync::Arc;
282
283    use rocketmq_security_api::Action;
284    use rocketmq_security_api::ResourcePattern;
285    use rocketmq_security_api::ResourceType;
286
287    use super::*;
288    use crate::authentication::enums::subject_type::SubjectType;
289    use crate::authentication::model::user::User;
290    use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
291    use crate::authorization::metadata_provider::local::LocalAuthorizationMetadataProvider;
292    use crate::authorization::model::policy::Policy;
293    use crate::authorization::model::resource::Resource;
294    use crate::config::AuthConfig;
295
296    #[tokio::test]
297    async fn test_acl_authorization_handler_allow() {
298        // Create a local metadata provider
299        let mut provider = LocalAuthorizationMetadataProvider::new();
300        provider.initialize(AuthConfig::default(), None).unwrap();
301        let provider = Arc::new(provider);
302
303        // Create ACL for user "alice" with ALLOW on topic "test-topic"
304        let user = User::of("alice");
305        let resource = Resource::of_topic("test-topic");
306        let policy = Policy::of(vec![resource.clone()], vec![Action::Pub], None, Decision::Allow);
307        let acl = Acl::of_subject_and_policy(&user, policy);
308
309        // Store ACL
310        provider.create_acl(acl).await.unwrap();
311
312        // Create handler
313        let handler = AclAuthorizationHandler::new(provider);
314
315        // Create authorization context
316        let context = DefaultAuthorizationContext::of("alice", SubjectType::User, resource, Action::Pub, "127.0.0.1");
317
318        // Test authorization - should succeed
319        let result = handler.handle(&context).await;
320        assert_eq!(result.unwrap(), AuthorizationDecision::Allow);
321    }
322
323    #[tokio::test]
324    async fn test_acl_authorization_handler_deny() {
325        let mut provider = LocalAuthorizationMetadataProvider::new();
326        provider.initialize(AuthConfig::default(), None).unwrap();
327        let provider = Arc::new(provider);
328
329        // Create ACL with DENY decision
330        let user = User::of("bob");
331        let resource = Resource::of_topic("forbidden-topic");
332        let policy = Policy::of(vec![resource.clone()], vec![Action::Pub], None, Decision::Deny);
333        let acl = Acl::of_subject_and_policy(&user, policy);
334
335        provider.create_acl(acl).await.unwrap();
336
337        let handler = AclAuthorizationHandler::new(provider);
338
339        let context = DefaultAuthorizationContext::of("bob", SubjectType::User, resource, Action::Pub, "192.168.1.1");
340
341        // An explicit policy denial is a normal decision, not an error.
342        let result = handler.handle(&context).await;
343        assert_eq!(
344            result.unwrap(),
345            AuthorizationDecision::Deny(AuthorizationDenial::PermissionDenied)
346        );
347    }
348
349    #[tokio::test]
350    async fn test_acl_authorization_handler_no_acl() {
351        let mut provider = LocalAuthorizationMetadataProvider::new();
352        provider.initialize(AuthConfig::default(), None).unwrap();
353        let provider = Arc::new(provider);
354        let handler = AclAuthorizationHandler::new(provider);
355
356        let resource = Resource::of_topic("test-topic");
357        let context = DefaultAuthorizationContext::of("charlie", SubjectType::User, resource, Action::Pub, "10.0.0.1");
358
359        // A missing ACL is a closed, typed denial.
360        let result = handler.handle(&context).await;
361        assert_eq!(
362            result.unwrap(),
363            AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown)
364        );
365    }
366
367    #[tokio::test]
368    async fn test_acl_authorization_handler_no_matching_policy() {
369        let mut provider = LocalAuthorizationMetadataProvider::new();
370        provider.initialize(AuthConfig::default(), None).unwrap();
371        let provider = Arc::new(provider);
372
373        // Create ACL for topic "topic-a" but request accesses "topic-b"
374        let user = User::of("dave");
375        let allowed_resource = Resource::of_topic("topic-a");
376        let policy = Policy::of(vec![allowed_resource], vec![Action::Pub], None, Decision::Allow);
377        let acl = Acl::of_subject_and_policy(&user, policy);
378
379        provider.create_acl(acl).await.unwrap();
380
381        let handler = AclAuthorizationHandler::new(provider);
382
383        let requested_resource = Resource::of_topic("topic-b");
384        let context =
385            DefaultAuthorizationContext::of("dave", SubjectType::User, requested_resource, Action::Pub, "172.16.0.1");
386
387        // The subject is known, but no policy applies to this request.
388        let result = handler.handle(&context).await;
389        assert_eq!(
390            result.unwrap(),
391            AuthorizationDecision::Deny(AuthorizationDenial::PolicyNotApplicable)
392        );
393    }
394
395    #[test]
396    fn test_compare_policy_entries_resource_type() {
397        let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
398        let handler = AclAuthorizationHandler::new(provider);
399
400        let topic_entry = PolicyEntry::of(
401            Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal),
402            vec![Action::Pub],
403            None,
404            Decision::Allow,
405        );
406
407        let any_entry = PolicyEntry::of(
408            Resource::of(ResourceType::Any, None, ResourcePattern::Any),
409            vec![Action::Pub],
410            None,
411            Decision::Allow,
412        );
413
414        // Topic should have higher priority than ANY
415        assert_eq!(handler.compare_policy_entries(&topic_entry, &any_entry), Ordering::Less);
416        assert_eq!(
417            handler.compare_policy_entries(&any_entry, &topic_entry),
418            Ordering::Greater
419        );
420    }
421
422    #[test]
423    fn test_compare_policy_entries_resource_pattern() {
424        let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
425        let handler = AclAuthorizationHandler::new(provider);
426
427        let literal = PolicyEntry::of(
428            Resource::of(ResourceType::Topic, Some("test".to_string()), ResourcePattern::Literal),
429            vec![Action::Pub],
430            None,
431            Decision::Allow,
432        );
433
434        let prefixed = PolicyEntry::of(
435            Resource::of(ResourceType::Topic, Some("test".to_string()), ResourcePattern::Prefixed),
436            vec![Action::Pub],
437            None,
438            Decision::Allow,
439        );
440
441        // LITERAL > PREFIXED
442        assert_eq!(handler.compare_policy_entries(&literal, &prefixed), Ordering::Less);
443    }
444
445    #[test]
446    fn test_compare_policy_entries_decision() {
447        let provider = Arc::new(LocalAuthorizationMetadataProvider::new());
448        let handler = AclAuthorizationHandler::new(provider);
449
450        let deny = PolicyEntry::of(
451            Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal),
452            vec![Action::Pub],
453            None,
454            Decision::Deny,
455        );
456
457        let allow = PolicyEntry::of(
458            Resource::of(ResourceType::Topic, Some("t1".to_string()), ResourcePattern::Literal),
459            vec![Action::Pub],
460            None,
461            Decision::Allow,
462        );
463
464        // DENY > ALLOW
465        assert_eq!(handler.compare_policy_entries(&deny, &allow), Ordering::Less);
466    }
467}