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 pbkdf2::pbkdf2_hmac;
26use sha2::Sha256;
27use snafu::{OptionExt, ResultExt, ensure};
28use subtle::ConstantTimeEq;
29
30use crate::common::{
31    Identity, MAX_PBKDF2_SHA256_ITERATIONS, MAX_PBKDF2_SHA256_SALT_LEN, PBKDF2_SHA256_HASH_LEN,
32    Password, auth_mysql_with_hash_stage_2,
33};
34use crate::error::{
35    IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedPasswordTypeSnafu,
36    UserNotFoundSnafu, UserPasswordMismatchSnafu,
37};
38use crate::user_info::{DefaultUserInfo, PermissionMode};
39use crate::{UserInfoRef, auth_mysql};
40
41#[async_trait::async_trait]
42pub trait UserProvider: Send + Sync {
43    fn name(&self) -> &str;
44
45    /// Checks whether a user is valid and allowed to access the database.
46    async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
47
48    /// Checks whether a connection request
49    /// from a certain user to a certain catalog/schema is legal.
50    /// This method should be called after [authenticate()](UserProvider::authenticate()).
51    async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
52
53    /// Combination of [authenticate()](UserProvider::authenticate()) and [authorize()](UserProvider::authorize()).
54    /// In most cases it's preferred for both convenience and performance.
55    async fn auth(
56        &self,
57        id: Identity<'_>,
58        password: Password<'_>,
59        catalog: &str,
60        schema: &str,
61    ) -> Result<UserInfoRef> {
62        let user_info = self.authenticate(id, password).await?;
63        self.authorize(catalog, schema, &user_info).await?;
64        Ok(user_info)
65    }
66
67    /// Returns whether this user provider implementation is backed by an external system.
68    fn external(&self) -> bool {
69        false
70    }
71}
72
73#[derive(Clone, PartialEq, Eq)]
74pub(crate) enum PasswordVerifier {
75    PlainText(String),
76    Pbkdf2Sha256 {
77        iterations: u32,
78        salt: Vec<u8>,
79        hash: Vec<u8>,
80    },
81    MysqlNativePassword {
82        hash_stage_2: Vec<u8>,
83    },
84}
85
86impl fmt::Debug for PasswordVerifier {
87    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88        match self {
89            PasswordVerifier::PlainText(_) => {
90                f.debug_tuple("PlainText").field(&"<REDACTED>").finish()
91            }
92            PasswordVerifier::Pbkdf2Sha256 { iterations, .. } => f
93                .debug_struct("Pbkdf2Sha256")
94                .field("iterations", iterations)
95                .field("salt", &"<REDACTED>")
96                .field("hash", &"<REDACTED>")
97                .finish(),
98            PasswordVerifier::MysqlNativePassword { .. } => f
99                .debug_struct("MysqlNativePassword")
100                .field("hash_stage_2", &"<REDACTED>")
101                .finish(),
102        }
103    }
104}
105
106impl PasswordVerifier {
107    fn parse(input: &str) -> Option<Self> {
108        if let Some(password) = input.strip_prefix("plain:") {
109            return Some(Self::PlainText(password.to_string()));
110        }
111
112        if let Some(verifier) = input.strip_prefix("pbkdf2_sha256:") {
113            let mut parts = verifier.split(':');
114            let iterations = parts.next()?.parse::<u32>().ok()?;
115            let salt = hex::decode(parts.next()?).ok()?;
116            let hash = hex::decode(parts.next()?).ok()?;
117            if parts.next().is_some()
118                || iterations == 0
119                || iterations > MAX_PBKDF2_SHA256_ITERATIONS
120                || salt.is_empty()
121                || salt.len() > MAX_PBKDF2_SHA256_SALT_LEN
122                || hash.len() != PBKDF2_SHA256_HASH_LEN
123            {
124                return None;
125            }
126
127            return Some(Self::Pbkdf2Sha256 {
128                iterations,
129                salt,
130                hash,
131            });
132        }
133
134        if let Some(verifier) = input.strip_prefix("mysql_native_password:") {
135            let hash_stage_2 = hex::decode(verifier).ok()?;
136            if hash_stage_2.len() != 20 {
137                return None;
138            }
139
140            return Some(Self::MysqlNativePassword { hash_stage_2 });
141        }
142
143        Some(Self::PlainText(input.to_string()))
144    }
145
146    fn verify_plain_text(&self, password: &str) -> bool {
147        match self {
148            PasswordVerifier::PlainText(expected) => {
149                expected.as_bytes().ct_eq(password.as_bytes()).into()
150            }
151            PasswordVerifier::Pbkdf2Sha256 {
152                iterations,
153                salt,
154                hash,
155            } => {
156                if hash.len() != PBKDF2_SHA256_HASH_LEN {
157                    return false;
158                }
159                let mut actual = [0u8; PBKDF2_SHA256_HASH_LEN];
160                pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, *iterations, &mut actual);
161                hash.as_slice().ct_eq(&actual[..]).into()
162            }
163            PasswordVerifier::MysqlNativePassword { .. } => false,
164        }
165    }
166
167    fn verify_mysql_native_password(
168        &self,
169        auth_data: &[u8],
170        salt: &[u8],
171        username: &str,
172    ) -> Result<()> {
173        match self {
174            PasswordVerifier::PlainText(password) => {
175                auth_mysql(auth_data, salt, username, password.as_bytes())
176            }
177            PasswordVerifier::MysqlNativePassword { hash_stage_2 } => {
178                auth_mysql_with_hash_stage_2(auth_data, salt, username, hash_stage_2)
179            }
180            PasswordVerifier::Pbkdf2Sha256 { .. } => UnsupportedPasswordTypeSnafu {
181                password_type: "mysql_native_password_with_pbkdf2_sha256_verifier",
182            }
183            .fail(),
184        }
185    }
186}
187
188/// Type alias for user info map.
189/// Key is username, value is (password verifier, permission_mode).
190pub type UserInfoMap = HashMap<String, (PasswordVerifier, PermissionMode)>;
191
192fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
193    // check valid path
194    let path = Path::new(filepath);
195    if !path.exists() {
196        return InvalidConfigSnafu {
197            value: filepath.to_string(),
198            msg: "UserProvider file must exist",
199        }
200        .fail();
201    }
202
203    ensure!(
204        path.is_file(),
205        InvalidConfigSnafu {
206            value: filepath,
207            msg: "UserProvider file must be a file",
208        }
209    );
210    let file = File::open(path).context(IoSnafu)?;
211    let credential = io::BufReader::new(file)
212        .lines()
213        .map_while(std::result::Result::ok)
214        .filter_map(|line| {
215            // The line format is:
216            // - `username=password` - Basic user with default permissions
217            // - `username:permission_mode=password` - User with specific permission mode
218            // - Lines starting with '#' are treated as comments and ignored
219            // - Empty lines are ignored
220            let line = line.trim();
221            if line.is_empty() || line.starts_with('#') {
222                return None;
223            }
224
225            parse_credential_line(line)
226        })
227        .collect::<HashMap<String, _>>();
228
229    ensure!(
230        !credential.is_empty(),
231        InvalidConfigSnafu {
232            value: filepath,
233            msg: "UserProvider's file must contains at least one valid credential",
234        }
235    );
236
237    Ok(credential)
238}
239
240/// Parse a line of credential in the format of `username=password` or `username:permission_mode=password`.
241///
242/// The password part accepts legacy plain text and explicit verifier formats:
243/// - `plain:<password>`
244/// - `pbkdf2_sha256:<iterations>:<hex-encoded-salt>:<hex-encoded-hash>`
245/// - `mysql_native_password:<hex-encoded-sha1-sha1-password>`
246pub(crate) fn parse_credential_line(
247    line: &str,
248) -> Option<(String, (PasswordVerifier, PermissionMode))> {
249    let parts = line.split('=').collect::<Vec<&str>>();
250    if parts.len() != 2 {
251        return None;
252    }
253
254    let (username_part, password) = (parts[0], parts[1]);
255    let (username, permission_mode) = if let Some((user, perm)) = username_part.split_once(':') {
256        (user, PermissionMode::from_str(perm))
257    } else {
258        (username_part, PermissionMode::default())
259    };
260
261    let verifier = PasswordVerifier::parse(password)?;
262
263    Some((username.to_string(), (verifier, permission_mode)))
264}
265
266fn authenticate_with_credential(
267    users: &UserInfoMap,
268    input_id: Identity<'_>,
269    input_pwd: Password<'_>,
270) -> Result<UserInfoRef> {
271    match input_id {
272        Identity::UserId(username, _) => {
273            ensure!(
274                !username.is_empty(),
275                IllegalParamSnafu {
276                    msg: "blank username"
277                }
278            );
279            let (verifier, permission_mode) = users.get(username).context(UserNotFoundSnafu {
280                username: username.to_string(),
281            })?;
282
283            match input_pwd {
284                Password::PlainText(pwd) => {
285                    ensure!(
286                        !pwd.expose_secret().is_empty(),
287                        IllegalParamSnafu {
288                            msg: "blank password"
289                        }
290                    );
291                    if verifier.verify_plain_text(pwd.expose_secret()) {
292                        Ok(DefaultUserInfo::with_name_and_permission(
293                            username,
294                            *permission_mode,
295                        ))
296                    } else {
297                        UserPasswordMismatchSnafu {
298                            username: username.to_string(),
299                        }
300                        .fail()
301                    }
302                }
303                Password::MysqlNativePassword(auth_data, salt) => verifier
304                    .verify_mysql_native_password(auth_data, salt, username)
305                    .map(|_| DefaultUserInfo::with_name_and_permission(username, *permission_mode)),
306                Password::PgMD5(_, _) => UnsupportedPasswordTypeSnafu {
307                    password_type: "pg_md5",
308                }
309                .fail(),
310            }
311        }
312    }
313}
314#[cfg(test)]
315mod tests {
316    use digest::Digest;
317    use sha1::Sha1;
318
319    use super::*;
320    use crate::common::mysql_native_password_hash;
321
322    fn plain(password: &str) -> PasswordVerifier {
323        PasswordVerifier::PlainText(password.to_string())
324    }
325
326    fn sha1_one(data: &[u8]) -> Vec<u8> {
327        let mut hasher = Sha1::new();
328        hasher.update(data);
329        hasher.finalize().to_vec()
330    }
331
332    fn mysql_native_password_auth_data(password: &str, salt: &[u8]) -> Vec<u8> {
333        let hash_stage_1 = sha1_one(password.as_bytes());
334        let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
335        let mut hasher = Sha1::new();
336        hasher.update(salt);
337        hasher.update(hash_stage_2);
338        let scramble = hasher.finalize();
339
340        hash_stage_1
341            .iter()
342            .zip(scramble.iter())
343            .map(|(lhs, rhs)| lhs ^ rhs)
344            .collect()
345    }
346
347    #[test]
348    fn test_parse_credential_line() {
349        // Basic username=password format
350        let result = parse_credential_line("admin=password123");
351        assert_eq!(
352            result,
353            Some((
354                "admin".to_string(),
355                (plain("password123"), PermissionMode::default())
356            ))
357        );
358
359        // Username with permission mode
360        let result = parse_credential_line("user:ReadOnly=secret");
361        assert_eq!(
362            result,
363            Some((
364                "user".to_string(),
365                (plain("secret"), PermissionMode::ReadOnly)
366            ))
367        );
368        let result = parse_credential_line("user:ro=secret");
369        assert_eq!(
370            result,
371            Some((
372                "user".to_string(),
373                (plain("secret"), PermissionMode::ReadOnly)
374            ))
375        );
376        // Username with WriteOnly permission mode
377        let result = parse_credential_line("writer:WriteOnly=mypass");
378        assert_eq!(
379            result,
380            Some((
381                "writer".to_string(),
382                (plain("mypass"), PermissionMode::WriteOnly)
383            ))
384        );
385
386        // Username with 'wo' as WriteOnly permission shorthand
387        let result = parse_credential_line("writer:wo=mypass");
388        assert_eq!(
389            result,
390            Some((
391                "writer".to_string(),
392                (plain("mypass"), PermissionMode::WriteOnly)
393            ))
394        );
395
396        // Username with complex password containing special characters
397        let result = parse_credential_line("admin:rw=p@ssw0rd!123");
398        assert_eq!(
399            result,
400            Some((
401                "admin".to_string(),
402                (plain("p@ssw0rd!123"), PermissionMode::ReadWrite)
403            ))
404        );
405
406        // Username with spaces should be preserved
407        let result = parse_credential_line("user name:WriteOnly=password");
408        assert_eq!(
409            result,
410            Some((
411                "user name".to_string(),
412                (plain("password"), PermissionMode::WriteOnly)
413            ))
414        );
415
416        let result = parse_credential_line("user=plain:password");
417        assert_eq!(
418            result,
419            Some((
420                "user".to_string(),
421                (plain("password"), PermissionMode::default())
422            ))
423        );
424
425        let iterations = 4096;
426        let salt = b"salt";
427        let mut hash = [0u8; 32];
428        pbkdf2_hmac::<Sha256>("password".as_bytes(), salt, iterations, &mut hash);
429        let result = parse_credential_line(&format!(
430            "user=pbkdf2_sha256:{iterations}:{}:{}",
431            hex::encode(salt),
432            hex::encode(hash)
433        ));
434        assert_eq!(
435            result,
436            Some((
437                "user".to_string(),
438                (
439                    PasswordVerifier::Pbkdf2Sha256 {
440                        iterations,
441                        salt: salt.to_vec(),
442                        hash: hash.to_vec(),
443                    },
444                    PermissionMode::default()
445                )
446            ))
447        );
448
449        let result = parse_credential_line("user=pbkdf2_sha256:4096:not-hex:abcd");
450        assert_eq!(result, None);
451
452        // A well-formed but truncated hash must be rejected: a short hash would let
453        // many wrong passwords pass by matching only a few derived bytes.
454        let result = parse_credential_line(&format!(
455            "user=pbkdf2_sha256:4096:{}:abcd",
456            hex::encode(salt)
457        ));
458        assert_eq!(result, None);
459
460        let result = parse_credential_line(&format!(
461            "user=pbkdf2_sha256:{}:{}:{}",
462            MAX_PBKDF2_SHA256_ITERATIONS + 1,
463            hex::encode(salt),
464            hex::encode(hash)
465        ));
466        assert_eq!(result, None);
467
468        let hash_stage_2 = mysql_native_password_hash("password".as_bytes());
469        let result = parse_credential_line(&format!(
470            "user=mysql_native_password:{}",
471            hex::encode(&hash_stage_2)
472        ));
473        assert_eq!(
474            result,
475            Some((
476                "user".to_string(),
477                (
478                    PasswordVerifier::MysqlNativePassword { hash_stage_2 },
479                    PermissionMode::default()
480                )
481            ))
482        );
483
484        let result = parse_credential_line("user=mysql_native_password:abcd");
485        assert_eq!(result, None);
486
487        // Invalid format - no equals sign
488        let result = parse_credential_line("invalid_line");
489        assert_eq!(result, None);
490
491        // Invalid format - multiple equals signs
492        let result = parse_credential_line("user=pass=word");
493        assert_eq!(result, None);
494
495        // Empty password
496        let result = parse_credential_line("user=");
497        assert_eq!(
498            result,
499            Some(("user".to_string(), (plain(""), PermissionMode::default())))
500        );
501
502        // Empty username
503        let result = parse_credential_line("=password");
504        assert_eq!(
505            result,
506            Some((
507                "".to_string(),
508                (plain("password"), PermissionMode::default())
509            ))
510        );
511    }
512
513    #[test]
514    fn test_authenticate_with_mysql_native_password_verifier() {
515        let password = "password";
516        let salt = b"12345678901234567890";
517        let hash_stage_2 = mysql_native_password_hash(password.as_bytes());
518        let auth_data = mysql_native_password_auth_data(password, salt);
519        let users = HashMap::from([(
520            "user".to_string(),
521            (
522                PasswordVerifier::MysqlNativePassword { hash_stage_2 },
523                PermissionMode::default(),
524            ),
525        )]);
526
527        let result = authenticate_with_credential(
528            &users,
529            Identity::UserId("user", None),
530            Password::MysqlNativePassword(&auth_data, salt),
531        );
532
533        assert!(result.is_ok());
534    }
535
536    #[test]
537    fn test_authenticate_with_plain_text_mysql_native_password() {
538        let password = "password";
539        let salt = b"12345678901234567890";
540        let auth_data = mysql_native_password_auth_data(password, salt);
541        let users = HashMap::from([(
542            "user".to_string(),
543            (
544                PasswordVerifier::PlainText(password.to_string()),
545                PermissionMode::default(),
546            ),
547        )]);
548
549        let result = authenticate_with_credential(
550            &users,
551            Identity::UserId("user", None),
552            Password::MysqlNativePassword(&auth_data, salt),
553        );
554
555        assert!(result.is_ok());
556    }
557
558    #[test]
559    fn test_pbkdf2_sha256_rejects_mysql_native_password() {
560        let password = "password";
561        let salt = b"salt";
562        let iterations = 4096;
563        let mut hash = [0u8; 32];
564        pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
565        let users = HashMap::from([(
566            "user".to_string(),
567            (
568                PasswordVerifier::Pbkdf2Sha256 {
569                    iterations,
570                    salt: salt.to_vec(),
571                    hash: hash.to_vec(),
572                },
573                PermissionMode::default(),
574            ),
575        )]);
576        let mysql_salt = b"12345678901234567890";
577        let auth_data = mysql_native_password_auth_data(password, mysql_salt);
578
579        let result = authenticate_with_credential(
580            &users,
581            Identity::UserId("user", None),
582            Password::MysqlNativePassword(&auth_data, mysql_salt),
583        );
584
585        assert!(result.is_err());
586    }
587
588    #[test]
589    fn test_password_verifier_debug_redacts_secrets() {
590        let debug = format!("{:?}", PasswordVerifier::PlainText("secret".to_string()));
591        assert!(debug.contains("<REDACTED>"));
592        assert!(!debug.contains("secret"));
593
594        let debug = format!(
595            "{:?}",
596            PasswordVerifier::Pbkdf2Sha256 {
597                iterations: 4096,
598                salt: b"super-secret-salt".to_vec(),
599                hash: b"super-secret-hash".to_vec(),
600            }
601        );
602        assert!(debug.contains("Pbkdf2Sha256"));
603        assert!(debug.contains("4096"));
604        assert!(!debug.contains("super-secret-salt"));
605        assert!(!debug.contains("super-secret-hash"));
606
607        let debug = format!(
608            "{:?}",
609            PasswordVerifier::MysqlNativePassword {
610                hash_stage_2: b"super-secret-hash".to_vec(),
611            }
612        );
613        assert!(debug.contains("MysqlNativePassword"));
614        assert!(!debug.contains("super-secret-hash"));
615    }
616}