rocketmq_auth/authorization/strategy/
abstract_authorization_strategy.rs1use 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
38pub type AuthorizationFuture<'a> = Pin<Box<dyn Future<Output = AuthServiceResult<AuthorizationDecision>> + 'a>>;
40
41pub trait AuthorizationStrategy: Send + Sync {
46 fn evaluate<'a>(&'a self, context: &'a DefaultAuthorizationContext) -> AuthorizationFuture<'a>;
68}
69
70pub struct AbstractAuthorizationStrategy {
102 auth_config: AuthConfig,
104
105 authorization_whitelist: HashSet<String>,
107
108 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 pub fn new(
137 auth_config: AuthConfig,
138 metadata_service: Option<Box<dyn Any + Send + Sync>>,
139 ) -> AuthServiceResult<Self> {
140 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 pub async fn do_evaluate(&self, context: &DefaultAuthorizationContext) -> AuthServiceResult<AuthorizationDecision> {
225 let _operation = self.enter_operation()?;
226 if !self.auth_config.authorization_enabled {
228 debug!("Authorization disabled in configuration, allowing access");
229 return Ok(AuthorizationDecision::Allow);
230 }
231
232 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 pub fn auth_config(&self) -> &AuthConfig {
253 &self.auth_config
254 }
255
256 pub fn authorization_whitelist(&self) -> &HashSet<String> {
258 &self.authorization_whitelist
259 }
260
261 pub fn has_authorization_provider(&self) -> bool {
263 self.authorization_provider.is_some()
264 }
265
266 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}