rocketmq_auth/authentication/strategy/
stateful_authentication_strategy.rs1use std::collections::HashSet;
21use std::sync::atomic::AtomicU64;
22use std::sync::atomic::Ordering;
23use std::sync::Arc;
24use std::time::Duration;
25
26use crate::authentication::context::default_authentication_context::DefaultAuthenticationContext;
27use crate::authentication::provider::AuthenticationProvider;
28use crate::authentication::strategy::abstract_authentication_strategy::AbstractAuthenticationStrategy;
29use crate::authentication::strategy::authenticate_with_provider;
30use crate::authentication::strategy::authentication_strategy::AuthenticationStrategy;
31use crate::authentication::strategy::AuthenticationFuture;
32use crate::authorization::context::authentication_context::AuthenticationContext;
33use crate::config::AuthConfig;
34use crate::AuthFailureKind;
35use crate::AuthMetrics;
36use crate::AuthOperation;
37use crate::AuthServiceError;
38use crate::AuthServiceResult;
39use moka::sync::Cache;
40
41const POUND: &str = "#";
42
43#[derive(Clone, Debug)]
45struct AuthCacheEntry {
46 success: bool,
48 error_kind: Option<AuthFailureKind>,
50}
51
52impl AuthCacheEntry {
53 fn success() -> Self {
54 Self {
55 success: true,
56 error_kind: None,
57 }
58 }
59
60 fn failure(error: &AuthServiceError) -> Self {
61 Self {
62 success: false,
63 error_kind: Some(error.kind()),
64 }
65 }
66}
67
68pub struct StatefulAuthenticationStrategy<P>
76where
77 P: AuthenticationProvider<Context = DefaultAuthenticationContext>,
78{
79 auth_config: AuthConfig,
81 authentication_white_set: HashSet<String>,
83 authentication_provider: Option<Arc<P>>,
85 auth_cache: Cache<String, AuthCacheEntry>,
87 acl_generation: Arc<AtomicU64>,
89 last_seen_acl_generation: AtomicU64,
91 metrics: AuthMetrics,
92}
93
94impl<P> StatefulAuthenticationStrategy<P>
95where
96 P: AuthenticationProvider<Context = DefaultAuthenticationContext> + Send + Sync + 'static,
97{
98 pub fn new(auth_config: AuthConfig, provider: Option<Arc<P>>) -> Self {
100 Self::new_with_acl_generation(auth_config, provider, Arc::new(AtomicU64::new(0)))
101 }
102
103 pub fn new_with_acl_generation(
105 auth_config: AuthConfig,
106 provider: Option<Arc<P>>,
107 acl_generation: Arc<AtomicU64>,
108 ) -> Self {
109 Self::new_with_acl_generation_and_metrics(auth_config, provider, acl_generation, AuthMetrics::default())
110 }
111
112 pub fn new_with_acl_generation_and_metrics(
113 auth_config: AuthConfig,
114 provider: Option<Arc<P>>,
115 acl_generation: Arc<AtomicU64>,
116 metrics: AuthMetrics,
117 ) -> Self {
118 let mut authentication_white_set = HashSet::new();
119
120 let whitelist = auth_config.authentication_whitelist.to_string();
121 if !whitelist.is_empty() {
122 for rpc_code in whitelist.split(',') {
123 let trimmed = rpc_code.trim();
124 if !trimmed.is_empty() {
125 authentication_white_set.insert(trimmed.to_string());
126 }
127 }
128 }
129
130 let auth_cache = Cache::builder()
131 .time_to_live(Duration::from_secs(
132 auth_config.stateful_authentication_cache_expired_second as u64,
133 ))
134 .max_capacity(auth_config.stateful_authentication_cache_max_num as u64)
135 .build();
136 let initial_generation = acl_generation.load(Ordering::Acquire);
137
138 Self {
139 auth_config,
140 authentication_white_set,
141 authentication_provider: provider,
142 auth_cache,
143 acl_generation,
144 last_seen_acl_generation: AtomicU64::new(initial_generation),
145 metrics,
146 }
147 }
148
149 fn build_cache_key(&self, context: &DefaultAuthenticationContext) -> AuthServiceResult<String> {
155 let channel_id = context
156 .base
157 .channel_id()
158 .ok_or_else(|| AuthServiceError::new(AuthOperation::Authenticate, AuthFailureKind::InvalidInput))?;
159 let generation = self.refresh_acl_generation();
160
161 if let Some(username) = context.username() {
162 Ok(format!("{}{}{}{}{}", generation, POUND, channel_id, POUND, username))
163 } else {
164 Ok(format!("{}{}{}", generation, POUND, channel_id))
165 }
166 }
167
168 fn refresh_acl_generation(&self) -> u64 {
169 let current = self.acl_generation.load(Ordering::Acquire);
170 let previous = self.last_seen_acl_generation.load(Ordering::Acquire);
171 if previous != current
172 && self
173 .last_seen_acl_generation
174 .compare_exchange(previous, current, Ordering::AcqRel, Ordering::Acquire)
175 .is_ok()
176 {
177 self.auth_cache.invalidate_all();
178 self.metrics.record_cache_invalidation();
179 }
180 current
181 }
182
183 async fn do_authenticate_internal(&self, context: &dyn AuthenticationContext) -> AuthServiceResult<()> {
185 if !self.auth_config.authentication_enabled {
186 return Ok(());
187 }
188
189 let default_context = context
190 .as_any()
191 .downcast_ref::<DefaultAuthenticationContext>()
192 .ok_or_else(|| AuthServiceError::new(AuthOperation::Authenticate, AuthFailureKind::InvalidInput))?;
193
194 if let Some(rpc_code) = default_context.base.rpc_code() {
195 if self.is_whitelisted(rpc_code.as_str()) {
196 return Ok(());
197 }
198 }
199
200 let provider = match &self.authentication_provider {
201 Some(p) => p,
202 None => return Ok(()),
203 };
204
205 authenticate_with_provider(provider.as_ref(), default_context).await
206 }
207
208 pub fn provider(&self) -> Option<&P> {
209 self.authentication_provider.as_deref()
210 }
211}
212
213impl<P> AbstractAuthenticationStrategy for StatefulAuthenticationStrategy<P>
214where
215 P: AuthenticationProvider<Context = DefaultAuthenticationContext> + Send + Sync + 'static,
216{
217 fn authentication_white_set(&self) -> &HashSet<String> {
218 &self.authentication_white_set
219 }
220}
221
222impl<P> AuthenticationStrategy for StatefulAuthenticationStrategy<P>
223where
224 P: AuthenticationProvider<Context = DefaultAuthenticationContext> + Send + Sync + 'static,
225{
226 fn authenticate<'a>(&'a self, context: &'a dyn AuthenticationContext) -> AuthenticationFuture<'a> {
227 Box::pin(async move {
228 let default_context = context
229 .as_any()
230 .downcast_ref::<DefaultAuthenticationContext>()
231 .ok_or_else(|| AuthServiceError::new(AuthOperation::Authenticate, AuthFailureKind::InvalidInput))?;
232
233 if default_context.base.channel_id().is_none() {
234 return self.do_authenticate_internal(context).await;
235 }
236
237 let cache_key = self.build_cache_key(default_context)?;
238
239 let result = if let Some(result) = self.auth_cache.get(&cache_key) {
240 self.metrics.record_cache_hit();
241 result
242 } else {
243 self.metrics.record_cache_miss();
244 let result = match self.do_authenticate_internal(context).await {
245 Ok(()) => AuthCacheEntry::success(),
246 Err(error) => AuthCacheEntry::failure(&error),
247 };
248 self.auth_cache.insert(cache_key, result.clone());
249 result
250 };
251
252 if !result.success {
253 return Err(AuthServiceError::new(
254 AuthOperation::Authenticate,
255 result.error_kind.unwrap_or(AuthFailureKind::Unauthenticated),
256 ));
257 }
258
259 Ok(())
260 })
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use std::any::Any;
267
268 use super::*;
269 use crate::authentication::provider::AuthenticationProvider;
270 use crate::authentication::provider::DefaultAuthenticationProvider;
271 use cheetah_string::CheetahString;
272
273 struct CountingAuthenticationProvider {
274 calls: Arc<std::sync::atomic::AtomicUsize>,
275 should_succeed: Arc<std::sync::atomic::AtomicBool>,
276 }
277
278 impl AuthenticationProvider for CountingAuthenticationProvider {
279 type Context = DefaultAuthenticationContext;
280
281 async fn initialize(
282 &mut self,
283 _config: AuthConfig,
284 _metadata_service: Option<Arc<dyn Any + Send + Sync>>,
285 ) -> AuthServiceResult<()> {
286 Ok(())
287 }
288
289 async fn authenticate(&self, _context: &Self::Context) -> AuthServiceResult<()> {
290 self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
291 if self.should_succeed.load(std::sync::atomic::Ordering::SeqCst) {
292 Ok(())
293 } else {
294 Err(AuthServiceError::new(
295 AuthOperation::Authenticate,
296 AuthFailureKind::Unauthenticated,
297 ))
298 }
299 }
300
301 fn new_context_from_metadata(
302 &self,
303 _metadata: &std::collections::HashMap<String, String>,
304 _request: Box<dyn Any + Send>,
305 ) -> Self::Context {
306 DefaultAuthenticationContext::new()
307 }
308
309 fn new_context_from_command(
310 &self,
311 _command: &rocketmq_protocol::protocol::remoting_command::RemotingCommand,
312 ) -> Self::Context {
313 DefaultAuthenticationContext::new()
314 }
315 }
316
317 #[test]
318 fn test_stateful_strategy_creation() {
319 let config = AuthConfig::default();
320 let provider = Arc::new(DefaultAuthenticationProvider::new());
321
322 let strategy = StatefulAuthenticationStrategy::new(config, Some(provider));
323
324 assert!(strategy.provider().is_some());
325 assert!(strategy.authentication_white_set().is_empty());
326 }
327
328 #[test]
329 fn test_build_cache_key_with_username() {
330 let config = AuthConfig::default();
331 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
332 StatefulAuthenticationStrategy::new(config, None);
333
334 let mut context = DefaultAuthenticationContext::new();
335 context.base.set_channel_id(Some(CheetahString::from("channel-123")));
336 context.set_username(CheetahString::from("user-456"));
337
338 let key = strategy.build_cache_key(&context).unwrap();
339 assert_eq!(key, "0#channel-123#user-456");
340 }
341
342 #[test]
343 fn test_build_cache_key_without_username() {
344 let config = AuthConfig::default();
345 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
346 StatefulAuthenticationStrategy::new(config, None);
347
348 let mut context = DefaultAuthenticationContext::new();
349 context.base.set_channel_id(Some(CheetahString::from("channel-789")));
350
351 let key = strategy.build_cache_key(&context).unwrap();
352 assert_eq!(key, "0#channel-789");
353 }
354
355 #[test]
356 fn test_build_cache_key_includes_acl_generation() {
357 let config = AuthConfig::default();
358 let acl_generation = Arc::new(AtomicU64::new(7));
359 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
360 StatefulAuthenticationStrategy::new_with_acl_generation(config, None, acl_generation.clone());
361
362 let mut context = DefaultAuthenticationContext::new();
363 context.base.set_channel_id(Some(CheetahString::from("channel-789")));
364
365 let key = strategy.build_cache_key(&context).unwrap();
366 assert_eq!(key, "7#channel-789");
367
368 acl_generation.store(8, Ordering::Release);
369 let key = strategy.build_cache_key(&context).unwrap();
370 assert_eq!(key, "8#channel-789");
371 }
372
373 #[tokio::test]
374 async fn test_authentication_disabled() {
375 let config = AuthConfig {
376 authentication_enabled: false,
377 ..Default::default()
378 };
379
380 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
381 StatefulAuthenticationStrategy::new(config, None);
382
383 let context = DefaultAuthenticationContext::new();
384 let result = strategy.authenticate(&context).await;
385 assert!(result.is_ok());
386 }
387
388 #[test]
389 fn test_cache_expiration_config() {
390 let config = AuthConfig {
391 stateful_authentication_cache_max_num: 500,
392 stateful_authentication_cache_expired_second: 30,
393 ..Default::default()
394 };
395
396 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
397 StatefulAuthenticationStrategy::new(config, None);
398
399 assert_eq!(strategy.auth_config.stateful_authentication_cache_max_num, 500);
400 assert_eq!(strategy.auth_config.stateful_authentication_cache_expired_second, 30);
401 }
402
403 #[test]
404 fn test_whitelist_parsing() {
405 let config = AuthConfig {
406 authentication_whitelist: "SEND_MESSAGE,PULL_MESSAGE".into(),
407 ..Default::default()
408 };
409
410 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
411 StatefulAuthenticationStrategy::new(config, None);
412
413 assert!(strategy.authentication_white_set().contains("SEND_MESSAGE"));
414 assert!(strategy.authentication_white_set().contains("PULL_MESSAGE"));
415 }
416
417 #[tokio::test]
418 async fn test_authentication_without_channel_id() {
419 let config = AuthConfig {
420 authentication_enabled: false,
421 ..Default::default()
422 };
423
424 let strategy: StatefulAuthenticationStrategy<DefaultAuthenticationProvider> =
425 StatefulAuthenticationStrategy::new(config, None);
426
427 let context = DefaultAuthenticationContext::new();
428
429 let result = strategy.authenticate(&context).await;
430 assert!(result.is_ok());
431 }
432
433 #[tokio::test(flavor = "multi_thread")]
434 async fn test_acl_generation_change_invalidates_cached_authentication_result() {
435 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
436 let should_succeed = Arc::new(std::sync::atomic::AtomicBool::new(true));
437 let provider = Arc::new(CountingAuthenticationProvider {
438 calls: calls.clone(),
439 should_succeed: should_succeed.clone(),
440 });
441 let acl_generation = Arc::new(AtomicU64::new(0));
442 let strategy = StatefulAuthenticationStrategy::new_with_acl_generation(
443 AuthConfig {
444 authentication_enabled: true,
445 ..AuthConfig::default()
446 },
447 Some(provider),
448 acl_generation.clone(),
449 );
450 let mut context = DefaultAuthenticationContext::new();
451 context.base.set_channel_id(Some(CheetahString::from("channel-1")));
452 context.set_username(CheetahString::from("alice"));
453
454 assert!(strategy.authenticate(&context).await.is_ok());
455 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
456
457 should_succeed.store(false, std::sync::atomic::Ordering::SeqCst);
458 assert!(strategy.authenticate(&context).await.is_ok());
459 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
460
461 acl_generation.fetch_add(1, Ordering::AcqRel);
462 let result = strategy.authenticate(&context).await;
463
464 assert!(result.is_err());
465 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 2);
466 }
467
468 #[tokio::test(flavor = "current_thread")]
469 async fn authenticates_inside_current_thread_runtime_without_block_in_place_panic() {
470 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
471 let should_succeed = Arc::new(std::sync::atomic::AtomicBool::new(true));
472 let provider = Arc::new(CountingAuthenticationProvider {
473 calls: calls.clone(),
474 should_succeed,
475 });
476 let strategy = StatefulAuthenticationStrategy::new(
477 AuthConfig {
478 authentication_enabled: true,
479 ..AuthConfig::default()
480 },
481 Some(provider),
482 );
483 let mut context = DefaultAuthenticationContext::new();
484 context
485 .base
486 .set_channel_id(Some(CheetahString::from("channel-current-thread")));
487 context.set_username(CheetahString::from("alice"));
488 assert!(strategy.authenticate(&context).await.is_ok());
489
490 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
491 }
492}