1pub(crate) mod static_user_provider;
16pub(crate) mod watch_file_user_provider;
17
18use std::collections::HashMap;
19use std::fs::File;
20use std::io::BufRead;
21use std::path::Path;
22use std::{fmt, io};
23
24use common_base::secrets::ExposeSecret;
25use pbkdf2::pbkdf2_hmac;
26use sha2::Sha256;
27use snafu::{OptionExt, ResultExt, ensure};
28use subtle::ConstantTimeEq;
29
30use crate::common::{
31 DEFAULT_PBKDF2_SHA256_SALT_LEN, Identity, MAX_PBKDF2_SHA256_ITERATIONS,
32 MAX_PBKDF2_SHA256_SALT_LEN, PBKDF2_SHA256_HASH_LEN, PG_SCRAM_SHA256_KEY_LEN, Password,
33 PgScramSha256Verifier, auth_mysql_with_hash_stage_2,
34};
35use crate::error::{
36 IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedPasswordTypeSnafu,
37 UserNotFoundSnafu, UserPasswordMismatchSnafu,
38};
39use crate::user_info::{DefaultUserInfo, PermissionMode};
40use crate::{UserInfoRef, auth_mysql};
41
42#[async_trait::async_trait]
43pub trait UserProvider: Send + Sync {
44 fn name(&self) -> &str;
45
46 async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
48
49 async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
53
54 async fn auth(
57 &self,
58 id: Identity<'_>,
59 password: Password<'_>,
60 catalog: &str,
61 schema: &str,
62 ) -> Result<UserInfoRef> {
63 let user_info = self.authenticate(id, password).await?;
64 self.authorize(catalog, schema, &user_info).await?;
65 Ok(user_info)
66 }
67
68 async fn postgres_auth_info(&self, _id: Identity<'_>) -> Result<PgAuthInfo> {
69 Ok(PgAuthInfo::Cleartext)
70 }
71
72 fn external(&self) -> bool {
74 false
75 }
76}
77
78pub enum PgAuthInfo {
79 ScramSha256 {
80 verifier: PgScramSha256Verifier,
81 user_info: Option<UserInfoRef>,
82 },
83 Cleartext,
84}
85
86#[derive(Clone)]
87pub(crate) enum PasswordVerifier {
88 PlainText {
89 password: String,
90 scram: PgScramSha256Verifier,
96 },
97 Pbkdf2Sha256 {
98 iterations: u32,
99 salt: Vec<u8>,
100 hash: Vec<u8>,
101 },
102 MysqlNativePassword {
103 hash_stage_2: Vec<u8>,
104 },
105 PgScramSha256(PgScramSha256Verifier),
106}
107
108impl PartialEq for PasswordVerifier {
109 fn eq(&self, other: &Self) -> bool {
110 match (self, other) {
113 (
114 PasswordVerifier::PlainText { password: a, .. },
115 PasswordVerifier::PlainText { password: b, .. },
116 ) => a == b,
117 (
118 PasswordVerifier::Pbkdf2Sha256 {
119 iterations: i1,
120 salt: s1,
121 hash: h1,
122 },
123 PasswordVerifier::Pbkdf2Sha256 {
124 iterations: i2,
125 salt: s2,
126 hash: h2,
127 },
128 ) => i1 == i2 && s1 == s2 && h1 == h2,
129 (
130 PasswordVerifier::MysqlNativePassword { hash_stage_2: a },
131 PasswordVerifier::MysqlNativePassword { hash_stage_2: b },
132 ) => a == b,
133 (PasswordVerifier::PgScramSha256(a), PasswordVerifier::PgScramSha256(b)) => a == b,
134 _ => false,
135 }
136 }
137}
138
139impl Eq for PasswordVerifier {}
140
141impl fmt::Debug for PasswordVerifier {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 match self {
144 PasswordVerifier::PlainText { .. } => {
145 f.debug_tuple("PlainText").field(&"<REDACTED>").finish()
146 }
147 PasswordVerifier::Pbkdf2Sha256 { iterations, .. } => f
148 .debug_struct("Pbkdf2Sha256")
149 .field("iterations", iterations)
150 .field("salt", &"<REDACTED>")
151 .field("hash", &"<REDACTED>")
152 .finish(),
153 PasswordVerifier::MysqlNativePassword { .. } => f
154 .debug_struct("MysqlNativePassword")
155 .field("hash_stage_2", &"<REDACTED>")
156 .finish(),
157 PasswordVerifier::PgScramSha256(_) => f
158 .debug_struct("PgScramSha256")
159 .field("verifier", &"<REDACTED>")
160 .finish(),
161 }
162 }
163}
164
165impl PasswordVerifier {
166 fn plain_text(password: String) -> Option<Self> {
171 let salt = rand::random::<[u8; DEFAULT_PBKDF2_SHA256_SALT_LEN]>();
172 let scram = PgScramSha256Verifier::from_password(
173 password.as_bytes(),
174 &salt,
175 crate::DEFAULT_PBKDF2_SHA256_ITERATIONS,
176 )
177 .ok()?;
178 Some(Self::PlainText { password, scram })
179 }
180
181 fn parse(input: &str) -> Option<Self> {
182 if let Some(password) = input.strip_prefix("plain:") {
183 return Self::plain_text(password.to_string());
184 }
185
186 if let Some(verifier) = input.strip_prefix("pbkdf2_sha256:") {
187 let mut parts = verifier.split(':');
188 let iterations = parts.next()?.parse::<u32>().ok()?;
189 let salt = hex::decode(parts.next()?).ok()?;
190 let hash = hex::decode(parts.next()?).ok()?;
191 if parts.next().is_some()
192 || iterations == 0
193 || iterations > MAX_PBKDF2_SHA256_ITERATIONS
194 || salt.is_empty()
195 || salt.len() > MAX_PBKDF2_SHA256_SALT_LEN
196 || hash.len() != PBKDF2_SHA256_HASH_LEN
197 {
198 return None;
199 }
200
201 return Some(Self::Pbkdf2Sha256 {
202 iterations,
203 salt,
204 hash,
205 });
206 }
207
208 if let Some(verifier) = input.strip_prefix("mysql_native_password:") {
209 let hash_stage_2 = hex::decode(verifier).ok()?;
210 if hash_stage_2.len() != 20 {
211 return None;
212 }
213
214 return Some(Self::MysqlNativePassword { hash_stage_2 });
215 }
216
217 if let Some(verifier) = input.strip_prefix("pg_scram_sha256:") {
218 let mut parts = verifier.split(':');
219 let iterations = parts.next()?.parse::<u32>().ok()?;
220 let salt = hex::decode(parts.next()?).ok()?;
221 let stored_key = hex::decode(parts.next()?).ok()?;
222 let server_key = hex::decode(parts.next()?).ok()?;
223 if parts.next().is_some()
224 || stored_key.len() != PG_SCRAM_SHA256_KEY_LEN
225 || server_key.len() != PG_SCRAM_SHA256_KEY_LEN
226 {
227 return None;
228 }
229
230 return PgScramSha256Verifier::new(iterations, salt, stored_key, server_key)
231 .ok()
232 .map(Self::PgScramSha256);
233 }
234
235 Self::plain_text(input.to_string())
236 }
237
238 fn supports_pg_scram_sha256(&self) -> bool {
239 matches!(
240 self,
241 PasswordVerifier::PlainText { .. } | PasswordVerifier::PgScramSha256(_)
242 )
243 }
244
245 fn to_pg_scram_sha256_verifier(&self) -> Option<PgScramSha256Verifier> {
246 match self {
247 PasswordVerifier::PlainText { scram, .. } => Some(scram.clone()),
248 PasswordVerifier::Pbkdf2Sha256 { .. } => None,
251 PasswordVerifier::PgScramSha256(verifier) => Some(verifier.clone()),
252 PasswordVerifier::MysqlNativePassword { .. } => None,
253 }
254 }
255
256 fn verify_plain_text(&self, password: &str) -> bool {
257 match self {
258 PasswordVerifier::PlainText {
259 password: expected, ..
260 } => expected.as_bytes().ct_eq(password.as_bytes()).into(),
261 PasswordVerifier::Pbkdf2Sha256 {
262 iterations,
263 salt,
264 hash,
265 } => {
266 if hash.len() != PBKDF2_SHA256_HASH_LEN {
267 return false;
268 }
269 let mut actual = [0u8; PBKDF2_SHA256_HASH_LEN];
270 pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, *iterations, &mut actual);
271 hash.as_slice().ct_eq(&actual[..]).into()
272 }
273 PasswordVerifier::MysqlNativePassword { .. } => false,
274 PasswordVerifier::PgScramSha256(verifier) => verifier
275 .verify_plain_password(password.as_bytes())
276 .unwrap_or(false),
277 }
278 }
279
280 fn verify_mysql_native_password(
281 &self,
282 auth_data: &[u8],
283 salt: &[u8],
284 username: &str,
285 ) -> Result<()> {
286 match self {
287 PasswordVerifier::PlainText { password, .. } => {
288 auth_mysql(auth_data, salt, username, password.as_bytes())
289 }
290 PasswordVerifier::MysqlNativePassword { hash_stage_2 } => {
291 auth_mysql_with_hash_stage_2(auth_data, salt, username, hash_stage_2)
292 }
293 PasswordVerifier::Pbkdf2Sha256 { .. } => UnsupportedPasswordTypeSnafu {
294 password_type: "mysql_native_password_with_pbkdf2_sha256_verifier",
295 }
296 .fail(),
297 PasswordVerifier::PgScramSha256(_) => UnsupportedPasswordTypeSnafu {
298 password_type: "mysql_native_password_with_pg_scram_sha256_verifier",
299 }
300 .fail(),
301 }
302 }
303}
304
305pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
308
309fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
310 let path = Path::new(filepath);
312 if !path.exists() {
313 return InvalidConfigSnafu {
314 value: filepath.to_string(),
315 msg: "UserProvider file must exist",
316 }
317 .fail();
318 }
319
320 ensure!(
321 path.is_file(),
322 InvalidConfigSnafu {
323 value: filepath,
324 msg: "UserProvider file must be a file",
325 }
326 );
327 let file = File::open(path).context(IoSnafu)?;
328 let credential = io::BufReader::new(file)
329 .lines()
330 .map_while(std::result::Result::ok)
331 .filter_map(|line| {
332 let line = line.trim();
338 if line.is_empty() || line.starts_with('#') {
339 return None;
340 }
341
342 parse_credential_line(line)
343 })
344 .collect::<HashMap<String, _>>();
345
346 ensure!(
347 !credential.is_empty(),
348 InvalidConfigSnafu {
349 value: filepath,
350 msg: "UserProvider's file must contains at least one valid credential",
351 }
352 );
353
354 Ok(credential)
355}
356
357pub(crate) fn parse_credential_line(
365 line: &str,
366) -> Option<(String, (PasswordVerifier, PermissionMode))> {
367 let parts = line.split('=').collect::<Vec<&str>>();
368 if parts.len() != 2 {
369 return None;
370 }
371
372 let (username_part, password) = (parts[0], parts[1]);
373 let (username, permission_mode) = if let Some((user, perm)) = username_part.split_once(':') {
374 (user, PermissionMode::from_str(perm))
375 } else {
376 (username_part, PermissionMode::default())
377 };
378
379 let verifier = PasswordVerifier::parse(password)?;
380
381 Some((username.to_string(), (verifier, permission_mode)))
382}
383
384pub(crate) fn postgres_auth_info_with_credential(
385 users: &UserInfoMap,
386 input_id: Identity<'_>,
387) -> Result<PgAuthInfo> {
388 match input_id {
389 Identity::UserId(username, _) => {
390 ensure!(
391 !username.is_empty(),
392 IllegalParamSnafu {
393 msg: "blank username"
394 }
395 );
396
397 if !users
398 .values()
399 .all(|(verifier, _)| verifier.supports_pg_scram_sha256())
400 {
401 return Ok(PgAuthInfo::Cleartext);
404 }
405
406 if let Some((verifier, permission_mode)) = users.get(username) {
407 if let Some(verifier) = verifier.to_pg_scram_sha256_verifier() {
408 return Ok(PgAuthInfo::ScramSha256 {
409 verifier,
410 user_info: Some(DefaultUserInfo::with_name_and_permission(
411 username,
412 *permission_mode,
413 )),
414 });
415 }
416
417 return Ok(PgAuthInfo::Cleartext);
418 }
419
420 Ok(PgAuthInfo::ScramSha256 {
424 verifier: PgScramSha256Verifier::mock_for_unknown_user(username.as_bytes()),
425 user_info: None,
426 })
427 }
428 }
429}
430
431fn authenticate_with_credential(
432 users: &UserInfoMap,
433 input_id: Identity<'_>,
434 input_pwd: Password<'_>,
435) -> Result<UserInfoRef> {
436 match input_id {
437 Identity::UserId(username, _) => {
438 ensure!(
439 !username.is_empty(),
440 IllegalParamSnafu {
441 msg: "blank username"
442 }
443 );
444 let (verifier, permission_mode) = users.get(username).context(UserNotFoundSnafu {
445 username: username.to_string(),
446 })?;
447
448 match input_pwd {
449 Password::PlainText(pwd) => {
450 ensure!(
451 !pwd.expose_secret().is_empty(),
452 IllegalParamSnafu {
453 msg: "blank password"
454 }
455 );
456 if verifier.verify_plain_text(pwd.expose_secret()) {
457 Ok(DefaultUserInfo::with_name_and_permission(
458 username,
459 *permission_mode,
460 ))
461 } else {
462 UserPasswordMismatchSnafu {
463 username: username.to_string(),
464 }
465 .fail()
466 }
467 }
468 Password::MysqlNativePassword(auth_data, salt) => verifier
469 .verify_mysql_native_password(auth_data, salt, username)
470 .map(|_| DefaultUserInfo::with_name_and_permission(username, *permission_mode)),
471 Password::PgMD5(_, _) => UnsupportedPasswordTypeSnafu {
472 password_type: "pg_md5",
473 }
474 .fail(),
475 }
476 }
477 }
478}
479#[cfg(test)]
480mod tests {
481 use digest::Digest;
482 use sha1::Sha1;
483
484 use super::*;
485 use crate::common::{format_pg_scram_sha256_password_verifier, mysql_native_password_hash};
486
487 fn plain(password: &str) -> PasswordVerifier {
488 PasswordVerifier::plain_text(password.to_string()).unwrap()
489 }
490
491 fn sha1_one(data: &[u8]) -> Vec<u8> {
492 let mut hasher = Sha1::new();
493 hasher.update(data);
494 hasher.finalize().to_vec()
495 }
496
497 fn mysql_native_password_auth_data(password: &str, salt: &[u8]) -> Vec<u8> {
498 let hash_stage_1 = sha1_one(password.as_bytes());
499 let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
500 let mut hasher = Sha1::new();
501 hasher.update(salt);
502 hasher.update(hash_stage_2);
503 let scramble = hasher.finalize();
504
505 hash_stage_1
506 .iter()
507 .zip(scramble.iter())
508 .map(|(lhs, rhs)| lhs ^ rhs)
509 .collect()
510 }
511
512 #[test]
513 fn test_parse_credential_line() {
514 let result = parse_credential_line("admin=password123");
516 assert_eq!(
517 result,
518 Some((
519 "admin".to_string(),
520 (plain("password123"), PermissionMode::default())
521 ))
522 );
523
524 let result = parse_credential_line("user:ReadOnly=secret");
526 assert_eq!(
527 result,
528 Some((
529 "user".to_string(),
530 (plain("secret"), PermissionMode::ReadOnly)
531 ))
532 );
533 let result = parse_credential_line("user:ro=secret");
534 assert_eq!(
535 result,
536 Some((
537 "user".to_string(),
538 (plain("secret"), PermissionMode::ReadOnly)
539 ))
540 );
541 let result = parse_credential_line("writer:WriteOnly=mypass");
543 assert_eq!(
544 result,
545 Some((
546 "writer".to_string(),
547 (plain("mypass"), PermissionMode::WriteOnly)
548 ))
549 );
550
551 let result = parse_credential_line("writer:wo=mypass");
553 assert_eq!(
554 result,
555 Some((
556 "writer".to_string(),
557 (plain("mypass"), PermissionMode::WriteOnly)
558 ))
559 );
560
561 let result = parse_credential_line("admin:rw=p@ssw0rd!123");
563 assert_eq!(
564 result,
565 Some((
566 "admin".to_string(),
567 (plain("p@ssw0rd!123"), PermissionMode::ReadWrite)
568 ))
569 );
570
571 let result = parse_credential_line("user name:WriteOnly=password");
573 assert_eq!(
574 result,
575 Some((
576 "user name".to_string(),
577 (plain("password"), PermissionMode::WriteOnly)
578 ))
579 );
580
581 let result = parse_credential_line("user=plain:password");
582 assert_eq!(
583 result,
584 Some((
585 "user".to_string(),
586 (plain("password"), PermissionMode::default())
587 ))
588 );
589
590 let iterations = 4096;
591 let salt = b"salt";
592 let mut hash = [0u8; 32];
593 pbkdf2_hmac::<Sha256>("password".as_bytes(), salt, iterations, &mut hash);
594 let result = parse_credential_line(&format!(
595 "user=pbkdf2_sha256:{iterations}:{}:{}",
596 hex::encode(salt),
597 hex::encode(hash)
598 ));
599 assert_eq!(
600 result,
601 Some((
602 "user".to_string(),
603 (
604 PasswordVerifier::Pbkdf2Sha256 {
605 iterations,
606 salt: salt.to_vec(),
607 hash: hash.to_vec(),
608 },
609 PermissionMode::default()
610 )
611 ))
612 );
613
614 let result = parse_credential_line("user=pbkdf2_sha256:4096:not-hex:abcd");
615 assert_eq!(result, None);
616
617 let result = parse_credential_line(&format!(
620 "user=pbkdf2_sha256:4096:{}:abcd",
621 hex::encode(salt)
622 ));
623 assert_eq!(result, None);
624
625 let result = parse_credential_line(&format!(
626 "user=pbkdf2_sha256:{}:{}:{}",
627 MAX_PBKDF2_SHA256_ITERATIONS + 1,
628 hex::encode(salt),
629 hex::encode(hash)
630 ));
631 assert_eq!(result, None);
632
633 let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
634 let result = parse_credential_line(&format!(
635 "user=mysql_native_password:{}",
636 hex::encode(&hash_stage_2)
637 ));
638 assert_eq!(
639 result,
640 Some((
641 "user".to_string(),
642 (
643 PasswordVerifier::MysqlNativePassword { hash_stage_2 },
644 PermissionMode::default()
645 )
646 ))
647 );
648
649 let result = parse_credential_line("user=mysql_native_password:abcd");
650 assert_eq!(result, None);
651
652 let verifier =
653 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
654 let result = parse_credential_line(&format!("user={verifier}"));
655 assert!(matches!(
656 result,
657 Some((
658 _,
659 (
660 PasswordVerifier::PgScramSha256(PgScramSha256Verifier { .. }),
661 PermissionMode::ReadWrite
662 )
663 ))
664 ));
665
666 let result = parse_credential_line("user=pg_scram_sha256:4096:73616c74:abcd:abcd");
667 assert_eq!(result, None);
668
669 let result = parse_credential_line("invalid_line");
671 assert_eq!(result, None);
672
673 let result = parse_credential_line("user=pass=word");
675 assert_eq!(result, None);
676
677 let result = parse_credential_line("user=");
679 assert_eq!(
680 result,
681 Some(("user".to_string(), (plain(""), PermissionMode::default())))
682 );
683
684 let result = parse_credential_line("=password");
686 assert_eq!(
687 result,
688 Some((
689 "".to_string(),
690 (plain("password"), PermissionMode::default())
691 ))
692 );
693 }
694
695 #[test]
696 fn test_authenticate_with_mysql_native_password_verifier() {
697 let password = "password";
698 let salt = b"12345678901234567890";
699 let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
700 let auth_data = mysql_native_password_auth_data(password, salt);
701 let users = HashMap::from([(
702 "user".to_string(),
703 (
704 PasswordVerifier::MysqlNativePassword { hash_stage_2 },
705 PermissionMode::default(),
706 ),
707 )]);
708
709 let result = authenticate_with_credential(
710 &users,
711 Identity::UserId("user", None),
712 Password::MysqlNativePassword(&auth_data, salt),
713 );
714
715 assert!(result.is_ok());
716 }
717
718 #[test]
719 fn test_authenticate_with_plain_text_mysql_native_password() {
720 let password = "password";
721 let salt = b"12345678901234567890";
722 let auth_data = mysql_native_password_auth_data(password, salt);
723 let users = HashMap::from([(
724 "user".to_string(),
725 (
726 PasswordVerifier::plain_text(password.to_string()).unwrap(),
727 PermissionMode::default(),
728 ),
729 )]);
730
731 let result = authenticate_with_credential(
732 &users,
733 Identity::UserId("user", None),
734 Password::MysqlNativePassword(&auth_data, salt),
735 );
736
737 assert!(result.is_ok());
738 }
739
740 #[test]
741 fn test_pbkdf2_sha256_rejects_mysql_native_password() {
742 let password = "password";
743 let salt = b"salt";
744 let iterations = 4096;
745 let mut hash = [0u8; 32];
746 pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
747 let users = HashMap::from([(
748 "user".to_string(),
749 (
750 PasswordVerifier::Pbkdf2Sha256 {
751 iterations,
752 salt: salt.to_vec(),
753 hash: hash.to_vec(),
754 },
755 PermissionMode::default(),
756 ),
757 )]);
758 let mysql_salt = b"12345678901234567890";
759 let auth_data = mysql_native_password_auth_data(password, mysql_salt);
760
761 let result = authenticate_with_credential(
762 &users,
763 Identity::UserId("user", None),
764 Password::MysqlNativePassword(&auth_data, mysql_salt),
765 );
766
767 assert!(result.is_err());
768 }
769
770 #[test]
771 fn test_authenticate_with_pg_scram_sha256_verifier_plain_text() {
772 let verifier =
773 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
774 let (_, user) = parse_credential_line(&format!("user={verifier}")).unwrap();
775 let users = HashMap::from([("user".to_string(), user)]);
776
777 let result = authenticate_with_credential(
778 &users,
779 Identity::UserId("user", None),
780 Password::PlainText("password".to_string().into()),
781 );
782 assert!(result.is_ok());
783
784 let result = authenticate_with_credential(
785 &users,
786 Identity::UserId("user", None),
787 Password::PlainText("wrong".to_string().into()),
788 );
789 assert!(result.is_err());
790 }
791
792 #[test]
793 fn test_plain_text_scram_verifier_is_stable() {
794 let verifier = PasswordVerifier::plain_text("password".to_string()).unwrap();
795 let first = verifier.to_pg_scram_sha256_verifier().unwrap();
796 let second = verifier.to_pg_scram_sha256_verifier().unwrap();
797
798 assert_eq!(first.salt(), second.salt());
802 assert_eq!(first.salt().len(), DEFAULT_PBKDF2_SHA256_SALT_LEN);
803 assert_eq!(first.iterations(), crate::DEFAULT_PBKDF2_SHA256_ITERATIONS);
804
805 assert!(first.verify_plain_password(b"password").unwrap());
807 assert!(!first.verify_plain_password(b"wrong").unwrap());
808 }
809
810 #[test]
811 fn test_postgres_auth_info_uses_scram_for_unknown_user() {
812 let verifier =
813 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
814 let (_, user) = parse_credential_line(&format!("user={verifier}")).unwrap();
815 let users = HashMap::from([("user".to_string(), user)]);
816
817 let auth_info =
818 postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
819 assert!(matches!(
820 auth_info,
821 PgAuthInfo::ScramSha256 {
822 user_info: None,
823 ..
824 }
825 ));
826 }
827
828 #[test]
829 fn test_postgres_auth_info_falls_back_to_cleartext() {
830 let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
831 let users = HashMap::from([(
832 "user".to_string(),
833 (
834 PasswordVerifier::MysqlNativePassword { hash_stage_2 },
835 PermissionMode::default(),
836 ),
837 )]);
838
839 let auth_info =
840 postgres_auth_info_with_credential(&users, Identity::UserId("user", None)).unwrap();
841 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
842
843 let auth_info =
844 postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
845 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
846 }
847
848 #[test]
849 fn test_postgres_auth_info_with_pbkdf2_falls_back_to_cleartext() {
850 let iterations = 4096;
851 let salt = b"salt";
852 let password = "pass\u{00a0}word";
853 let mut hash = [0u8; PBKDF2_SHA256_HASH_LEN];
854 pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
855 let users = HashMap::from([(
856 "user".to_string(),
857 (
858 PasswordVerifier::Pbkdf2Sha256 {
859 iterations,
860 salt: salt.to_vec(),
861 hash: hash.to_vec(),
862 },
863 PermissionMode::default(),
864 ),
865 )]);
866
867 let auth_info =
868 postgres_auth_info_with_credential(&users, Identity::UserId("user", None)).unwrap();
869 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
870
871 assert!(
872 authenticate_with_credential(
873 &users,
874 Identity::UserId("user", None),
875 Password::PlainText(password.to_string().into()),
876 )
877 .is_ok()
878 );
879 }
880
881 #[test]
882 fn test_postgres_auth_info_mixed_verifiers_fall_back_to_cleartext() {
883 let verifier =
884 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
885 let (_, scram_user) = parse_credential_line(&format!("scram={verifier}")).unwrap();
886 let mysql_user = (
887 PasswordVerifier::MysqlNativePassword {
888 hash_stage_2: mysql_native_password_hash("password".as_bytes()),
889 },
890 PermissionMode::default(),
891 );
892 let users = HashMap::from([
893 ("scram".to_string(), scram_user),
894 ("mysql".to_string(), mysql_user),
895 ]);
896
897 let auth_info =
898 postgres_auth_info_with_credential(&users, Identity::UserId("scram", None)).unwrap();
899 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
900
901 let auth_info =
902 postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
903 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
904 }
905
906 #[test]
907 fn test_password_verifier_debug_redacts_secrets() {
908 let debug = format!(
909 "{:?}",
910 PasswordVerifier::plain_text("secret".to_string()).unwrap()
911 );
912 assert!(debug.contains("<REDACTED>"));
913 assert!(!debug.contains("secret"));
914
915 let debug = format!(
916 "{:?}",
917 PasswordVerifier::Pbkdf2Sha256 {
918 iterations: 4096,
919 salt: b"super-secret-salt".to_vec(),
920 hash: b"super-secret-hash".to_vec(),
921 }
922 );
923 assert!(debug.contains("Pbkdf2Sha256"));
924 assert!(debug.contains("4096"));
925 assert!(!debug.contains("super-secret-salt"));
926 assert!(!debug.contains("super-secret-hash"));
927
928 let debug = format!(
929 "{:?}",
930 PasswordVerifier::MysqlNativePassword {
931 hash_stage_2: b"super-secret-hash".to_vec(),
932 }
933 );
934 assert!(debug.contains("MysqlNativePassword"));
935 assert!(!debug.contains("super-secret-hash"));
936 }
937}