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