rocketmq_auth/authorization/strategy/
stateful_authorization_strategy.rs1use std::any::Any;
22use std::collections::HashMap;
23use std::sync::atomic::AtomicU64;
24use std::sync::atomic::AtomicUsize;
25use std::sync::atomic::Ordering;
26use std::sync::Arc;
27use std::sync::RwLock;
28use std::sync::RwLockWriteGuard;
29use std::time::Duration;
30use std::time::Instant;
31
32use rocketmq_security_api::AuthorizationDecision;
33#[cfg(test)]
34use rocketmq_security_api::AuthorizationDenial;
35use tracing::debug;
36
37use crate::authorization::context::default_authorization_context::DefaultAuthorizationContext;
38use crate::authorization::strategy::abstract_authorization_strategy::AbstractAuthorizationStrategy;
39use crate::authorization::strategy::abstract_authorization_strategy::AuthorizationStrategy;
40use crate::authorization::strategy::evaluate_base_authorization;
41use crate::authorization::strategy::AuthorizationFuture;
42use crate::config::AuthConfig;
43use crate::AuthMetrics;
44#[cfg(test)]
45use crate::AuthServiceError;
46use crate::AuthServiceResult;
47
48type AuthCache = HashMap<String, CachedAuthResult>;
49
50#[derive(Clone, Debug)]
52struct CachedAuthResult {
53 decision: AuthorizationDecision,
55 cached_at: Instant,
57}
58
59impl CachedAuthResult {
60 fn new(decision: AuthorizationDecision) -> Self {
61 Self {
62 decision,
63 cached_at: Instant::now(),
64 }
65 }
66
67 fn is_expired(&self, ttl: Duration) -> bool {
68 self.cached_at.elapsed() > ttl
69 }
70}
71
72fn cacheable_decision(
73 result: &AuthServiceResult<AuthorizationDecision>,
74 cache_negative_result: bool,
75) -> Option<AuthorizationDecision> {
76 match result {
77 Ok(AuthorizationDecision::Allow) => Some(AuthorizationDecision::Allow),
78 Ok(decision @ AuthorizationDecision::Deny(_)) if cache_negative_result => Some(*decision),
79 Ok(AuthorizationDecision::Deny(_)) | Err(_) => None,
80 }
81}
82
83pub struct StatefulAuthorizationStrategy {
148 base: AbstractAuthorizationStrategy,
150
151 auth_cache: Arc<RwLock<AuthCache>>,
153
154 cache_ttl: Duration,
156
157 cache_max_size: usize,
159
160 request_counter: AtomicUsize,
162
163 acl_generation: Arc<AtomicU64>,
165
166 last_seen_acl_generation: AtomicU64,
168 cache_negative_result: bool,
169 metrics: AuthMetrics,
170}
171
172impl StatefulAuthorizationStrategy {
173 pub fn new(
191 auth_config: AuthConfig,
192 metadata_service: Option<Box<dyn Any + Send + Sync>>,
193 ) -> AuthServiceResult<Self> {
194 let acl_generation = metadata_service
195 .as_ref()
196 .and_then(|service| service.downcast_ref::<crate::ProviderRegistry>())
197 .map(crate::ProviderRegistry::acl_generation_counter)
198 .unwrap_or_default();
199 Self::new_with_acl_generation(auth_config, metadata_service, acl_generation)
200 }
201
202 pub fn new_with_acl_generation(
204 auth_config: AuthConfig,
205 metadata_service: Option<Box<dyn Any + Send + Sync>>,
206 acl_generation: Arc<AtomicU64>,
207 ) -> AuthServiceResult<Self> {
208 Self::new_with_acl_generation_and_metrics(auth_config, metadata_service, acl_generation, AuthMetrics::default())
209 }
210
211 pub fn new_with_acl_generation_and_metrics(
212 auth_config: AuthConfig,
213 metadata_service: Option<Box<dyn Any + Send + Sync>>,
214 acl_generation: Arc<AtomicU64>,
215 metrics: AuthMetrics,
216 ) -> AuthServiceResult<Self> {
217 let cache_ttl = Duration::from_secs(auth_config.stateful_authorization_cache_expired_second as u64);
218 let cache_max_size = auth_config.stateful_authorization_cache_max_num as usize;
219 let cache_negative_result = auth_config.stateful_authorization_cache_negative_enable;
220
221 let base = AbstractAuthorizationStrategy::new(auth_config, metadata_service)?;
222 let initial_generation = acl_generation.load(Ordering::Acquire);
223
224 debug!(
225 "StatefulAuthorizationStrategy initialized with TTL: {:?}, max size: {}",
226 cache_ttl, cache_max_size
227 );
228
229 Ok(Self {
230 base,
231 auth_cache: Arc::new(RwLock::new(HashMap::new())),
232 cache_ttl,
233 cache_max_size,
234 request_counter: AtomicUsize::new(0),
235 acl_generation,
236 last_seen_acl_generation: AtomicU64::new(initial_generation),
237 cache_negative_result,
238 metrics,
239 })
240 }
241
242 fn build_key(&self, context: &DefaultAuthorizationContext) -> String {
246 let generation = self.refresh_acl_generation();
247 let channel_id = context.channel_id().unwrap_or("");
248 let subject_key = context.subject().map(|s| s.subject_key()).unwrap_or_default();
249 let resource_key = context.resource().map(|r| format!("{:?}", r)).unwrap_or_default();
250 let actions = context
251 .actions()
252 .iter()
253 .map(|a| format!("{:?}", a))
254 .collect::<Vec<_>>()
255 .join(",");
256 let source_ip = context.source_ip().unwrap_or("");
257
258 format!(
259 "{}#{}#{}#{}#{}#{}",
260 generation, channel_id, subject_key, resource_key, actions, source_ip
261 )
262 }
263
264 fn refresh_acl_generation(&self) -> u64 {
265 let current = self.acl_generation.load(Ordering::Acquire);
266 let previous = self.last_seen_acl_generation.load(Ordering::Acquire);
267 if previous != current
268 && self
269 .last_seen_acl_generation
270 .compare_exchange(previous, current, Ordering::AcqRel, Ordering::Acquire)
271 .is_ok()
272 {
273 self.clear_cache();
274 self.metrics.record_cache_invalidation();
275 }
276 current
277 }
278
279 fn recover_poisoned_cache(&self) {
280 let mut cache = match self.auth_cache.write() {
281 Ok(cache) => cache,
282 Err(poisoned) => poisoned.into_inner(),
283 };
284 cache.clear();
285 self.auth_cache.clear_poison();
286 debug!("Authorization cache lock poisoned; cache cleared");
287 }
288
289 fn write_cache_recovering(&self) -> RwLockWriteGuard<'_, AuthCache> {
290 match self.auth_cache.write() {
291 Ok(cache) => cache,
292 Err(poisoned) => {
293 let mut cache = poisoned.into_inner();
294 cache.clear();
295 self.auth_cache.clear_poison();
296 debug!("Authorization cache write lock poisoned; cache cleared before reuse");
297 cache
298 }
299 }
300 }
301
302 fn evict_expired(&self) {
304 let mut cache = self.write_cache_recovering();
305 let before_size = cache.len();
306
307 cache.retain(|_, result| !result.is_expired(self.cache_ttl));
308
309 let evicted = before_size - cache.len();
310 if evicted > 0 {
311 debug!("Evicted {} expired cache entries", evicted);
312 }
313 }
314
315 fn evict_if_full(&self) {
317 let mut cache = self.write_cache_recovering();
318
319 if cache.len() >= self.cache_max_size {
320 let to_remove = (self.cache_max_size / 10).max(1);
322 let keys_to_remove: Vec<String> = cache.keys().take(to_remove).cloned().collect();
323
324 for key in keys_to_remove {
325 cache.remove(&key);
326 }
327
328 debug!("Evicted {} entries to maintain cache size", to_remove);
329 }
330 }
331
332 pub fn base(&self) -> &AbstractAuthorizationStrategy {
334 &self.base
335 }
336
337 pub fn cache_size(&self) -> usize {
339 match self.auth_cache.read() {
340 Ok(cache) => cache.len(),
341 Err(poisoned) => {
342 drop(poisoned);
343 self.recover_poisoned_cache();
344 0
345 }
346 }
347 }
348
349 pub fn clear_cache(&self) {
351 let mut cache = self.write_cache_recovering();
352 cache.clear();
353 debug!("Authorization cache cleared");
354 }
355}
356
357impl AuthorizationStrategy for StatefulAuthorizationStrategy {
358 fn evaluate<'a>(&'a self, context: &'a DefaultAuthorizationContext) -> AuthorizationFuture<'a> {
385 Box::pin(async move {
386 let _operation = self.base.enter_operation()?;
387 if context.channel_id().is_none_or(str::is_empty) {
389 debug!("No channel ID, bypassing cache");
390 return evaluate_base_authorization(&self.base, context).await;
391 }
392
393 let cache_key = self.build_key(context);
394
395 {
397 match self.auth_cache.read() {
398 Ok(cache) => {
399 if let Some(cached_result) = cache.get(&cache_key) {
400 if !cached_result.is_expired(self.cache_ttl) {
401 debug!("Authorization cache hit");
402 self.metrics.record_cache_hit();
403 return Ok(cached_result.decision);
404 } else {
405 debug!("Authorization cache entry expired");
406 }
407 }
408 }
409 Err(poisoned) => {
410 drop(poisoned);
411 self.recover_poisoned_cache();
412 debug!("Authorization cache unavailable after poison recovery");
413 }
414 }
415 }
416
417 debug!("Authorization cache miss");
419 self.metrics.record_cache_miss();
420
421 let result = evaluate_base_authorization(&self.base, context).await;
422
423 if let Some(decision) = cacheable_decision(&result, self.cache_negative_result) {
426 self.evict_if_full();
427
428 let mut cache = self.write_cache_recovering();
429 cache.insert(cache_key.clone(), CachedAuthResult::new(decision));
430 debug!("Authorization decision cached");
431 }
432
433 let counter = self.request_counter.fetch_add(1, Ordering::Relaxed);
435 if counter.is_multiple_of(100) {
436 self.evict_expired();
437 }
438
439 result
440 })
441 }
442}
443
444#[cfg(test)]
445mod tests {
446 use cheetah_string::CheetahString;
447 use rocketmq_security_api::Action;
448
449 use super::*;
450 use crate::authentication::enums::subject_type::SubjectType;
451 use crate::authorization::model::resource::Resource;
452
453 fn create_test_config() -> AuthConfig {
454 AuthConfig {
455 config_name: CheetahString::from("test"),
456 cluster_name: CheetahString::from("test-cluster"),
457 auth_config_path: CheetahString::from("/tmp/auth"),
458 acl_file: CheetahString::new(),
459 acl_file_watch_enabled: false,
460 acl_file_watch_interval_millis: 5_000,
461 authentication_enabled: false,
462 authentication_provider: CheetahString::new(),
463 authentication_metadata_provider: CheetahString::new(),
464 authentication_strategy: CheetahString::new(),
465 authentication_whitelist: CheetahString::new(),
466 init_authentication_user: CheetahString::new(),
467 inner_client_authentication_credentials: CheetahString::new(),
468 signature_algorithm: Default::default(),
469 request_timestamp_expired_millis: 0,
470 authorization_enabled: true,
471 authorization_provider: CheetahString::new(),
472 authorization_metadata_provider: CheetahString::new(),
473 authorization_strategy: CheetahString::new(),
474 authorization_whitelist: CheetahString::new(),
475 migrate_auth_from_v1_enabled: false,
476 user_cache_max_num: 1000,
477 user_cache_expired_second: 300,
478 user_cache_refresh_second: 60,
479 acl_cache_max_num: 1000,
480 acl_cache_expired_second: 300,
481 acl_cache_refresh_second: 60,
482 stateful_authentication_cache_max_num: 1000,
483 stateful_authentication_cache_expired_second: 300,
484 stateful_authorization_cache_max_num: 100,
485 stateful_authorization_cache_expired_second: 60,
486 stateful_authorization_cache_negative_enable: false,
487 ..AuthConfig::default()
488 }
489 }
490
491 #[test]
492 fn test_stateful_strategy_creation() {
493 let config = create_test_config();
494 let strategy = StatefulAuthorizationStrategy::new(config, None);
495 assert!(strategy.is_ok());
496 }
497
498 #[test]
499 fn test_cache_key_building() {
500 let config = create_test_config();
501 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
502
503 let mut context = DefaultAuthorizationContext::default();
504 context.set_channel_id("ch-123".to_string());
505 context.set_source_ip("192.168.0.1".to_string());
506
507 let key = strategy.build_key(&context);
508 assert!(key.starts_with("0#"));
509 assert!(key.contains("ch-123"));
510 assert!(key.contains("192.168.0.1"));
511 }
512
513 #[test]
514 fn test_cache_key_includes_acl_generation() {
515 let config = create_test_config();
516 let acl_generation = Arc::new(AtomicU64::new(11));
517 let strategy =
518 StatefulAuthorizationStrategy::new_with_acl_generation(config, None, acl_generation.clone()).unwrap();
519
520 let mut context = DefaultAuthorizationContext::default();
521 context.set_channel_id("ch-123".to_string());
522 context.set_source_ip("192.168.0.1".to_string());
523
524 let key = strategy.build_key(&context);
525 assert!(key.starts_with("11#ch-123#"));
526
527 acl_generation.store(12, Ordering::Release);
528 let key = strategy.build_key(&context);
529 assert!(key.starts_with("12#ch-123#"));
530 }
531
532 #[tokio::test]
533 async fn test_no_channel_id_bypasses_cache() {
534 let mut config = create_test_config();
535 config.authorization_enabled = false;
536
537 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
538 let context = DefaultAuthorizationContext::default();
539
540 let result = strategy.evaluate(&context).await;
541 assert!(result.is_ok());
542 assert_eq!(strategy.cache_size(), 0); }
544
545 #[test]
546 fn test_clear_cache() {
547 let config = create_test_config();
548 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
549
550 strategy.clear_cache();
551 assert_eq!(strategy.cache_size(), 0);
552 }
553
554 #[tokio::test]
555 async fn test_acl_generation_change_invalidates_cached_authorization_result() {
556 let mut config = create_test_config();
557 config.authorization_enabled = false;
558 let acl_generation = Arc::new(AtomicU64::new(0));
559 let strategy =
560 StatefulAuthorizationStrategy::new_with_acl_generation(config, None, acl_generation.clone()).unwrap();
561 let mut context = DefaultAuthorizationContext::default();
562 context.set_channel_id("ch-123".to_string());
563 context.set_source_ip("192.168.0.1".to_string());
564
565 assert_eq!(strategy.evaluate(&context).await.unwrap(), AuthorizationDecision::Allow);
566 assert_eq!(strategy.cache_size(), 1);
567
568 acl_generation.fetch_add(1, Ordering::AcqRel);
569 assert_eq!(strategy.evaluate(&context).await.unwrap(), AuthorizationDecision::Allow);
570 assert_eq!(strategy.cache_size(), 1);
571
572 let cache = strategy.auth_cache.read().unwrap();
573 assert!(cache.keys().all(|key| key.starts_with("1#")));
574 }
575
576 #[tokio::test]
577 async fn poisoned_authorization_cache_is_cleared_and_evaluation_continues() {
578 let mut config = create_test_config();
579 config.authorization_enabled = false;
580 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
581 let cache = strategy.auth_cache.clone();
582
583 let poison_result = std::thread::spawn(move || {
584 let _guard = cache.write().unwrap();
585 panic!("poison authorization cache for test");
586 })
587 .join();
588 assert!(poison_result.is_err());
589
590 let mut context = DefaultAuthorizationContext::default();
591 context.set_channel_id("ch-poison".to_string());
592
593 assert_eq!(strategy.evaluate(&context).await.unwrap(), AuthorizationDecision::Allow);
594 assert_eq!(strategy.cache_size(), 1);
595 assert!(strategy.auth_cache.read().is_ok());
596 }
597
598 #[tokio::test(flavor = "current_thread")]
599 async fn evaluate_cache_miss_inside_current_thread_runtime_without_nested_block_on_panic() {
600 let mut config = create_test_config();
601 config.authorization_enabled = false;
602 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
603 let mut context = DefaultAuthorizationContext::default();
604 context.set_channel_id("ch-current-thread".to_string());
605 assert_eq!(strategy.evaluate(&context).await.unwrap(), AuthorizationDecision::Allow);
606
607 assert_eq!(strategy.cache_size(), 1);
608 }
609
610 #[tokio::test]
611 async fn denied_authorization_is_not_cached_by_default() {
612 let config = create_test_config();
613 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
614 let mut context = DefaultAuthorizationContext::of(
615 "alice",
616 SubjectType::User,
617 Resource::of_topic("TopicA"),
618 Action::Pub,
619 "127.0.0.1",
620 );
621 context.set_channel_id("ch-denied-default".to_string());
622
623 assert_eq!(
624 strategy.evaluate(&context).await.unwrap(),
625 AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown)
626 );
627 assert_eq!(strategy.cache_size(), 0);
628 }
629
630 #[tokio::test]
631 async fn denied_authorization_can_be_cached_when_enabled() {
632 let mut config = create_test_config();
633 config.stateful_authorization_cache_negative_enable = true;
634 let strategy = StatefulAuthorizationStrategy::new(config, None).unwrap();
635 let mut context = DefaultAuthorizationContext::of(
636 "alice",
637 SubjectType::User,
638 Resource::of_topic("TopicA"),
639 Action::Pub,
640 "127.0.0.1",
641 );
642 context.set_channel_id("ch-denied-cached".to_string());
643
644 assert_eq!(
645 strategy.evaluate(&context).await.unwrap(),
646 AuthorizationDecision::Deny(AuthorizationDenial::SubjectUnknown)
647 );
648 assert_eq!(strategy.cache_size(), 1);
649 }
650
651 #[test]
652 fn operational_errors_are_never_cacheable() {
653 let result = Err(AuthServiceError::invalid_context("broken context".to_owned()));
654
655 assert_eq!(cacheable_decision(&result, false), None);
656 assert_eq!(cacheable_decision(&result, true), None);
657 }
658}