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