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 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
45/// Reserved SQL-protocol username selecting bearer-token authentication.
46///
47/// User providers must not define this as a password-authenticated user.
48/// SQL servers carry the token through their clear-password exchange; this
49/// selector does not itself require TLS, so transport policy remains a server
50/// deployment choice.
51pub const BEARER_TOKEN_USER: &str = "*";
52
53/// A login method advertised by the HTTP authentication configuration endpoint.
54#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
55#[serde(tag = "type", rename_all = "snake_case")]
56pub enum AuthMethod {
57    /// Username and password authentication.
58    Password,
59    /// OpenID Connect authentication for browser clients.
60    Oidc {
61        /// Stable identifier for this identity provider.
62        id: String,
63        /// Human-readable identity provider name.
64        display_name: String,
65        /// OpenID Connect issuer URL.
66        issuer: String,
67        /// Public client identifier.
68        client_id: String,
69        /// OAuth scopes requested during login.
70        scopes: Vec<String>,
71    },
72}
73
74#[async_trait::async_trait]
75pub trait UserProvider: Send + Sync {
76    fn name(&self) -> &str;
77
78    /// Checks whether a user is valid and allowed to access the database.
79    async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
80
81    /// Checks whether a connection request
82    /// from a certain user to a certain catalog/schema is legal.
83    /// This method should be called after [authenticate()](UserProvider::authenticate()).
84    async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
85
86    /// Combination of [authenticate()](UserProvider::authenticate()) and [authorize()](UserProvider::authorize()).
87    /// In most cases it's preferred for both convenience and performance.
88    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    /// Authenticates an opaque bearer token (e.g. a JWT or an OAuth2 access
101    /// token) and derives its user identity.
102    ///
103    /// Unlike [auth()](Self::auth), the caller has no `Identity`/`Password` —
104    /// the provider validates the token and *derives* the identity from it.
105    /// The token is opaque to the server, so JWT/JWKS/OIDC validation policy
106    /// stays pluggable and out of core.
107    ///
108    /// The default rejects token auth with
109    /// [`Error::UnsupportedAuthMethod`], so password-only providers keep
110    /// today's behavior. Providers that support token auth override this to
111    /// validate the token and resolve it to a user.
112    async fn authenticate_bearer_token(&self, _token: &str, _catalog: &str) -> Result<UserInfoRef> {
113        UnsupportedAuthMethodSnafu {
114            method: "bearer token",
115        }
116        .fail()
117    }
118
119    /// Combination of [`authenticate_bearer_token`](Self::authenticate_bearer_token)
120    /// and [`authorize`](Self::authorize).
121    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    /// Returns the login methods that browser clients can use.
133    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    /// Selects authentication after the MySQL handshake supplies a username.
146    /// The user is resolved in the same scope as [`authenticate`](Self::authenticate).
147    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    /// Returns whether this user provider implementation is backed by an external system.
156    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    /// Returns the MySQL authentication plugin name sent on the wire.
172    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 verifier derived once at load time. Precomputing it keeps the
193        /// Postgres SCRAM `server-first-message` (stable salt, fixed iterations)
194        /// and per-connection cost indistinguishable from stored-hash and
195        /// unknown users, instead of running PBKDF2 with a fresh salt on every
196        /// connection.
197        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        // The precomputed SCRAM verifier is a cache derived from the password, so
213        // two plaintext verifiers are equal iff their passwords match.
214        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    /// Builds a plaintext verifier, precomputing a stable SCRAM verifier so the
269    /// Postgres SCRAM handshake for this user carries a stable salt and runs no
270    /// per-connection PBKDF2. Uses [`DEFAULT_PBKDF2_SHA256_ITERATIONS`] to match
271    /// the mock verifier handed to unknown users.
272    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            // Legacy PBKDF2 verifiers were derived without SASLprep, and the
335            // original password is unavailable to normalize them safely.
336            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
391/// Type alias for user info map.
392/// Key is username, value is (password verifier, permission_mode).
393pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
394
395fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
396    // check valid path
397    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                // A read error (I/O failure or invalid UTF-8) ends the iterator,
421                // so every remaining credential is dropped. Warn instead of
422                // vanishing silently, matching the malformed-line handling below.
423                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            // The line format is:
435            // - `username=password` - Basic user with default permissions
436            // - `username:permission_mode=password` - User with specific permission mode
437            // - Lines starting with '#' are treated as comments and ignored
438            // - Empty lines are ignored
439            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                // Don't log the line: it carries the password/verifier. A common
447                // cause is a plaintext password containing `=`, which splits the
448                // line into more than two parts.
449                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
474/// Returns the users whose verifier cannot back a Postgres SCRAM handshake.
475///
476/// Only [`PasswordVerifier::PlainText`] and [`PasswordVerifier::PgScramSha256`]
477/// support SCRAM. A `mysql_native_password` verifier is a double-SHA1 digest
478/// unrelated to PBKDF2, and a `pbkdf2_sha256` verifier was derived without
479/// SASLprep and cannot be safely reused as a SCRAM secret without the original
480/// password. Either kind forces the whole Postgres endpoint to fall back to
481/// cleartext (see [`postgres_auth_info_with_credential`]).
482fn 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
490/// Warns once per credential load when the set disables Postgres SCRAM, so
491/// operators don't unknowingly serve cleartext passwords over Postgres while
492/// believing SCRAM is in effect.
493pub(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
509/// Parse a line of credential in the format of `username=password` or `username:permission_mode=password`.
510///
511/// The password part accepts legacy plain text and explicit verifier formats:
512/// - `plain:<password>`
513/// - `pbkdf2_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-hash>`
514/// - `mysql_native_password:<hex-encoded-sha1-sha1-password>`
515/// - `pg_scram_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-stored-key>:<hex-encoded-server-key>`
516pub(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                // PostgreSQL chooses one auth method during startup. Selecting it
554                // per username would expose user or verifier existence.
555                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            // Unknown user: hand back a deterministic mock verifier so the SCRAM
573            // handshake is indistinguishable from a real user, without running
574            // PBKDF2 or leaking existence through an unstable salt.
575            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        // Basic username=password format
667        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        // Username with permission mode
677        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        // Username with WriteOnly permission mode
694        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        // Username with 'wo' as WriteOnly permission shorthand
704        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        // Username with complex password containing special characters
714        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        // Username with spaces should be preserved
724        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        // A well-formed but truncated hash must be rejected: a short hash would let
770        // many wrong passwords pass by matching only a few derived bytes.
771        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        // Invalid format - no equals sign
822        let result = parse_credential_line("invalid_line");
823        assert_eq!(result, None);
824
825        // Invalid format - multiple equals signs
826        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        // Empty password
838        let result = parse_credential_line("user=");
839        assert_eq!(
840            result,
841            Some(("user".to_string(), (plain(""), PermissionMode::default())))
842        );
843
844        // Empty username
845        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        // A plaintext-backed user must present a stable salt and fixed iterations
959        // across connections without re-running PBKDF2, otherwise the SCRAM
960        // server-first message (and its timing) leaks that the user exists.
961        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        // The derived verifier must still accept the real password.
966        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        // A pbkdf2_sha256 verifier is flagged alongside mysql_native_password:
1106        // both force Postgres to fall back to cleartext, while plain and
1107        // pg_scram back SCRAM.
1108        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}