Skip to main content

auth/
user_provider.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15pub(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
44/// Reserved SQL-protocol username selecting bearer-token authentication.
45///
46/// User providers must not define this as a password-authenticated user.
47/// SQL servers carry the token through their clear-password exchange; this
48/// selector does not itself require TLS, so transport policy remains a server
49/// deployment choice.
50pub const BEARER_TOKEN_USER: &str = "*";
51
52#[async_trait::async_trait]
53pub trait UserProvider: Send + Sync {
54    fn name(&self) -> &str;
55
56    /// Checks whether a user is valid and allowed to access the database.
57    async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
58
59    /// Checks whether a connection request
60    /// from a certain user to a certain catalog/schema is legal.
61    /// This method should be called after [authenticate()](UserProvider::authenticate()).
62    async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
63
64    /// Combination of [authenticate()](UserProvider::authenticate()) and [authorize()](UserProvider::authorize()).
65    /// In most cases it's preferred for both convenience and performance.
66    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    /// Authenticates an opaque bearer token (e.g. a JWT or an OAuth2 access
79    /// token) and derives its user identity.
80    ///
81    /// Unlike [auth()](Self::auth), the caller has no `Identity`/`Password` —
82    /// the provider validates the token and *derives* the identity from it.
83    /// The token is opaque to the server, so JWT/JWKS/OIDC validation policy
84    /// stays pluggable and out of core.
85    ///
86    /// The default rejects token auth with
87    /// [`Error::UnsupportedAuthMethod`], so password-only providers keep
88    /// today's behavior. Providers that support token auth override this to
89    /// validate the token and resolve it to a user.
90    async fn authenticate_bearer_token(&self, _token: &str, _catalog: &str) -> Result<UserInfoRef> {
91        UnsupportedAuthMethodSnafu {
92            method: "bearer token",
93        }
94        .fail()
95    }
96
97    /// Combination of [`authenticate_bearer_token`](Self::authenticate_bearer_token)
98    /// and [`authorize`](Self::authorize).
99    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    /// Returns whether this user provider implementation is backed by an external system.
123    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    /// Returns the MySQL authentication plugin name sent on the wire.
139    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 verifier derived once at load time. Precomputing it keeps the
160        /// Postgres SCRAM `server-first-message` (stable salt, fixed iterations)
161        /// and per-connection cost indistinguishable from stored-hash and
162        /// unknown users, instead of running PBKDF2 with a fresh salt on every
163        /// connection.
164        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        // The precomputed SCRAM verifier is a cache derived from the password, so
180        // two plaintext verifiers are equal iff their passwords match.
181        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    /// Builds a plaintext verifier, precomputing a stable SCRAM verifier so the
236    /// Postgres SCRAM handshake for this user carries a stable salt and runs no
237    /// per-connection PBKDF2. Uses [`DEFAULT_PBKDF2_SHA256_ITERATIONS`] to match
238    /// the mock verifier handed to unknown users.
239    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            // Legacy PBKDF2 verifiers were derived without SASLprep, and the
302            // original password is unavailable to normalize them safely.
303            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
358/// Type alias for user info map.
359/// Key is username, value is (password verifier, permission_mode).
360pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
361
362fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
363    // check valid path
364    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                // A read error (I/O failure or invalid UTF-8) ends the iterator,
388                // so every remaining credential is dropped. Warn instead of
389                // vanishing silently, matching the malformed-line handling below.
390                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            // The line format is:
402            // - `username=password` - Basic user with default permissions
403            // - `username:permission_mode=password` - User with specific permission mode
404            // - Lines starting with '#' are treated as comments and ignored
405            // - Empty lines are ignored
406            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                // Don't log the line: it carries the password/verifier. A common
414                // cause is a plaintext password containing `=`, which splits the
415                // line into more than two parts.
416                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
441/// Returns the users whose verifier cannot back a Postgres SCRAM handshake.
442///
443/// Only [`PasswordVerifier::PlainText`] and [`PasswordVerifier::PgScramSha256`]
444/// support SCRAM. A `mysql_native_password` verifier is a double-SHA1 digest
445/// unrelated to PBKDF2, and a `pbkdf2_sha256` verifier was derived without
446/// SASLprep and cannot be safely reused as a SCRAM secret without the original
447/// password. Either kind forces the whole Postgres endpoint to fall back to
448/// cleartext (see [`postgres_auth_info_with_credential`]).
449fn 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
457/// Warns once per credential load when the set disables Postgres SCRAM, so
458/// operators don't unknowingly serve cleartext passwords over Postgres while
459/// believing SCRAM is in effect.
460pub(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
476/// Parse a line of credential in the format of `username=password` or `username:permission_mode=password`.
477///
478/// The password part accepts legacy plain text and explicit verifier formats:
479/// - `plain:<password>`
480/// - `pbkdf2_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-hash>`
481/// - `mysql_native_password:<hex-encoded-sha1-sha1-password>`
482/// - `pg_scram_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-stored-key>:<hex-encoded-server-key>`
483pub(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                // PostgreSQL chooses one auth method during startup. Selecting it
521                // per username would expose user or verifier existence.
522                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            // Unknown user: hand back a deterministic mock verifier so the SCRAM
540            // handshake is indistinguishable from a real user, without running
541            // PBKDF2 or leaking existence through an unstable salt.
542            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        // Basic username=password format
634        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        // Username with permission mode
644        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        // Username with WriteOnly permission mode
661        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        // Username with 'wo' as WriteOnly permission shorthand
671        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        // Username with complex password containing special characters
681        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        // Username with spaces should be preserved
691        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        // A well-formed but truncated hash must be rejected: a short hash would let
737        // many wrong passwords pass by matching only a few derived bytes.
738        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        // Invalid format - no equals sign
789        let result = parse_credential_line("invalid_line");
790        assert_eq!(result, None);
791
792        // Invalid format - multiple equals signs
793        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        // Empty password
805        let result = parse_credential_line("user=");
806        assert_eq!(
807            result,
808            Some(("user".to_string(), (plain(""), PermissionMode::default())))
809        );
810
811        // Empty username
812        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        // A plaintext-backed user must present a stable salt and fixed iterations
926        // across connections without re-running PBKDF2, otherwise the SCRAM
927        // server-first message (and its timing) leaks that the user exists.
928        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        // The derived verifier must still accept the real password.
933        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        // A pbkdf2_sha256 verifier is flagged alongside mysql_native_password:
1073        // both force Postgres to fall back to cleartext, while plain and
1074        // pg_scram back SCRAM.
1075        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}