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 common_telemetry::warn;
26use pbkdf2::pbkdf2_hmac;
27use sha2::Sha256;
28use snafu::{OptionExt, ResultExt, ensure};
29use subtle::ConstantTimeEq;
30
31use crate::common::{
32 DEFAULT_PBKDF2_SHA256_SALT_LEN, Identity, MAX_PBKDF2_SHA256_ITERATIONS,
33 MAX_PBKDF2_SHA256_SALT_LEN, PBKDF2_SHA256_HASH_LEN, Password, PgScramSha256Verifier,
34 auth_mysql_with_hash_stage_2, parse_mysql_native_password_verifier,
35 parse_pg_scram_sha256_password_verifier,
36};
37use crate::error::{
38 IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedAuthMethodSnafu,
39 UnsupportedPasswordTypeSnafu, UserNotFoundSnafu, UserPasswordMismatchSnafu,
40};
41use crate::user_info::{DefaultUserInfo, PermissionMode};
42use crate::{UserInfoRef, auth_mysql};
43
44pub const BEARER_TOKEN_USER: &str = "*";
51
52#[async_trait::async_trait]
53pub trait UserProvider: Send + Sync {
54 fn name(&self) -> &str;
55
56 async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
58
59 async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
63
64 async fn auth(
67 &self,
68 id: Identity<'_>,
69 password: Password<'_>,
70 catalog: &str,
71 schema: &str,
72 ) -> Result<UserInfoRef> {
73 let user_info = self.authenticate(id, password).await?;
74 self.authorize(catalog, schema, &user_info).await?;
75 Ok(user_info)
76 }
77
78 async fn authenticate_bearer_token(&self, _token: &str, _catalog: &str) -> Result<UserInfoRef> {
91 UnsupportedAuthMethodSnafu {
92 method: "bearer token",
93 }
94 .fail()
95 }
96
97 async fn auth_bearer_token(
100 &self,
101 token: &str,
102 catalog: &str,
103 schema: &str,
104 ) -> Result<UserInfoRef> {
105 let user_info = self.authenticate_bearer_token(token, catalog).await?;
106 self.authorize(catalog, schema, &user_info).await?;
107 Ok(user_info)
108 }
109
110 fn mysql_auth_method(&self) -> MysqlAuthMethod {
111 if self.external() {
112 MysqlAuthMethod::ClearPassword
113 } else {
114 MysqlAuthMethod::NativePassword
115 }
116 }
117
118 async fn postgres_auth_info(&self, _id: Identity<'_>, _catalog: &str) -> Result<PgAuthInfo> {
119 Ok(PgAuthInfo::Cleartext)
120 }
121
122 fn external(&self) -> bool {
124 false
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum MysqlAuthMethod {
130 NativePassword,
131 ClearPassword,
132}
133
134impl MysqlAuthMethod {
135 pub const NATIVE_PASSWORD_PLUGIN: &'static str = "mysql_native_password";
136 pub const CLEAR_PASSWORD_PLUGIN: &'static str = "mysql_clear_password";
137
138 pub const fn plugin_name(self) -> &'static str {
140 match self {
141 Self::NativePassword => Self::NATIVE_PASSWORD_PLUGIN,
142 Self::ClearPassword => Self::CLEAR_PASSWORD_PLUGIN,
143 }
144 }
145}
146
147pub enum PgAuthInfo {
148 ScramSha256 {
149 verifier: PgScramSha256Verifier,
150 user_info: Option<UserInfoRef>,
151 },
152 Cleartext,
153}
154
155#[derive(Clone)]
156pub(crate) enum PasswordVerifier {
157 PlainText {
158 password: String,
159 scram: PgScramSha256Verifier,
165 },
166 Pbkdf2Sha256 {
167 iterations: u32,
168 salt: Vec<u8>,
169 hash: Vec<u8>,
170 },
171 MysqlNativePassword {
172 hash_stage_2: Vec<u8>,
173 },
174 PgScramSha256(PgScramSha256Verifier),
175}
176
177impl PartialEq for PasswordVerifier {
178 fn eq(&self, other: &Self) -> bool {
179 match (self, other) {
182 (
183 PasswordVerifier::PlainText { password: a, .. },
184 PasswordVerifier::PlainText { password: b, .. },
185 ) => a == b,
186 (
187 PasswordVerifier::Pbkdf2Sha256 {
188 iterations: i1,
189 salt: s1,
190 hash: h1,
191 },
192 PasswordVerifier::Pbkdf2Sha256 {
193 iterations: i2,
194 salt: s2,
195 hash: h2,
196 },
197 ) => i1 == i2 && s1 == s2 && h1 == h2,
198 (
199 PasswordVerifier::MysqlNativePassword { hash_stage_2: a },
200 PasswordVerifier::MysqlNativePassword { hash_stage_2: b },
201 ) => a == b,
202 (PasswordVerifier::PgScramSha256(a), PasswordVerifier::PgScramSha256(b)) => a == b,
203 _ => false,
204 }
205 }
206}
207
208impl Eq for PasswordVerifier {}
209
210impl fmt::Debug for PasswordVerifier {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 match self {
213 PasswordVerifier::PlainText { .. } => {
214 f.debug_tuple("PlainText").field(&"<REDACTED>").finish()
215 }
216 PasswordVerifier::Pbkdf2Sha256 { iterations, .. } => f
217 .debug_struct("Pbkdf2Sha256")
218 .field("iterations", iterations)
219 .field("salt", &"<REDACTED>")
220 .field("hash", &"<REDACTED>")
221 .finish(),
222 PasswordVerifier::MysqlNativePassword { .. } => f
223 .debug_struct("MysqlNativePassword")
224 .field("hash_stage_2", &"<REDACTED>")
225 .finish(),
226 PasswordVerifier::PgScramSha256(_) => f
227 .debug_struct("PgScramSha256")
228 .field("verifier", &"<REDACTED>")
229 .finish(),
230 }
231 }
232}
233
234impl PasswordVerifier {
235 fn plain_text(password: String) -> Option<Self> {
240 let salt = rand::random::<[u8; DEFAULT_PBKDF2_SHA256_SALT_LEN]>();
241 let scram = PgScramSha256Verifier::from_password(
242 password.as_bytes(),
243 &salt,
244 crate::DEFAULT_PBKDF2_SHA256_ITERATIONS,
245 )
246 .ok()?;
247 Some(Self::PlainText { password, scram })
248 }
249
250 fn parse(input: &str) -> Option<Self> {
251 if let Some(password) = input.strip_prefix("plain:") {
252 return Self::plain_text(password.to_string());
253 }
254
255 if let Some(verifier) = input.strip_prefix("pbkdf2_sha256:") {
256 let mut parts = verifier.split(':');
257 let iterations = parts.next()?.parse::<u32>().ok()?;
258 let salt = hex::decode(parts.next()?).ok()?;
259 let hash = hex::decode(parts.next()?).ok()?;
260 if parts.next().is_some()
261 || iterations == 0
262 || iterations > MAX_PBKDF2_SHA256_ITERATIONS
263 || salt.is_empty()
264 || salt.len() > MAX_PBKDF2_SHA256_SALT_LEN
265 || hash.len() != PBKDF2_SHA256_HASH_LEN
266 {
267 return None;
268 }
269
270 return Some(Self::Pbkdf2Sha256 {
271 iterations,
272 salt,
273 hash,
274 });
275 }
276
277 if input.starts_with("mysql_native_password:") {
278 let hash_stage_2 = parse_mysql_native_password_verifier(input).ok()?;
279 return Some(Self::MysqlNativePassword { hash_stage_2 });
280 }
281
282 if input.starts_with("pg_scram_sha256:") {
283 return parse_pg_scram_sha256_password_verifier(input)
284 .ok()
285 .map(Self::PgScramSha256);
286 }
287
288 Self::plain_text(input.to_string())
289 }
290
291 fn supports_pg_scram_sha256(&self) -> bool {
292 matches!(
293 self,
294 PasswordVerifier::PlainText { .. } | PasswordVerifier::PgScramSha256(_)
295 )
296 }
297
298 fn to_pg_scram_sha256_verifier(&self) -> Option<PgScramSha256Verifier> {
299 match self {
300 PasswordVerifier::PlainText { scram, .. } => Some(scram.clone()),
301 PasswordVerifier::Pbkdf2Sha256 { .. } => None,
304 PasswordVerifier::PgScramSha256(verifier) => Some(verifier.clone()),
305 PasswordVerifier::MysqlNativePassword { .. } => None,
306 }
307 }
308
309 fn verify_plain_text(&self, password: &str) -> bool {
310 match self {
311 PasswordVerifier::PlainText {
312 password: expected, ..
313 } => expected.as_bytes().ct_eq(password.as_bytes()).into(),
314 PasswordVerifier::Pbkdf2Sha256 {
315 iterations,
316 salt,
317 hash,
318 } => {
319 if hash.len() != PBKDF2_SHA256_HASH_LEN {
320 return false;
321 }
322 let mut actual = [0u8; PBKDF2_SHA256_HASH_LEN];
323 pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, *iterations, &mut actual);
324 hash.as_slice().ct_eq(&actual[..]).into()
325 }
326 PasswordVerifier::MysqlNativePassword { .. } => false,
327 PasswordVerifier::PgScramSha256(verifier) => verifier
328 .verify_plain_password(password.as_bytes())
329 .unwrap_or(false),
330 }
331 }
332
333 fn verify_mysql_native_password(
334 &self,
335 auth_data: &[u8],
336 salt: &[u8],
337 username: &str,
338 ) -> Result<()> {
339 match self {
340 PasswordVerifier::PlainText { password, .. } => {
341 auth_mysql(auth_data, salt, username, password.as_bytes())
342 }
343 PasswordVerifier::MysqlNativePassword { hash_stage_2 } => {
344 auth_mysql_with_hash_stage_2(auth_data, salt, username, hash_stage_2)
345 }
346 PasswordVerifier::Pbkdf2Sha256 { .. } => UnsupportedPasswordTypeSnafu {
347 password_type: "mysql_native_password_with_pbkdf2_sha256_verifier",
348 }
349 .fail(),
350 PasswordVerifier::PgScramSha256(_) => UnsupportedPasswordTypeSnafu {
351 password_type: "mysql_native_password_with_pg_scram_sha256_verifier",
352 }
353 .fail(),
354 }
355 }
356}
357
358pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
361
362fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
363 let path = Path::new(filepath);
365 if !path.exists() {
366 return InvalidConfigSnafu {
367 value: filepath.to_string(),
368 msg: "UserProvider file must exist",
369 }
370 .fail();
371 }
372
373 ensure!(
374 path.is_file(),
375 InvalidConfigSnafu {
376 value: filepath,
377 msg: "UserProvider file must be a file",
378 }
379 );
380 let file = File::open(path).context(IoSnafu)?;
381 let credential = io::BufReader::new(file)
382 .lines()
383 .enumerate()
384 .map_while(|(idx, line)| match line {
385 Ok(line) => Some((idx, line)),
386 Err(err) => {
387 warn!(
391 "Failed to read line {} of user provider file {}: {}; \
392 all remaining credentials are ignored",
393 idx + 1,
394 filepath,
395 err
396 );
397 None
398 }
399 })
400 .filter_map(|(idx, line)| {
401 let line = line.trim();
407 if line.is_empty() || line.starts_with('#') {
408 return None;
409 }
410
411 let parsed = parse_credential_line(line);
412 if parsed.is_none() {
413 warn!(
417 "Ignoring malformed credential at line {} of user provider file {}: \
418 expected `username[:permission]=verifier` with exactly one `=` \
419 (passwords containing `=` are not supported)",
420 idx + 1,
421 filepath
422 );
423 }
424 parsed
425 })
426 .collect::<HashMap<String, _>>();
427
428 ensure!(
429 !credential.is_empty(),
430 InvalidConfigSnafu {
431 value: filepath,
432 msg: "UserProvider's file must contains at least one valid credential",
433 }
434 );
435
436 warn_if_pg_scram_disabled(&credential);
437
438 Ok(credential)
439}
440
441fn pg_scram_unsupported_users(users: &UserInfoMap) -> Vec<&str> {
450 users
451 .iter()
452 .filter(|(_, (verifier, _))| !verifier.supports_pg_scram_sha256())
453 .map(|(username, _)| username.as_str())
454 .collect()
455}
456
457pub(crate) fn warn_if_pg_scram_disabled(users: &UserInfoMap) {
461 let unsupported = pg_scram_unsupported_users(users);
462 if !unsupported.is_empty() {
463 warn!(
464 "Postgres SCRAM authentication is disabled: {} of {} user(s) use a \
465 non-SCRAM password verifier {:?}, so all Postgres password \
466 authentication falls back to cleartext. Ensure TLS is enabled; if you \
467 rely on Postgres SCRAM, generate every user's verifier with the \
468 pg_scram_sha256 format.",
469 unsupported.len(),
470 users.len(),
471 unsupported
472 );
473 }
474}
475
476pub(crate) fn parse_credential_line(
484 line: &str,
485) -> Option<(String, (PasswordVerifier, PermissionMode))> {
486 let parts = line.split('=').collect::<Vec<&str>>();
487 if parts.len() != 2 {
488 return None;
489 }
490
491 let (username_part, password) = (parts[0], parts[1]);
492 let (username, permission_mode) = if let Some((user, perm)) = username_part.split_once(':') {
493 (user, PermissionMode::from_str(perm)?)
494 } else {
495 (username_part, PermissionMode::default())
496 };
497
498 let verifier = PasswordVerifier::parse(password)?;
499
500 Some((username.to_string(), (verifier, permission_mode)))
501}
502
503pub(crate) fn postgres_auth_info_with_credential(
504 users: &UserInfoMap,
505 input_id: Identity<'_>,
506) -> Result<PgAuthInfo> {
507 match input_id {
508 Identity::UserId(username, _) => {
509 ensure!(
510 !username.is_empty(),
511 IllegalParamSnafu {
512 msg: "blank username"
513 }
514 );
515
516 if !users
517 .values()
518 .all(|(verifier, _)| verifier.supports_pg_scram_sha256())
519 {
520 return Ok(PgAuthInfo::Cleartext);
523 }
524
525 if let Some((verifier, permission_mode)) = users.get(username) {
526 if let Some(verifier) = verifier.to_pg_scram_sha256_verifier() {
527 return Ok(PgAuthInfo::ScramSha256 {
528 verifier,
529 user_info: Some(DefaultUserInfo::with_name_and_permission(
530 username,
531 *permission_mode,
532 )),
533 });
534 }
535
536 return Ok(PgAuthInfo::Cleartext);
537 }
538
539 Ok(PgAuthInfo::ScramSha256 {
543 verifier: PgScramSha256Verifier::mock_for_unknown_user(username.as_bytes()),
544 user_info: None,
545 })
546 }
547 }
548}
549
550fn authenticate_with_credential(
551 users: &UserInfoMap,
552 input_id: Identity<'_>,
553 input_pwd: Password<'_>,
554) -> Result<UserInfoRef> {
555 match input_id {
556 Identity::UserId(username, _) => {
557 ensure!(
558 !username.is_empty(),
559 IllegalParamSnafu {
560 msg: "blank username"
561 }
562 );
563 let (verifier, permission_mode) = users.get(username).context(UserNotFoundSnafu {
564 username: username.to_string(),
565 })?;
566
567 match input_pwd {
568 Password::PlainText(pwd) => {
569 ensure!(
570 !pwd.expose_secret().is_empty(),
571 IllegalParamSnafu {
572 msg: "blank password"
573 }
574 );
575 if verifier.verify_plain_text(pwd.expose_secret()) {
576 Ok(DefaultUserInfo::with_name_and_permission(
577 username,
578 *permission_mode,
579 ))
580 } else {
581 UserPasswordMismatchSnafu {
582 username: username.to_string(),
583 }
584 .fail()
585 }
586 }
587 Password::MysqlNativePassword(auth_data, salt) => verifier
588 .verify_mysql_native_password(auth_data, salt, username)
589 .map(|_| DefaultUserInfo::with_name_and_permission(username, *permission_mode)),
590 Password::PgMD5(_, _) => UnsupportedPasswordTypeSnafu {
591 password_type: "pg_md5",
592 }
593 .fail(),
594 }
595 }
596 }
597}
598#[cfg(test)]
599mod tests {
600 use digest::Digest;
601 use sha1::Sha1;
602
603 use super::*;
604 use crate::common::{format_pg_scram_sha256_password_verifier, mysql_native_password_hash};
605
606 fn plain(password: &str) -> PasswordVerifier {
607 PasswordVerifier::plain_text(password.to_string()).unwrap()
608 }
609
610 fn sha1_one(data: &[u8]) -> Vec<u8> {
611 let mut hasher = Sha1::new();
612 hasher.update(data);
613 hasher.finalize().to_vec()
614 }
615
616 fn mysql_native_password_auth_data(password: &str, salt: &[u8]) -> Vec<u8> {
617 let hash_stage_1 = sha1_one(password.as_bytes());
618 let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
619 let mut hasher = Sha1::new();
620 hasher.update(salt);
621 hasher.update(hash_stage_2);
622 let scramble = hasher.finalize();
623
624 hash_stage_1
625 .iter()
626 .zip(scramble.iter())
627 .map(|(lhs, rhs)| lhs ^ rhs)
628 .collect()
629 }
630
631 #[test]
632 fn test_parse_credential_line() {
633 let result = parse_credential_line("admin=password123");
635 assert_eq!(
636 result,
637 Some((
638 "admin".to_string(),
639 (plain("password123"), PermissionMode::default())
640 ))
641 );
642
643 let result = parse_credential_line("user:ReadOnly=secret");
645 assert_eq!(
646 result,
647 Some((
648 "user".to_string(),
649 (plain("secret"), PermissionMode::ReadOnly)
650 ))
651 );
652 let result = parse_credential_line("user:ro=secret");
653 assert_eq!(
654 result,
655 Some((
656 "user".to_string(),
657 (plain("secret"), PermissionMode::ReadOnly)
658 ))
659 );
660 let result = parse_credential_line("writer:WriteOnly=mypass");
662 assert_eq!(
663 result,
664 Some((
665 "writer".to_string(),
666 (plain("mypass"), PermissionMode::WriteOnly)
667 ))
668 );
669
670 let result = parse_credential_line("writer:wo=mypass");
672 assert_eq!(
673 result,
674 Some((
675 "writer".to_string(),
676 (plain("mypass"), PermissionMode::WriteOnly)
677 ))
678 );
679
680 let result = parse_credential_line("admin:rw=p@ssw0rd!123");
682 assert_eq!(
683 result,
684 Some((
685 "admin".to_string(),
686 (plain("p@ssw0rd!123"), PermissionMode::ReadWrite)
687 ))
688 );
689
690 let result = parse_credential_line("user name:WriteOnly=password");
692 assert_eq!(
693 result,
694 Some((
695 "user name".to_string(),
696 (plain("password"), PermissionMode::WriteOnly)
697 ))
698 );
699
700 let result = parse_credential_line("user=plain:password");
701 assert_eq!(
702 result,
703 Some((
704 "user".to_string(),
705 (plain("password"), PermissionMode::default())
706 ))
707 );
708
709 let iterations = 4096;
710 let salt = b"salt";
711 let mut hash = [0u8; 32];
712 pbkdf2_hmac::<Sha256>("password".as_bytes(), salt, iterations, &mut hash);
713 let result = parse_credential_line(&format!(
714 "user=pbkdf2_sha256:{iterations}:{}:{}",
715 hex::encode(salt),
716 hex::encode(hash)
717 ));
718 assert_eq!(
719 result,
720 Some((
721 "user".to_string(),
722 (
723 PasswordVerifier::Pbkdf2Sha256 {
724 iterations,
725 salt: salt.to_vec(),
726 hash: hash.to_vec(),
727 },
728 PermissionMode::default()
729 )
730 ))
731 );
732
733 let result = parse_credential_line("user=pbkdf2_sha256:4096:not-hex:abcd");
734 assert_eq!(result, None);
735
736 let result = parse_credential_line(&format!(
739 "user=pbkdf2_sha256:4096:{}:abcd",
740 hex::encode(salt)
741 ));
742 assert_eq!(result, None);
743
744 let result = parse_credential_line(&format!(
745 "user=pbkdf2_sha256:{}:{}:{}",
746 MAX_PBKDF2_SHA256_ITERATIONS + 1,
747 hex::encode(salt),
748 hex::encode(hash)
749 ));
750 assert_eq!(result, None);
751
752 let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
753 let result = parse_credential_line(&format!(
754 "user=mysql_native_password:{}",
755 hex::encode(&hash_stage_2)
756 ));
757 assert_eq!(
758 result,
759 Some((
760 "user".to_string(),
761 (
762 PasswordVerifier::MysqlNativePassword { hash_stage_2 },
763 PermissionMode::default()
764 )
765 ))
766 );
767
768 let result = parse_credential_line("user=mysql_native_password:abcd");
769 assert_eq!(result, None);
770
771 let verifier =
772 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
773 let result = parse_credential_line(&format!("user={verifier}"));
774 assert!(matches!(
775 result,
776 Some((
777 _,
778 (
779 PasswordVerifier::PgScramSha256(PgScramSha256Verifier { .. }),
780 PermissionMode::ReadWrite
781 )
782 ))
783 ));
784
785 let result = parse_credential_line("user=pg_scram_sha256:4096:73616c74:abcd:abcd");
786 assert_eq!(result, None);
787
788 let result = parse_credential_line("invalid_line");
790 assert_eq!(result, None);
791
792 let result = parse_credential_line("user=pass=word");
794 assert_eq!(result, None);
795
796 for line in [
797 "user:readonyl=password",
798 "user:=password",
799 "user:arbitrary=password",
800 ] {
801 assert_eq!(parse_credential_line(line), None);
802 }
803
804 let result = parse_credential_line("user=");
806 assert_eq!(
807 result,
808 Some(("user".to_string(), (plain(""), PermissionMode::default())))
809 );
810
811 let result = parse_credential_line("=password");
813 assert_eq!(
814 result,
815 Some((
816 "".to_string(),
817 (plain("password"), PermissionMode::default())
818 ))
819 );
820 }
821
822 #[test]
823 fn test_authenticate_with_mysql_native_password_verifier() {
824 let password = "password";
825 let salt = b"12345678901234567890";
826 let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
827 let auth_data = mysql_native_password_auth_data(password, salt);
828 let users = HashMap::from([(
829 "user".to_string(),
830 (
831 PasswordVerifier::MysqlNativePassword { hash_stage_2 },
832 PermissionMode::default(),
833 ),
834 )]);
835
836 let result = authenticate_with_credential(
837 &users,
838 Identity::UserId("user", None),
839 Password::MysqlNativePassword(&auth_data, salt),
840 );
841
842 assert!(result.is_ok());
843 }
844
845 #[test]
846 fn test_authenticate_with_plain_text_mysql_native_password() {
847 let password = "password";
848 let salt = b"12345678901234567890";
849 let auth_data = mysql_native_password_auth_data(password, salt);
850 let users = HashMap::from([(
851 "user".to_string(),
852 (
853 PasswordVerifier::plain_text(password.to_string()).unwrap(),
854 PermissionMode::default(),
855 ),
856 )]);
857
858 let result = authenticate_with_credential(
859 &users,
860 Identity::UserId("user", None),
861 Password::MysqlNativePassword(&auth_data, salt),
862 );
863
864 assert!(result.is_ok());
865 }
866
867 #[test]
868 fn test_pbkdf2_sha256_rejects_mysql_native_password() {
869 let password = "password";
870 let salt = b"salt";
871 let iterations = 4096;
872 let mut hash = [0u8; 32];
873 pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
874 let users = HashMap::from([(
875 "user".to_string(),
876 (
877 PasswordVerifier::Pbkdf2Sha256 {
878 iterations,
879 salt: salt.to_vec(),
880 hash: hash.to_vec(),
881 },
882 PermissionMode::default(),
883 ),
884 )]);
885 let mysql_salt = b"12345678901234567890";
886 let auth_data = mysql_native_password_auth_data(password, mysql_salt);
887
888 let result = authenticate_with_credential(
889 &users,
890 Identity::UserId("user", None),
891 Password::MysqlNativePassword(&auth_data, mysql_salt),
892 );
893
894 assert!(result.is_err());
895 }
896
897 #[test]
898 fn test_authenticate_with_pg_scram_sha256_verifier_plain_text() {
899 let verifier =
900 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
901 let (_, user) = parse_credential_line(&format!("user={verifier}")).unwrap();
902 let users = HashMap::from([("user".to_string(), user)]);
903
904 let result = authenticate_with_credential(
905 &users,
906 Identity::UserId("user", None),
907 Password::PlainText("password".to_string().into()),
908 );
909 assert!(result.is_ok());
910
911 let result = authenticate_with_credential(
912 &users,
913 Identity::UserId("user", None),
914 Password::PlainText("wrong".to_string().into()),
915 );
916 assert!(result.is_err());
917 }
918
919 #[test]
920 fn test_plain_text_scram_verifier_is_stable() {
921 let verifier = PasswordVerifier::plain_text("password".to_string()).unwrap();
922 let first = verifier.to_pg_scram_sha256_verifier().unwrap();
923 let second = verifier.to_pg_scram_sha256_verifier().unwrap();
924
925 assert_eq!(first.salt(), second.salt());
929 assert_eq!(first.salt().len(), DEFAULT_PBKDF2_SHA256_SALT_LEN);
930 assert_eq!(first.iterations(), crate::DEFAULT_PBKDF2_SHA256_ITERATIONS);
931
932 assert!(first.verify_plain_password(b"password").unwrap());
934 assert!(!first.verify_plain_password(b"wrong").unwrap());
935 }
936
937 #[test]
938 fn test_postgres_auth_info_uses_scram_for_unknown_user() {
939 let verifier =
940 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
941 let (_, user) = parse_credential_line(&format!("user={verifier}")).unwrap();
942 let users = HashMap::from([("user".to_string(), user)]);
943
944 let auth_info =
945 postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
946 assert!(matches!(
947 auth_info,
948 PgAuthInfo::ScramSha256 {
949 user_info: None,
950 ..
951 }
952 ));
953 }
954
955 #[test]
956 fn test_postgres_auth_info_falls_back_to_cleartext() {
957 let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
958 let users = HashMap::from([(
959 "user".to_string(),
960 (
961 PasswordVerifier::MysqlNativePassword { hash_stage_2 },
962 PermissionMode::default(),
963 ),
964 )]);
965
966 let auth_info =
967 postgres_auth_info_with_credential(&users, Identity::UserId("user", None)).unwrap();
968 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
969
970 let auth_info =
971 postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
972 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
973 }
974
975 #[test]
976 fn test_postgres_auth_info_with_pbkdf2_falls_back_to_cleartext() {
977 let iterations = 4096;
978 let salt = b"salt";
979 let password = "pass\u{00a0}word";
980 let mut hash = [0u8; PBKDF2_SHA256_HASH_LEN];
981 pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
982 let users = HashMap::from([(
983 "user".to_string(),
984 (
985 PasswordVerifier::Pbkdf2Sha256 {
986 iterations,
987 salt: salt.to_vec(),
988 hash: hash.to_vec(),
989 },
990 PermissionMode::default(),
991 ),
992 )]);
993
994 let auth_info =
995 postgres_auth_info_with_credential(&users, Identity::UserId("user", None)).unwrap();
996 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
997
998 assert!(
999 authenticate_with_credential(
1000 &users,
1001 Identity::UserId("user", None),
1002 Password::PlainText(password.to_string().into()),
1003 )
1004 .is_ok()
1005 );
1006 }
1007
1008 #[test]
1009 fn test_postgres_auth_info_mixed_verifiers_fall_back_to_cleartext() {
1010 let verifier =
1011 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
1012 let (_, scram_user) = parse_credential_line(&format!("scram={verifier}")).unwrap();
1013 let mysql_user = (
1014 PasswordVerifier::MysqlNativePassword {
1015 hash_stage_2: mysql_native_password_hash("password".as_bytes()),
1016 },
1017 PermissionMode::default(),
1018 );
1019 let users = HashMap::from([
1020 ("scram".to_string(), scram_user),
1021 ("mysql".to_string(), mysql_user),
1022 ]);
1023
1024 let auth_info =
1025 postgres_auth_info_with_credential(&users, Identity::UserId("scram", None)).unwrap();
1026 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
1027
1028 let auth_info =
1029 postgres_auth_info_with_credential(&users, Identity::UserId("unknown", None)).unwrap();
1030 assert!(matches!(auth_info, PgAuthInfo::Cleartext));
1031 }
1032
1033 #[test]
1034 fn test_pg_scram_unsupported_users() {
1035 let scram_verifier =
1036 format_pg_scram_sha256_password_verifier(b"password", b"salt", 4096).unwrap();
1037 let (_, scram_user) = parse_credential_line(&format!("scram={scram_verifier}")).unwrap();
1038
1039 let mut hash = [0u8; PBKDF2_SHA256_HASH_LEN];
1040 pbkdf2_hmac::<Sha256>(b"password", b"salt", 4096, &mut hash);
1041
1042 let users = HashMap::from([
1043 (
1044 "plain".to_string(),
1045 (plain("password"), PermissionMode::default()),
1046 ),
1047 ("scram".to_string(), scram_user),
1048 (
1049 "pbkdf2".to_string(),
1050 (
1051 PasswordVerifier::Pbkdf2Sha256 {
1052 iterations: 4096,
1053 salt: b"salt".to_vec(),
1054 hash: hash.to_vec(),
1055 },
1056 PermissionMode::default(),
1057 ),
1058 ),
1059 (
1060 "mysql".to_string(),
1061 (
1062 PasswordVerifier::MysqlNativePassword {
1063 hash_stage_2: mysql_native_password_hash(b"password"),
1064 },
1065 PermissionMode::default(),
1066 ),
1067 ),
1068 ]);
1069
1070 let mut unsupported = pg_scram_unsupported_users(&users);
1071 unsupported.sort();
1072 assert_eq!(unsupported, vec!["mysql", "pbkdf2"]);
1076 }
1077
1078 #[test]
1079 fn test_password_verifier_debug_redacts_secrets() {
1080 let debug = format!(
1081 "{:?}",
1082 PasswordVerifier::plain_text("secret".to_string()).unwrap()
1083 );
1084 assert!(debug.contains("<REDACTED>"));
1085 assert!(!debug.contains("secret"));
1086
1087 let debug = format!(
1088 "{:?}",
1089 PasswordVerifier::Pbkdf2Sha256 {
1090 iterations: 4096,
1091 salt: b"super-secret-salt".to_vec(),
1092 hash: b"super-secret-hash".to_vec(),
1093 }
1094 );
1095 assert!(debug.contains("Pbkdf2Sha256"));
1096 assert!(debug.contains("4096"));
1097 assert!(!debug.contains("super-secret-salt"));
1098 assert!(!debug.contains("super-secret-hash"));
1099
1100 let debug = format!(
1101 "{:?}",
1102 PasswordVerifier::MysqlNativePassword {
1103 hash_stage_2: b"super-secret-hash".to_vec(),
1104 }
1105 );
1106 assert!(debug.contains("MysqlNativePassword"));
1107 assert!(!debug.contains("super-secret-hash"));
1108 }
1109}