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    /// Selects authentication after the MySQL handshake supplies a username.
119    /// The user is resolved in the same scope as [`authenticate`](Self::authenticate).
120    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    /// Returns whether this user provider implementation is backed by an external system.
129    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    /// Returns the MySQL authentication plugin name sent on the wire.
145    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 verifier derived once at load time. Precomputing it keeps the
166        /// Postgres SCRAM `server-first-message` (stable salt, fixed iterations)
167        /// and per-connection cost indistinguishable from stored-hash and
168        /// unknown users, instead of running PBKDF2 with a fresh salt on every
169        /// connection.
170        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        // The precomputed SCRAM verifier is a cache derived from the password, so
186        // two plaintext verifiers are equal iff their passwords match.
187        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    /// Builds a plaintext verifier, precomputing a stable SCRAM verifier so the
242    /// Postgres SCRAM handshake for this user carries a stable salt and runs no
243    /// per-connection PBKDF2. Uses [`DEFAULT_PBKDF2_SHA256_ITERATIONS`] to match
244    /// the mock verifier handed to unknown users.
245    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            // Legacy PBKDF2 verifiers were derived without SASLprep, and the
308            // original password is unavailable to normalize them safely.
309            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
364/// Type alias for user info map.
365/// Key is username, value is (password verifier, permission_mode).
366pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
367
368fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
369    // check valid path
370    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                // A read error (I/O failure or invalid UTF-8) ends the iterator,
394                // so every remaining credential is dropped. Warn instead of
395                // vanishing silently, matching the malformed-line handling below.
396                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            // The line format is:
408            // - `username=password` - Basic user with default permissions
409            // - `username:permission_mode=password` - User with specific permission mode
410            // - Lines starting with '#' are treated as comments and ignored
411            // - Empty lines are ignored
412            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                // Don't log the line: it carries the password/verifier. A common
420                // cause is a plaintext password containing `=`, which splits the
421                // line into more than two parts.
422                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
447/// Returns the users whose verifier cannot back a Postgres SCRAM handshake.
448///
449/// Only [`PasswordVerifier::PlainText`] and [`PasswordVerifier::PgScramSha256`]
450/// support SCRAM. A `mysql_native_password` verifier is a double-SHA1 digest
451/// unrelated to PBKDF2, and a `pbkdf2_sha256` verifier was derived without
452/// SASLprep and cannot be safely reused as a SCRAM secret without the original
453/// password. Either kind forces the whole Postgres endpoint to fall back to
454/// cleartext (see [`postgres_auth_info_with_credential`]).
455fn 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
463/// Warns once per credential load when the set disables Postgres SCRAM, so
464/// operators don't unknowingly serve cleartext passwords over Postgres while
465/// believing SCRAM is in effect.
466pub(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
482/// Parse a line of credential in the format of `username=password` or `username:permission_mode=password`.
483///
484/// The password part accepts legacy plain text and explicit verifier formats:
485/// - `plain:<password>`
486/// - `pbkdf2_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-hash>`
487/// - `mysql_native_password:<hex-encoded-sha1-sha1-password>`
488/// - `pg_scram_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-stored-key>:<hex-encoded-server-key>`
489pub(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                // PostgreSQL chooses one auth method during startup. Selecting it
527                // per username would expose user or verifier existence.
528                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            // Unknown user: hand back a deterministic mock verifier so the SCRAM
546            // handshake is indistinguishable from a real user, without running
547            // PBKDF2 or leaking existence through an unstable salt.
548            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        // Basic username=password format
640        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        // Username with permission mode
650        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        // Username with WriteOnly permission mode
667        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        // Username with 'wo' as WriteOnly permission shorthand
677        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        // Username with complex password containing special characters
687        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        // Username with spaces should be preserved
697        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        // A well-formed but truncated hash must be rejected: a short hash would let
743        // many wrong passwords pass by matching only a few derived bytes.
744        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        // Invalid format - no equals sign
795        let result = parse_credential_line("invalid_line");
796        assert_eq!(result, None);
797
798        // Invalid format - multiple equals signs
799        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        // Empty password
811        let result = parse_credential_line("user=");
812        assert_eq!(
813            result,
814            Some(("user".to_string(), (plain(""), PermissionMode::default())))
815        );
816
817        // Empty username
818        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        // A plaintext-backed user must present a stable salt and fixed iterations
932        // across connections without re-running PBKDF2, otherwise the SCRAM
933        // server-first message (and its timing) leaks that the user exists.
934        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        // The derived verifier must still accept the real password.
939        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        // A pbkdf2_sha256 verifier is flagged alongside mysql_native_password:
1079        // both force Postgres to fall back to cleartext, while plain and
1080        // pg_scram back SCRAM.
1081        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}