rocketmq_auth/authentication/builder/
default_authentication_context_builder.rs1use std::collections::BTreeMap;
21use std::collections::HashMap;
22
23use base64::Engine;
24use cheetah_string::CheetahString;
25use chrono::DateTime;
26use chrono::NaiveDateTime;
27use rocketmq_model::common::mix_all::UNIQUE_MSG_QUERY_FLAG;
28use rocketmq_model::common::mq_version::RocketMqVersion;
29use rocketmq_protocol::protocol::remoting_command::RemotingCommand;
30use tracing::warn;
31
32use crate::authentication::builder::AuthenticationContextBuilder;
33use crate::authentication::context::default_authentication_context::DefaultAuthenticationContext;
34use crate::AuthFailureKind;
35use crate::AuthOperation;
36use crate::AuthServiceError;
37use crate::AuthServiceResult;
38
39const CREDENTIAL: &str = "Credential";
41const SIGNATURE: &str = "Signature";
42const ACCESS_KEY: &str = "AccessKey";
43const SIGNATURE_FIELD: &str = "Signature";
44const X_MQ_DATE_TIME: &str = "x-mq-date-time";
45const DATE_TIME: &str = "DateTime";
46const TIMESTAMP: &str = "Timestamp";
47const TIMESTAMP_LOWERCASE: &str = "timestamp";
48const MALFORMED_GRPC_AUTHORIZATION_FRAGMENT_REASON: &str = "malformed_grpc_authorization_fragment";
49
50const SPACE: char = ' ';
52const COMMA: char = ',';
53const EQUAL: char = '=';
54const SLASH: char = '/';
55
56#[derive(Debug, Default, Clone, Copy)]
88pub struct DefaultAuthenticationContextBuilder;
89
90impl DefaultAuthenticationContextBuilder {
91 pub fn new() -> Self {
93 Self
94 }
95
96 pub fn build_from_grpc_metadata_map(
101 &self,
102 metadata: &HashMap<String, String>,
103 request: &dyn std::any::Any,
104 ) -> AuthServiceResult<DefaultAuthenticationContext> {
105 Self::build_from_grpc_values(
106 request,
107 Self::metadata_value(metadata, "authorization"),
108 Self::metadata_value(metadata, X_MQ_DATE_TIME),
109 Self::metadata_value(metadata, "channel-id"),
110 )
111 }
112
113 fn hex_to_base64(hex_input: &str) -> AuthServiceResult<String> {
127 let bytes = hex::decode(hex_input).map_err(|source| {
128 AuthServiceError::with_source(AuthOperation::BuildContext, AuthFailureKind::InvalidInput, source)
129 })?;
130 Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
131 }
132
133 pub(crate) fn combine_request_content(
147 request: &RemotingCommand,
148 sorted_fields: &BTreeMap<CheetahString, CheetahString>,
149 ) -> Vec<u8> {
150 let mut content = Vec::new();
151
152 for value in sorted_fields.values() {
155 content.extend_from_slice(value.as_bytes());
156 }
157
158 if let Some(body) = request.body() {
160 content.extend_from_slice(body);
161 }
162
163 content
164 }
165
166 fn parse_request_timestamp_millis(input: &str) -> Option<i64> {
167 let value = input.trim();
168 if value.is_empty() {
169 return None;
170 }
171
172 if value.bytes().all(|byte| byte.is_ascii_digit()) {
173 let timestamp = value.parse::<i64>().ok()?;
174 return if value.len() == 10 {
175 timestamp.checked_mul(1000)
176 } else {
177 Some(timestamp)
178 };
179 }
180
181 if let Ok(datetime) = NaiveDateTime::parse_from_str(value, "%Y%m%dT%H%M%SZ") {
182 return Some(datetime.and_utc().timestamp_millis());
183 }
184
185 DateTime::parse_from_rfc3339(value)
186 .ok()
187 .map(|datetime| datetime.timestamp_millis())
188 }
189
190 fn set_request_timestamp(context: &mut DefaultAuthenticationContext, value: &str) {
191 context.set_request_timestamp(CheetahString::from(value));
192 if let Some(timestamp_millis) = Self::parse_request_timestamp_millis(value) {
193 context.set_request_timestamp_millis(timestamp_millis);
194 }
195 }
196
197 fn metadata_value<'a>(metadata: &'a HashMap<String, String>, key: &str) -> Option<&'a str> {
198 metadata.get(key).map(String::as_str).or_else(|| {
199 metadata
200 .iter()
201 .find_map(|(candidate, value)| candidate.eq_ignore_ascii_case(key).then_some(value.as_str()))
202 })
203 }
204
205 fn build_from_grpc_values(
206 request: &dyn std::any::Any,
207 authorization: Option<&str>,
208 datetime: Option<&str>,
209 channel_id: Option<&str>,
210 ) -> AuthServiceResult<DefaultAuthenticationContext> {
211 let mut context = DefaultAuthenticationContext::new();
212
213 if let Some(channel_id) = channel_id {
214 context.base.set_channel_id(Some(CheetahString::from(channel_id)));
215 }
216
217 let rpc_code = std::any::type_name_of_val(request);
218 context.base.set_rpc_code(Some(CheetahString::from(rpc_code)));
219
220 let authorization = authorization.unwrap_or("");
221 if authorization.is_empty() {
222 return Ok(context);
223 }
224
225 let datetime = datetime
226 .ok_or_else(|| AuthServiceError::new(AuthOperation::BuildContext, AuthFailureKind::Unauthenticated))?;
227 Self::set_request_timestamp(&mut context, datetime);
228
229 let parts: Vec<&str> = authorization.splitn(2, SPACE).collect();
230 if parts.len() != 2 {
231 return Err(AuthServiceError::new(
232 AuthOperation::BuildContext,
233 AuthFailureKind::Unauthenticated,
234 ));
235 }
236
237 let key_values: Vec<&str> = parts[1].split(COMMA).collect();
238 for key_value in key_values {
239 let kv: Vec<&str> = key_value.trim().splitn(2, EQUAL).collect();
240 if kv.len() != 2 {
241 warn!(
242 reason = MALFORMED_GRPC_AUTHORIZATION_FRAGMENT_REASON,
243 "Ignoring malformed gRPC authorization fragment"
244 );
245 continue;
246 }
247
248 let auth_item = kv[0];
249 let value = kv[1];
250
251 match auth_item {
252 CREDENTIAL => {
253 let credential_parts: Vec<&str> = value.split(SLASH).collect();
254 if credential_parts.is_empty() {
255 return Err(AuthServiceError::new(
256 AuthOperation::BuildContext,
257 AuthFailureKind::Unauthenticated,
258 ));
259 }
260 context.set_username(CheetahString::from(credential_parts[0]));
261 }
262 SIGNATURE => {
263 let base64_signature = Self::hex_to_base64(value)?;
264 context.set_signature(CheetahString::from(base64_signature));
265 }
266 _ => {}
267 }
268 }
269
270 context.set_content(datetime.as_bytes().to_vec());
271
272 Ok(context)
273 }
274
275 fn remoting_request_timestamp(fields: &std::collections::HashMap<CheetahString, CheetahString>) -> Option<&str> {
276 fields
277 .get(&CheetahString::from(TIMESTAMP))
278 .or_else(|| fields.get(&CheetahString::from(TIMESTAMP_LOWERCASE)))
279 .or_else(|| fields.get(&CheetahString::from(DATE_TIME)))
280 .or_else(|| fields.get(&CheetahString::from(X_MQ_DATE_TIME)))
281 .map(CheetahString::as_str)
282 }
283}
284
285impl AuthenticationContextBuilder<DefaultAuthenticationContext> for DefaultAuthenticationContextBuilder {
286 #[cfg(feature = "grpc")]
287 fn build_from_grpc(
288 &self,
289 metadata: &tonic::metadata::MetadataMap,
290 request: &dyn std::any::Any,
291 ) -> AuthServiceResult<DefaultAuthenticationContext> {
292 Self::build_from_grpc_values(
293 request,
294 metadata.get("authorization").and_then(|v| v.to_str().ok()),
295 metadata.get(X_MQ_DATE_TIME).and_then(|v| v.to_str().ok()),
296 metadata.get("channel-id").and_then(|v| v.to_str().ok()),
297 )
298 }
299
300 fn build_from_remoting(
301 &self,
302 request: &RemotingCommand,
303 channel_id: Option<&str>,
304 ) -> AuthServiceResult<DefaultAuthenticationContext> {
305 let mut context = DefaultAuthenticationContext::new();
306
307 if let Some(id) = channel_id {
309 context.base.set_channel_id(Some(CheetahString::from(id)));
310 }
311
312 context
314 .base
315 .set_rpc_code(Some(CheetahString::from(request.code().to_string())));
316
317 let fields = match request.ext_fields() {
319 Some(fields) => fields,
320 None => return Ok(context), };
322
323 let access_key = match fields.get(&CheetahString::from(ACCESS_KEY)) {
325 Some(key) => key,
326 None => return Ok(context), };
328
329 context.set_username(access_key.clone());
331
332 if let Some(signature) = fields.get(&CheetahString::from(SIGNATURE_FIELD)) {
334 context.set_signature(signature.clone());
335 }
336
337 if let Some(request_timestamp) = Self::remoting_request_timestamp(fields) {
338 Self::set_request_timestamp(&mut context, request_timestamp);
339 }
340
341 let mut sorted_map = BTreeMap::new();
343
344 for (key, value) in fields.iter() {
345 if request.version() <= RocketMqVersion::V4_9_3.ordinal() as i32 && key.as_str() == UNIQUE_MSG_QUERY_FLAG {
347 continue;
348 }
349
350 if key.as_str() != SIGNATURE_FIELD {
352 sorted_map.insert(key.clone(), value.clone());
353 }
354 }
355
356 let content = Self::combine_request_content(request, &sorted_map);
358 context.set_content(content);
359
360 Ok(context)
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367 use std::collections::HashMap;
368 use std::sync::Arc;
369 use std::sync::Mutex;
370
371 use tracing_subscriber::fmt::MakeWriter;
372
373 #[derive(Clone)]
374 struct LogCapture {
375 output: Arc<Mutex<Vec<u8>>>,
376 }
377
378 impl LogCapture {
379 fn new() -> Self {
380 Self {
381 output: Arc::new(Mutex::new(Vec::new())),
382 }
383 }
384
385 fn contents(&self) -> String {
386 String::from_utf8(
387 self.output
388 .lock()
389 .expect("test log capture lock should not be poisoned")
390 .clone(),
391 )
392 .expect("test log output should be valid UTF-8")
393 }
394 }
395
396 struct LogCaptureWriter {
397 output: Arc<Mutex<Vec<u8>>>,
398 }
399
400 impl std::io::Write for LogCaptureWriter {
401 fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
402 self.output
403 .lock()
404 .expect("test log capture lock should not be poisoned")
405 .extend_from_slice(buffer);
406 Ok(buffer.len())
407 }
408
409 fn flush(&mut self) -> std::io::Result<()> {
410 Ok(())
411 }
412 }
413
414 impl<'a> MakeWriter<'a> for LogCapture {
415 type Writer = LogCaptureWriter;
416
417 fn make_writer(&'a self) -> Self::Writer {
418 LogCaptureWriter {
419 output: Arc::clone(&self.output),
420 }
421 }
422 }
423
424 #[test]
425 fn test_hex_to_base64() {
426 let hex = "48656C6C6F";
427 let result = DefaultAuthenticationContextBuilder::hex_to_base64(hex).unwrap();
428 assert_eq!(result, "SGVsbG8=");
429 }
430
431 #[test]
432 fn test_hex_to_base64_invalid() {
433 let hex = "ZZZZ";
434 let result = DefaultAuthenticationContextBuilder::hex_to_base64(hex);
435 assert!(result.is_err());
436 }
437
438 #[test]
439 fn test_combine_request_content_matches_java_value_only_order() {
440 let request = RemotingCommand::create_remoting_command(10).set_body(Vec::from("body"));
441 let mut sorted_fields = BTreeMap::new();
442 sorted_fields.insert(CheetahString::from("AccessKey"), CheetahString::from("alice"));
443 sorted_fields.insert(CheetahString::from("topic"), CheetahString::from("topic-a"));
444
445 let content = DefaultAuthenticationContextBuilder::combine_request_content(&request, &sorted_fields);
446
447 assert_eq!(content, b"alicetopic-abody");
448 }
449
450 #[test]
451 fn parse_request_timestamp_millis_supports_java_grpc_format() {
452 let timestamp = DefaultAuthenticationContextBuilder::parse_request_timestamp_millis("20231227T194619Z");
453
454 assert_eq!(timestamp, Some(1_703_706_379_000));
455 }
456
457 #[test]
458 fn parse_request_timestamp_millis_supports_epoch_seconds_and_millis() {
459 assert_eq!(
460 DefaultAuthenticationContextBuilder::parse_request_timestamp_millis("1703706379"),
461 Some(1_703_706_379_000)
462 );
463 assert_eq!(
464 DefaultAuthenticationContextBuilder::parse_request_timestamp_millis("1703706379000"),
465 Some(1_703_706_379_000)
466 );
467 }
468
469 #[test]
470 fn parse_request_timestamp_millis_supports_rfc3339() {
471 let timestamp = DefaultAuthenticationContextBuilder::parse_request_timestamp_millis("2023-12-27T19:46:19Z");
472
473 assert_eq!(timestamp, Some(1_703_706_379_000));
474 }
475
476 #[test]
477 fn build_from_grpc_metadata_map_parses_authorization_header() {
478 let builder = DefaultAuthenticationContextBuilder::new();
479 let mut metadata = HashMap::new();
480 metadata.insert(
481 "Authorization".to_owned(),
482 "MQv2-HMAC-SHA1 Credential=alice/us-east/service, SignedHeaders=x-mq-date-time, Signature=48656C6C6F"
483 .to_owned(),
484 );
485 metadata.insert("X-Mq-Date-Time".to_owned(), "20231227T194619Z".to_owned());
486 metadata.insert("Channel-Id".to_owned(), "channel-a".to_owned());
487
488 let context = builder.build_from_grpc_metadata_map(&metadata, &()).unwrap();
489
490 assert_eq!(context.username(), Some(&CheetahString::from("alice")));
491 assert_eq!(context.signature(), Some(&CheetahString::from("SGVsbG8=")));
492 assert_eq!(context.content(), Some(b"20231227T194619Z".as_slice()));
493 assert_eq!(context.base.channel_id(), Some(&CheetahString::from("channel-a")));
494 assert_eq!(context.request_timestamp_millis(), Some(1_703_706_379_000));
495 }
496
497 #[test]
498 fn malformed_grpc_authorization_fragment_is_not_logged() {
499 const SYNTHETIC_CREDENTIAL: &str = "SYNTHETIC_CREDENTIAL_9720";
500 const SYNTHETIC_FRAGMENT: &str = "SYNTHETIC_FRAGMENT_9720";
501 const SYNTHETIC_SIGNATURE_LIKE: &str = "SYNTHETIC_SIGNATURE_LIKE_9720";
502 const SYNTHETIC_TOKEN: &str = "SYNTHETIC_TOKEN_9720";
503 const SYNTHETIC_BODY: &str = "SYNTHETIC_BODY_9720";
504
505 let authorization = format!(
506 "MQv2-HMAC-SHA1 Credential={SYNTHETIC_CREDENTIAL}/scope, SignedHeaders=x-mq-date-time, {SYNTHETIC_FRAGMENT}:{SYNTHETIC_SIGNATURE_LIKE}:{SYNTHETIC_TOKEN}:{SYNTHETIC_BODY}"
507 );
508 let capture = LogCapture::new();
509 let subscriber = tracing_subscriber::fmt()
510 .with_writer(capture.clone())
511 .with_ansi(false)
512 .without_time()
513 .with_max_level(tracing::Level::WARN)
514 .finish();
515 let guard = tracing::subscriber::set_default(subscriber);
516 let builder = DefaultAuthenticationContextBuilder::new();
517 let mut metadata = HashMap::new();
518 metadata.insert("authorization".to_owned(), authorization.clone());
519 metadata.insert("x-mq-date-time".to_owned(), "20231227T194619Z".to_owned());
520
521 let context = builder.build_from_grpc_metadata_map(&metadata, &()).unwrap();
522
523 drop(guard);
524 let logs = capture.contents();
525 assert_eq!(context.username(), Some(&CheetahString::from(SYNTHETIC_CREDENTIAL)));
526 assert!(logs.contains(MALFORMED_GRPC_AUTHORIZATION_FRAGMENT_REASON));
527 assert!(logs.contains("Ignoring malformed gRPC authorization fragment"));
528 assert!(!logs.contains(SYNTHETIC_FRAGMENT));
529 assert!(!logs.contains(SYNTHETIC_SIGNATURE_LIKE));
530 assert!(!logs.contains(SYNTHETIC_TOKEN));
531 assert!(!logs.contains(SYNTHETIC_BODY));
532 assert!(!logs.contains(SYNTHETIC_CREDENTIAL));
533 assert!(!logs.contains(&authorization));
534 }
535
536 #[test]
537 fn test_build_from_remoting_no_ext_fields() {
538 use rocketmq_protocol::protocol::command_custom_header::CommandCustomHeader;
539
540 #[derive(Debug, Clone)]
541 struct EmptyHeader;
542 impl CommandCustomHeader for EmptyHeader {
543 fn to_map(
544 &self,
545 ) -> Option<std::collections::HashMap<cheetah_string::CheetahString, cheetah_string::CheetahString>>
546 {
547 None
548 }
549 }
550
551 let builder = DefaultAuthenticationContextBuilder::new();
552 let request = RemotingCommand::create_request_command(1, EmptyHeader);
553 let result = builder.build_from_remoting(&request, Some("test-channel")).unwrap();
554
555 assert_eq!(result.base.channel_id(), Some(&CheetahString::from("test-channel")));
556 assert!(result.username().is_none());
557 }
558
559 #[test]
560 fn test_build_from_remoting_uses_java_signature_content() {
561 let mut ext_fields = HashMap::new();
562 ext_fields.insert(CheetahString::from("topic"), CheetahString::from("topic-a"));
563 ext_fields.insert(CheetahString::from("AccessKey"), CheetahString::from("alice"));
564 ext_fields.insert(CheetahString::from("Signature"), CheetahString::from("sig"));
565
566 let request = RemotingCommand::create_remoting_command(10)
567 .set_ext_fields(ext_fields)
568 .set_body(Vec::from("body"));
569 let builder = DefaultAuthenticationContextBuilder::new();
570
571 let context = builder.build_from_remoting(&request, Some("test-channel")).unwrap();
572
573 assert_eq!(context.username(), Some(&CheetahString::from("alice")));
574 assert_eq!(context.signature(), Some(&CheetahString::from("sig")));
575 assert_eq!(context.content(), Some(b"alicetopic-abody".as_slice()));
576 }
577
578 #[test]
579 fn test_build_from_remoting_extracts_request_timestamp() {
580 let mut ext_fields = HashMap::new();
581 ext_fields.insert(CheetahString::from("AccessKey"), CheetahString::from("alice"));
582 ext_fields.insert(CheetahString::from("Signature"), CheetahString::from("sig"));
583 ext_fields.insert(CheetahString::from("Timestamp"), CheetahString::from("1703706379000"));
584
585 let request = RemotingCommand::create_remoting_command(10).set_ext_fields(ext_fields);
586 let builder = DefaultAuthenticationContextBuilder::new();
587
588 let context = builder.build_from_remoting(&request, Some("test-channel")).unwrap();
589
590 assert_eq!(context.request_timestamp(), Some(&CheetahString::from("1703706379000")));
591 assert_eq!(context.request_timestamp_millis(), Some(1_703_706_379_000));
592 }
593}