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;
21use std::io::BufRead;
22use std::path::Path;
23
24use common_base::secrets::ExposeSecret;
25use snafu::{OptionExt, ResultExt, ensure};
26
27use crate::common::{Identity, Password};
28use crate::error::{
29    IllegalParamSnafu, InvalidConfigSnafu, IoSnafu, Result, UnsupportedPasswordTypeSnafu,
30    UserNotFoundSnafu, UserPasswordMismatchSnafu,
31};
32use crate::user_info::{DefaultUserInfo, PermissionMode};
33use crate::{UserInfoRef, auth_mysql};
34
35#[async_trait::async_trait]
36pub trait UserProvider: Send + Sync {
37    fn name(&self) -> &str;
38
39    /// Checks whether a user is valid and allowed to access the database.
40    async fn authenticate(&self, id: Identity<'_>, password: Password<'_>) -> Result<UserInfoRef>;
41
42    /// Checks whether a connection request
43    /// from a certain user to a certain catalog/schema is legal.
44    /// This method should be called after [authenticate()](UserProvider::authenticate()).
45    async fn authorize(&self, catalog: &str, schema: &str, user_info: &UserInfoRef) -> Result<()>;
46
47    /// Combination of [authenticate()](UserProvider::authenticate()) and [authorize()](UserProvider::authorize()).
48    /// In most cases it's preferred for both convenience and performance.
49    async fn auth(
50        &self,
51        id: Identity<'_>,
52        password: Password<'_>,
53        catalog: &str,
54        schema: &str,
55    ) -> Result<UserInfoRef> {
56        let user_info = self.authenticate(id, password).await?;
57        self.authorize(catalog, schema, &user_info).await?;
58        Ok(user_info)
59    }
60
61    /// Returns whether this user provider implementation is backed by an external system.
62    fn external(&self) -> bool {
63        false
64    }
65}
66
67/// Type alias for user info map
68/// Key is username, value is (password, permission_mode)
69pub type UserInfoMap = HashMap<String, (Vec<u8>, PermissionMode)>;
70
71fn load_credential_from_file(filepath: &str) -> Result<UserInfoMap> {
72    // check valid path
73    let path = Path::new(filepath);
74    if !path.exists() {
75        return InvalidConfigSnafu {
76            value: filepath.to_string(),
77            msg: "UserProvider file must exist",
78        }
79        .fail();
80    }
81
82    ensure!(
83        path.is_file(),
84        InvalidConfigSnafu {
85            value: filepath,
86            msg: "UserProvider file must be a file",
87        }
88    );
89    let file = File::open(path).context(IoSnafu)?;
90    let credential = io::BufReader::new(file)
91        .lines()
92        .map_while(std::result::Result::ok)
93        .filter_map(|line| {
94            // The line format is:
95            // - `username=password` - Basic user with default permissions
96            // - `username:permission_mode=password` - User with specific permission mode
97            // - Lines starting with '#' are treated as comments and ignored
98            // - Empty lines are ignored
99            let line = line.trim();
100            if line.is_empty() || line.starts_with('#') {
101                return None;
102            }
103
104            parse_credential_line(line)
105        })
106        .collect::<HashMap<String, _>>();
107
108    ensure!(
109        !credential.is_empty(),
110        InvalidConfigSnafu {
111            value: filepath,
112            msg: "UserProvider's file must contains at least one valid credential",
113        }
114    );
115
116    Ok(credential)
117}
118
119/// Parse a line of credential in the format of `username=password` or `username:permission_mode=password`
120pub(crate) fn parse_credential_line(line: &str) -> Option<(String, (Vec<u8>, PermissionMode))> {
121    let parts = line.split('=').collect::<Vec<&str>>();
122    if parts.len() != 2 {
123        return None;
124    }
125
126    let (username_part, password) = (parts[0], parts[1]);
127    let (username, permission_mode) = if let Some((user, perm)) = username_part.split_once(':') {
128        (user, PermissionMode::from_str(perm))
129    } else {
130        (username_part, PermissionMode::default())
131    };
132
133    Some((
134        username.to_string(),
135        (password.as_bytes().to_vec(), permission_mode),
136    ))
137}
138
139fn authenticate_with_credential(
140    users: &UserInfoMap,
141    input_id: Identity<'_>,
142    input_pwd: Password<'_>,
143) -> Result<UserInfoRef> {
144    match input_id {
145        Identity::UserId(username, _) => {
146            ensure!(
147                !username.is_empty(),
148                IllegalParamSnafu {
149                    msg: "blank username"
150                }
151            );
152            let (save_pwd, permission_mode) = users.get(username).context(UserNotFoundSnafu {
153                username: username.to_string(),
154            })?;
155
156            match input_pwd {
157                Password::PlainText(pwd) => {
158                    ensure!(
159                        !pwd.expose_secret().is_empty(),
160                        IllegalParamSnafu {
161                            msg: "blank password"
162                        }
163                    );
164                    if save_pwd == pwd.expose_secret().as_bytes() {
165                        Ok(DefaultUserInfo::with_name_and_permission(
166                            username,
167                            *permission_mode,
168                        ))
169                    } else {
170                        UserPasswordMismatchSnafu {
171                            username: username.to_string(),
172                        }
173                        .fail()
174                    }
175                }
176                Password::MysqlNativePassword(auth_data, salt) => {
177                    auth_mysql(auth_data, salt, username, save_pwd).map(|_| {
178                        DefaultUserInfo::with_name_and_permission(username, *permission_mode)
179                    })
180                }
181                Password::PgMD5(_, _) => UnsupportedPasswordTypeSnafu {
182                    password_type: "pg_md5",
183                }
184                .fail(),
185            }
186        }
187    }
188}
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_parse_credential_line() {
195        // Basic username=password format
196        let result = parse_credential_line("admin=password123");
197        assert_eq!(
198            result,
199            Some((
200                "admin".to_string(),
201                ("password123".as_bytes().to_vec(), PermissionMode::default())
202            ))
203        );
204
205        // Username with permission mode
206        let result = parse_credential_line("user:ReadOnly=secret");
207        assert_eq!(
208            result,
209            Some((
210                "user".to_string(),
211                ("secret".as_bytes().to_vec(), PermissionMode::ReadOnly)
212            ))
213        );
214        let result = parse_credential_line("user:ro=secret");
215        assert_eq!(
216            result,
217            Some((
218                "user".to_string(),
219                ("secret".as_bytes().to_vec(), PermissionMode::ReadOnly)
220            ))
221        );
222        // Username with WriteOnly permission mode
223        let result = parse_credential_line("writer:WriteOnly=mypass");
224        assert_eq!(
225            result,
226            Some((
227                "writer".to_string(),
228                ("mypass".as_bytes().to_vec(), PermissionMode::WriteOnly)
229            ))
230        );
231
232        // Username with 'wo' as WriteOnly permission shorthand
233        let result = parse_credential_line("writer:wo=mypass");
234        assert_eq!(
235            result,
236            Some((
237                "writer".to_string(),
238                ("mypass".as_bytes().to_vec(), PermissionMode::WriteOnly)
239            ))
240        );
241
242        // Username with complex password containing special characters
243        let result = parse_credential_line("admin:rw=p@ssw0rd!123");
244        assert_eq!(
245            result,
246            Some((
247                "admin".to_string(),
248                (
249                    "p@ssw0rd!123".as_bytes().to_vec(),
250                    PermissionMode::ReadWrite
251                )
252            ))
253        );
254
255        // Username with spaces should be preserved
256        let result = parse_credential_line("user name:WriteOnly=password");
257        assert_eq!(
258            result,
259            Some((
260                "user name".to_string(),
261                ("password".as_bytes().to_vec(), PermissionMode::WriteOnly)
262            ))
263        );
264
265        // Invalid format - no equals sign
266        let result = parse_credential_line("invalid_line");
267        assert_eq!(result, None);
268
269        // Invalid format - multiple equals signs
270        let result = parse_credential_line("user=pass=word");
271        assert_eq!(result, None);
272
273        // Empty password
274        let result = parse_credential_line("user=");
275        assert_eq!(
276            result,
277            Some((
278                "user".to_string(),
279                ("".as_bytes().to_vec(), PermissionMode::default())
280            ))
281        );
282
283        // Empty username
284        let result = parse_credential_line("=password");
285        assert_eq!(
286            result,
287            Some((
288                "".to_string(),
289                ("password".as_bytes().to_vec(), PermissionMode::default())
290            ))
291        );
292    }
293}