Skip to main content

rocketmq_auth/authorization/strategy/
abstract_authorization_strategy.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//! Abstract authorization strategy base implementation.
16//!
17//! This module provides a base implementation for authorization strategies, encapsulating
18//! common logic such as whitelist checking, authorization provider initialization, and
19//! error handling.
20
21use std::any::Any;
22use std::collections::HashSet;
23use std::future::Future;
24use std::pin::Pin;
25use std::sync::Arc;
26
27use rocketmq_security_api::AuthorizationDecision;
28use tracing::debug;
29
30use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
31use crate::authorization::factory::AuthorizationFactory;
32use crate::authorization::provider::AuthorizationProvider;
33use crate::authorization::provider::DefaultAuthorizationProvider;
34use crate::config::AuthConfig;
35use crate::AuthServiceError;
36use crate::AuthServiceResult;
37
38/// Result type for authorization strategy operations.
39pub type AuthorizationFuture<'a> = Pin<Box<dyn Future<Output = AuthServiceResult<AuthorizationDecision>> + 'a>>;
40
41/// Trait defining the core authorization strategy behavior.
42///
43/// This trait must be implemented by all authorization strategies. It provides
44/// the entry point for authorization evaluation.
45pub trait AuthorizationStrategy: Send + Sync {
46    /// Evaluates the authorization context to determine if access should be granted.
47    ///
48    /// # Arguments
49    ///
50    /// * `context` - The authorization context containing request information
51    ///
52    /// # Returns
53    ///
54    /// * `Ok(AuthorizationDecision)` if evaluation reaches an allow or deny
55    /// * `Err(AuthServiceError)` if evaluation cannot make a decision
56    ///
57    /// # Examples
58    ///
59    /// ```rust,ignore
60    /// let strategy = StatelessAuthorizationStrategy::new(config, None)?;
61    /// let context = DefaultAuthorizationContext::of(subject, resource, action, source_ip);
62    /// match strategy.evaluate(&context).await? {
63    ///     AuthorizationDecision::Allow => proceed(),
64    ///     AuthorizationDecision::Deny(reason) => reject(reason),
65    /// }
66    /// ```
67    fn evaluate<'a>(&'a self, context: &'a DefaultAuthorizationContext) -> AuthorizationFuture<'a>;
68}
69
70/// Abstract base implementation for authorization strategies.
71///
72/// This struct provides common functionality for all authorization strategies:
73/// - Whitelist checking
74/// - Configuration management
75/// - Default authorization logic
76///
77/// Concrete strategies should use this as a base and implement their specific
78/// caching or stateful behavior on top.
79///
80/// # Architecture
81///
82/// ```text
83/// AbstractAuthorizationStrategy
84/// ├── auth_config: Configuration settings
85/// └── authorization_whitelist: RPC codes that bypass authorization
86/// ```
87///
88/// # Examples
89///
90/// ```rust,ignore
91/// // Create a stateless strategy
92/// let strategy = AbstractAuthorizationStrategy::new(auth_config, None)?;
93///
94/// // Evaluate authorization
95/// let context = DefaultAuthorizationContext::of(subject, resource, action, source_ip);
96/// match strategy.do_evaluate(&context).await? {
97///     AuthorizationDecision::Allow => proceed(),
98///     AuthorizationDecision::Deny(reason) => reject(reason),
99/// }
100/// ```
101pub struct AbstractAuthorizationStrategy {
102    /// Authorization configuration
103    auth_config: AuthConfig,
104
105    /// Set of RPC codes that bypass authorization (whitelist)
106    authorization_whitelist: HashSet<String>,
107
108    /// Provider used for concrete authorization checks.
109    authorization_provider: Option<Arc<DefaultAuthorizationProvider>>,
110    admission: Option<Arc<crate::provider_owner::ProviderAdmission>>,
111}
112
113impl AbstractAuthorizationStrategy {
114    pub(crate) fn enter_operation(&self) -> AuthServiceResult<Option<crate::provider_owner::ProviderOperation>> {
115        self.admission.as_ref().map(|admission| admission.enter()).transpose()
116    }
117
118    /// Creates a new `AbstractAuthorizationStrategy` instance.
119    ///
120    /// # Arguments
121    ///
122    /// * `auth_config` - Authorization configuration
123    /// * `metadata_service` - Optional metadata service supplier for provider initialization
124    ///
125    /// # Returns
126    ///
127    /// Returns a new `AbstractAuthorizationStrategy` instance with initialized
128    /// whitelist.
129    ///
130    /// # Examples
131    ///
132    /// ```rust,ignore
133    /// let config = AuthConfig::default();
134    /// let strategy = AbstractAuthorizationStrategy::new(config, None)?;
135    /// ```
136    pub fn new(
137        auth_config: AuthConfig,
138        metadata_service: Option<Box<dyn Any + Send + Sync>>,
139    ) -> AuthServiceResult<Self> {
140        // Parse and build whitelist from configuration
141        let mut authorization_whitelist = HashSet::new();
142        let whitelist_str = auth_config.authorization_whitelist.as_str();
143
144        if !whitelist_str.is_empty() {
145            for rpc_code in whitelist_str.split(',') {
146                let trimmed = rpc_code.trim();
147                if !trimmed.is_empty() {
148                    authorization_whitelist.insert(trimmed.to_string());
149                    debug!("Added authorization whitelist entry");
150                }
151            }
152        }
153
154        debug!(
155            "AbstractAuthorizationStrategy initialized with {} whitelisted RPC codes",
156            authorization_whitelist.len()
157        );
158
159        let admission = metadata_service
160            .as_ref()
161            .map(|service| {
162                service
163                    .downcast_ref::<crate::ProviderRegistry>()
164                    .map(crate::ProviderRegistry::admission)
165                    .ok_or_else(|| {
166                        AuthServiceError::new(
167                            crate::AuthOperation::InitializeProvider,
168                            crate::AuthFailureKind::Unsupported,
169                        )
170                    })
171            })
172            .transpose()?;
173        let authorization_provider = Some(if metadata_service.is_some() {
174            let mut provider = DefaultAuthorizationProvider::new();
175            provider.initialize_with_metadata(auth_config.clone(), metadata_service)?;
176            Arc::new(provider)
177        } else {
178            AuthorizationFactory::get_provider(&auth_config)?
179        });
180
181        Ok(Self {
182            auth_config,
183            authorization_whitelist,
184            authorization_provider,
185            admission,
186        })
187    }
188
189    /// Core authorization evaluation logic shared by all strategies.
190    ///
191    /// This method encapsulates the common authorization flow:
192    /// 1. Check if context is null
193    /// 2. Check if authorization is enabled
194    /// 3. Check whitelist
195    /// 4. Fails closed when no concrete authorization provider is attached
196    ///
197    /// # Arguments
198    ///
199    /// * `context` - The authorization context to evaluate
200    ///
201    /// # Returns
202    ///
203    /// * `Ok(AuthorizationDecision)` when evaluation reaches a final decision
204    /// * `Err(AuthServiceError)` when evaluation fails operationally
205    ///
206    /// # Errors
207    ///
208    /// Returns an error if:
209    /// - The authorization provider rejects the request
210    /// - An internal error occurs during evaluation
211    ///
212    /// # Examples
213    ///
214    /// ```rust,ignore
215    /// let strategy = AbstractAuthorizationStrategy::new(config, None)?;
216    /// let context = DefaultAuthorizationContext::of(subject, resource, action, ip);
217    ///
218    /// // This will check all preconditions
219    /// match strategy.do_evaluate(&context).await? {
220    ///     AuthorizationDecision::Allow => proceed(),
221    ///     AuthorizationDecision::Deny(reason) => reject(reason),
222    /// }
223    /// ```
224    pub async fn do_evaluate(&self, context: &DefaultAuthorizationContext) -> AuthServiceResult<AuthorizationDecision> {
225        let _operation = self.enter_operation()?;
226        // Check if authorization is enabled
227        if !self.auth_config.authorization_enabled {
228            debug!("Authorization disabled in configuration, allowing access");
229            return Ok(AuthorizationDecision::Allow);
230        }
231
232        // Check whitelist
233        if let Some(rpc_code) = context.rpc_code() {
234            if self.authorization_whitelist.contains(rpc_code) {
235                debug!("Authorization request class is whitelisted, allowing access");
236                return Ok(AuthorizationDecision::Allow);
237            }
238        }
239
240        debug!(
241            action_count = context.actions().len(),
242            "authorization evaluation started"
243        );
244
245        let provider = self.authorization_provider.as_ref().ok_or_else(|| {
246            AuthServiceError::not_initialized("authorization strategy does not have a concrete provider".to_owned())
247        })?;
248        provider.authorize(context).await
249    }
250
251    /// Gets a reference to the authorization configuration.
252    pub fn auth_config(&self) -> &AuthConfig {
253        &self.auth_config
254    }
255
256    /// Gets a reference to the whitelist set.
257    pub fn authorization_whitelist(&self) -> &HashSet<String> {
258        &self.authorization_whitelist
259    }
260
261    /// Returns true when a concrete provider is attached.
262    pub fn has_authorization_provider(&self) -> bool {
263        self.authorization_provider.is_some()
264    }
265
266    /// Checks if a given RPC code is whitelisted.
267    ///
268    /// # Arguments
269    ///
270    /// * `rpc_code` - The RPC code to check
271    ///
272    /// # Returns
273    ///
274    /// `true` if the RPC code is in the whitelist, `false` otherwise
275    pub fn is_whitelisted(&self, rpc_code: &str) -> bool {
276        self.authorization_whitelist.contains(rpc_code)
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use cheetah_string::CheetahString;
283    use rocketmq_security_api::Action;
284
285    use super::*;
286    use crate::authentication::enums::subject_type::SubjectType;
287    use crate::authorization::model::resource::Resource;
288
289    fn create_test_config(enabled: bool, whitelist: &str) -> AuthConfig {
290        AuthConfig {
291            config_name: CheetahString::from("test"),
292            cluster_name: CheetahString::from("test-cluster"),
293            auth_config_path: CheetahString::from("/tmp/auth"),
294            acl_file: CheetahString::new(),
295            acl_file_watch_enabled: false,
296            acl_file_watch_interval_millis: 5_000,
297            authentication_enabled: false,
298            authentication_provider: CheetahString::new(),
299            authentication_metadata_provider: CheetahString::new(),
300            authentication_strategy: CheetahString::new(),
301            authentication_whitelist: CheetahString::new(),
302            init_authentication_user: CheetahString::new(),
303            inner_client_authentication_credentials: CheetahString::new(),
304            signature_algorithm: Default::default(),
305            request_timestamp_expired_millis: 0,
306            authorization_enabled: enabled,
307            authorization_provider: CheetahString::new(),
308            authorization_metadata_provider: CheetahString::new(),
309            authorization_strategy: CheetahString::new(),
310            authorization_whitelist: CheetahString::from(whitelist),
311            migrate_auth_from_v1_enabled: false,
312            user_cache_max_num: 1000,
313            user_cache_expired_second: 300,
314            user_cache_refresh_second: 60,
315            acl_cache_max_num: 1000,
316            acl_cache_expired_second: 300,
317            acl_cache_refresh_second: 60,
318            stateful_authentication_cache_max_num: 1000,
319            stateful_authentication_cache_expired_second: 300,
320            stateful_authorization_cache_max_num: 1000,
321            stateful_authorization_cache_expired_second: 300,
322            stateful_authorization_cache_negative_enable: false,
323            ..AuthConfig::default()
324        }
325    }
326
327    #[test]
328    fn test_whitelist_parsing() {
329        let config = create_test_config(true, "10,11,12");
330        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
331
332        assert_eq!(strategy.authorization_whitelist().len(), 3);
333        assert!(strategy.is_whitelisted("10"));
334        assert!(strategy.is_whitelisted("11"));
335        assert!(strategy.is_whitelisted("12"));
336        assert!(!strategy.is_whitelisted("13"));
337    }
338
339    #[test]
340    fn test_whitelist_with_spaces() {
341        let config = create_test_config(true, " 10 , 11 , 12 ");
342        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
343
344        assert_eq!(strategy.authorization_whitelist().len(), 3);
345        assert!(strategy.is_whitelisted("10"));
346        assert!(strategy.is_whitelisted("11"));
347        assert!(strategy.is_whitelisted("12"));
348    }
349
350    #[test]
351    fn test_empty_whitelist() {
352        let config = create_test_config(true, "");
353        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
354
355        assert_eq!(strategy.authorization_whitelist().len(), 0);
356        assert!(!strategy.is_whitelisted("10"));
357    }
358
359    #[test]
360    fn test_disabled_authorization() {
361        let config = create_test_config(false, "");
362        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
363
364        assert!(!strategy.auth_config().authorization_enabled);
365    }
366
367    #[test]
368    fn test_provider_is_initialized() {
369        let config = create_test_config(true, "");
370        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
371
372        assert!(strategy.has_authorization_provider());
373    }
374
375    #[test]
376    fn provider_initialization_preserves_factory_error_classification() {
377        let mut config = create_test_config(true, "");
378        config.authorization_provider = CheetahString::from("unsupported-provider");
379
380        let Err(error) = AbstractAuthorizationStrategy::new(config, None) else {
381            panic!("an unsupported provider must fail initialization");
382        };
383        assert_eq!(error.operation(), crate::AuthOperation::Initialize);
384        assert_eq!(error.kind(), crate::AuthFailureKind::Unsupported);
385        assert!(!error.source_present());
386    }
387
388    #[tokio::test]
389    async fn test_do_evaluate_disabled() {
390        let config = create_test_config(false, "");
391        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
392
393        let context = DefaultAuthorizationContext::default();
394        let result = strategy.do_evaluate(&context).await;
395
396        assert_eq!(result.unwrap(), AuthorizationDecision::Allow);
397    }
398
399    #[tokio::test]
400    async fn test_do_evaluate_delegates_to_provider() {
401        let config = create_test_config(true, "");
402        let strategy = AbstractAuthorizationStrategy::new(config, None).unwrap();
403
404        let context = DefaultAuthorizationContext::of(
405            "alice",
406            SubjectType::User,
407            Resource::of_topic("topic-a"),
408            Action::Get,
409            "127.0.0.1",
410        );
411        let result = strategy.do_evaluate(&context).await;
412
413        assert!(matches!(result, Ok(AuthorizationDecision::Deny(_))));
414    }
415}