rocketmq_auth/authentication/strategy/
stateless_authentication_strategy.rs1use std::collections::HashSet;
21use std::sync::Arc;
22
23use crate::authentication::context::default_authentication_context::DefaultAuthenticationContext;
24use crate::authentication::provider::AuthenticationProvider;
25use crate::authentication::strategy::abstract_authentication_strategy::AbstractAuthenticationStrategy;
26use crate::authentication::strategy::authenticate_with_provider;
27use crate::authentication::strategy::authentication_strategy::AuthenticationStrategy;
28use crate::authentication::strategy::AuthenticationFuture;
29use crate::authorization::context::authentication_context::AuthenticationContext;
30use crate::config::AuthConfig;
31use crate::AuthFailureKind;
32use crate::AuthOperation;
33use crate::AuthServiceError;
34use crate::AuthServiceResult;
35
36pub struct StatelessAuthenticationStrategy<P>
45where
46 P: AuthenticationProvider<Context = DefaultAuthenticationContext>,
47{
48 auth_config: AuthConfig,
49 authentication_white_set: HashSet<String>,
50 authentication_provider: Option<Arc<P>>,
51}
52
53impl<P> StatelessAuthenticationStrategy<P>
54where
55 P: AuthenticationProvider<Context = DefaultAuthenticationContext> + Send + Sync + 'static,
56{
57 pub fn new(auth_config: AuthConfig, provider: Option<Arc<P>>) -> Self {
59 let mut authentication_white_set = HashSet::new();
60
61 let whitelist = auth_config.authentication_whitelist.to_string();
62 if !whitelist.is_empty() {
63 for rpc_code in whitelist.split(',') {
64 let trimmed = rpc_code.trim();
65 if !trimmed.is_empty() {
66 authentication_white_set.insert(trimmed.to_string());
67 }
68 }
69 }
70
71 Self {
72 auth_config,
73 authentication_white_set,
74 authentication_provider: provider,
75 }
76 }
77
78 async fn do_authenticate_internal(&self, context: &dyn AuthenticationContext) -> AuthServiceResult<()> {
80 if !self.auth_config.authentication_enabled {
81 return Ok(());
82 }
83
84 let default_context = context
85 .as_any()
86 .downcast_ref::<DefaultAuthenticationContext>()
87 .ok_or_else(|| AuthServiceError::new(AuthOperation::Authenticate, AuthFailureKind::InvalidInput))?;
88
89 if let Some(rpc_code) = default_context.base.rpc_code() {
90 if self.is_whitelisted(rpc_code.as_str()) {
91 return Ok(());
92 }
93 }
94
95 let provider = match &self.authentication_provider {
96 Some(p) => p,
97 None => return Ok(()),
98 };
99
100 authenticate_with_provider(provider.as_ref(), default_context).await
101 }
102
103 pub fn provider(&self) -> Option<&P> {
104 self.authentication_provider.as_deref()
105 }
106}
107
108impl<P> AbstractAuthenticationStrategy for StatelessAuthenticationStrategy<P>
109where
110 P: AuthenticationProvider<Context = DefaultAuthenticationContext> + Send + Sync + 'static,
111{
112 fn authentication_white_set(&self) -> &HashSet<String> {
113 &self.authentication_white_set
114 }
115}
116
117impl<P> AuthenticationStrategy for StatelessAuthenticationStrategy<P>
118where
119 P: AuthenticationProvider<Context = DefaultAuthenticationContext> + Send + Sync + 'static,
120{
121 fn authenticate<'a>(&'a self, context: &'a dyn AuthenticationContext) -> AuthenticationFuture<'a> {
122 Box::pin(self.do_authenticate_internal(context))
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use std::any::Any;
129
130 use super::*;
131 use crate::authentication::provider::DefaultAuthenticationProvider;
132 use cheetah_string::CheetahString;
133
134 struct CountingAuthenticationProvider {
135 calls: Arc<std::sync::atomic::AtomicUsize>,
136 }
137
138 impl AuthenticationProvider for CountingAuthenticationProvider {
139 type Context = DefaultAuthenticationContext;
140
141 async fn initialize(
142 &mut self,
143 _config: AuthConfig,
144 _metadata_service: Option<Arc<dyn Any + Send + Sync>>,
145 ) -> AuthServiceResult<()> {
146 Ok(())
147 }
148
149 async fn authenticate(&self, _context: &Self::Context) -> AuthServiceResult<()> {
150 self.calls.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
151 Ok(())
152 }
153
154 fn new_context_from_metadata(
155 &self,
156 _metadata: &std::collections::HashMap<String, String>,
157 _request: Box<dyn Any + Send>,
158 ) -> Self::Context {
159 DefaultAuthenticationContext::new()
160 }
161
162 fn new_context_from_command(
163 &self,
164 _command: &rocketmq_protocol::protocol::remoting_command::RemotingCommand,
165 ) -> Self::Context {
166 DefaultAuthenticationContext::new()
167 }
168 }
169
170 #[test]
171 fn test_stateless_strategy_creation() {
172 let config = AuthConfig::default();
173 let provider = Arc::new(DefaultAuthenticationProvider::new());
174
175 let strategy = StatelessAuthenticationStrategy::new(config, Some(provider));
176
177 assert!(strategy.provider().is_some());
178 assert!(strategy.authentication_white_set().is_empty());
179 }
180
181 #[tokio::test]
182 async fn test_authentication_disabled() {
183 let config = AuthConfig {
184 authentication_enabled: false,
185 ..Default::default()
186 };
187
188 let strategy: StatelessAuthenticationStrategy<DefaultAuthenticationProvider> =
189 StatelessAuthenticationStrategy::new(config, None);
190
191 let context = DefaultAuthenticationContext::new();
192 let result = strategy.authenticate(&context).await;
193 assert!(result.is_ok());
194 }
195
196 #[test]
197 fn test_whitelist_parsing() {
198 let config = AuthConfig {
199 authentication_whitelist: "SEND_MESSAGE,PULL_MESSAGE".into(),
200 ..Default::default()
201 };
202
203 let strategy: StatelessAuthenticationStrategy<DefaultAuthenticationProvider> =
204 StatelessAuthenticationStrategy::new(config, None);
205
206 assert!(strategy.authentication_white_set().contains("SEND_MESSAGE"));
207 assert!(strategy.authentication_white_set().contains("PULL_MESSAGE"));
208 }
209
210 #[test]
211 fn test_whitelist_with_spaces() {
212 let config = AuthConfig {
213 authentication_whitelist: " SEND_MESSAGE , PULL_MESSAGE , QUERY_MESSAGE ".into(),
214 ..Default::default()
215 };
216
217 let strategy: StatelessAuthenticationStrategy<DefaultAuthenticationProvider> =
218 StatelessAuthenticationStrategy::new(config, None);
219
220 assert_eq!(strategy.authentication_white_set().len(), 3);
221 assert!(strategy.authentication_white_set().contains("SEND_MESSAGE"));
222 assert!(strategy.authentication_white_set().contains("PULL_MESSAGE"));
223 assert!(strategy.authentication_white_set().contains("QUERY_MESSAGE"));
224 }
225
226 #[tokio::test]
227 async fn test_authentication_enabled_without_provider() {
228 let config = AuthConfig {
229 authentication_enabled: true,
230 ..Default::default()
231 };
232
233 let strategy: StatelessAuthenticationStrategy<DefaultAuthenticationProvider> =
234 StatelessAuthenticationStrategy::new(config, None);
235
236 let context = DefaultAuthenticationContext::new();
237 let result = strategy.authenticate(&context).await;
238 assert!(result.is_ok());
239 }
240
241 #[tokio::test]
242 async fn test_whitelisted_rpc_code() {
243 let config = AuthConfig {
244 authentication_enabled: true,
245 authentication_whitelist: "100,200".into(),
246 ..Default::default()
247 };
248
249 let strategy: StatelessAuthenticationStrategy<DefaultAuthenticationProvider> =
250 StatelessAuthenticationStrategy::new(config, None);
251
252 let mut context = DefaultAuthenticationContext::new();
253 context.base.set_rpc_code(Some(CheetahString::from("100")));
254
255 let result = strategy.authenticate(&context).await;
256 assert!(result.is_ok());
257 }
258
259 #[tokio::test]
260 async fn test_no_caching_behavior() {
261 let config = AuthConfig {
262 authentication_enabled: false,
263 ..Default::default()
264 };
265
266 let strategy: StatelessAuthenticationStrategy<DefaultAuthenticationProvider> =
267 StatelessAuthenticationStrategy::new(config, None);
268
269 let context = DefaultAuthenticationContext::new();
270
271 let result1 = strategy.authenticate(&context).await;
272 assert!(result1.is_ok());
273
274 let result2 = strategy.authenticate(&context).await;
275 assert!(result2.is_ok());
276 }
277
278 #[tokio::test(flavor = "current_thread")]
279 async fn authenticates_inside_current_thread_runtime_without_block_in_place_panic() {
280 let calls = Arc::new(std::sync::atomic::AtomicUsize::new(0));
281 let provider = Arc::new(CountingAuthenticationProvider { calls: calls.clone() });
282 let strategy = StatelessAuthenticationStrategy::new(
283 AuthConfig {
284 authentication_enabled: true,
285 ..Default::default()
286 },
287 Some(provider),
288 );
289 let context = DefaultAuthenticationContext::new();
290 assert!(strategy.authenticate(&context).await.is_ok());
291
292 assert_eq!(calls.load(std::sync::atomic::Ordering::SeqCst), 1);
293 }
294}