1use indoc::formatdoc;
2use itertools::Itertools;
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6
7use sqlx::MySqlConnection;
8use sqlx::prelude::*;
9
10use crate::{
11 core::{
12 common::UnixUser,
13 database_privileges::DATABASE_PRIVILEGE_FIELDS,
14 protocol::{
15 CreateUserError, CreateUsersResponse, DropUserError, DropUsersResponse,
16 ListAllUsersError, ListAllUsersResponse, ListUsersError, ListUsersResponse,
17 LockUserError, LockUsersResponse, SetPasswordError, SetUserPasswordResponse,
18 UnlockUserError, UnlockUsersResponse,
19 },
20 types::MySQLUser,
21 },
22 server::{
23 common::{create_user_group_matching_regex, try_get_with_binary_fallback},
24 input_sanitization::{quote_literal, validate_name, validate_ownership_by_unix_user},
25 },
26};
27
28async fn unsafe_user_exists(
30 db_user: &str,
31 connection: &mut MySqlConnection,
32) -> Result<bool, sqlx::Error> {
33 let result = sqlx::query(
34 r#"
35 SELECT EXISTS(
36 SELECT 1
37 FROM `mysql`.`user`
38 WHERE `User` = ?
39 )
40 "#,
41 )
42 .bind(db_user)
43 .fetch_one(connection)
44 .await
45 .map(|row| row.get::<bool, _>(0));
46
47 if let Err(err) = &result {
48 log::error!("Failed to check if database user exists: {:?}", err);
49 }
50
51 result
52}
53
54pub async fn create_database_users(
55 db_users: Vec<MySQLUser>,
56 unix_user: &UnixUser,
57 connection: &mut MySqlConnection,
58) -> CreateUsersResponse {
59 let mut results = BTreeMap::new();
60
61 for db_user in db_users {
62 if let Err(err) = validate_name(&db_user) {
63 results.insert(db_user, Err(CreateUserError::SanitizationError(err)));
64 continue;
65 }
66
67 if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
68 results.insert(db_user, Err(CreateUserError::OwnershipError(err)));
69 continue;
70 }
71
72 match unsafe_user_exists(&db_user, &mut *connection).await {
73 Ok(true) => {
74 results.insert(db_user, Err(CreateUserError::UserAlreadyExists));
75 continue;
76 }
77 Err(err) => {
78 results.insert(db_user, Err(CreateUserError::MySqlError(err.to_string())));
79 continue;
80 }
81 _ => {}
82 }
83
84 let result = sqlx::query(format!("CREATE USER {}@'%'", quote_literal(&db_user),).as_str())
85 .execute(&mut *connection)
86 .await
87 .map(|_| ())
88 .map_err(|err| CreateUserError::MySqlError(err.to_string()));
89
90 if let Err(err) = &result {
91 log::error!("Failed to create database user '{}': {:?}", &db_user, err);
92 }
93
94 results.insert(db_user, result);
95 }
96
97 results
98}
99
100pub async fn drop_database_users(
101 db_users: Vec<MySQLUser>,
102 unix_user: &UnixUser,
103 connection: &mut MySqlConnection,
104) -> DropUsersResponse {
105 let mut results = BTreeMap::new();
106
107 for db_user in db_users {
108 if let Err(err) = validate_name(&db_user) {
109 results.insert(db_user, Err(DropUserError::SanitizationError(err)));
110 continue;
111 }
112
113 if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
114 results.insert(db_user, Err(DropUserError::OwnershipError(err)));
115 continue;
116 }
117
118 match unsafe_user_exists(&db_user, &mut *connection).await {
119 Ok(false) => {
120 results.insert(db_user, Err(DropUserError::UserDoesNotExist));
121 continue;
122 }
123 Err(err) => {
124 results.insert(db_user, Err(DropUserError::MySqlError(err.to_string())));
125 continue;
126 }
127 _ => {}
128 }
129
130 let result = sqlx::query(format!("DROP USER {}@'%'", quote_literal(&db_user),).as_str())
131 .execute(&mut *connection)
132 .await
133 .map(|_| ())
134 .map_err(|err| DropUserError::MySqlError(err.to_string()));
135
136 if let Err(err) = &result {
137 log::error!("Failed to drop database user '{}': {:?}", &db_user, err);
138 }
139
140 results.insert(db_user, result);
141 }
142
143 results
144}
145
146pub async fn set_password_for_database_user(
147 db_user: &MySQLUser,
148 password: &str,
149 unix_user: &UnixUser,
150 connection: &mut MySqlConnection,
151) -> SetUserPasswordResponse {
152 if let Err(err) = validate_name(db_user) {
153 return Err(SetPasswordError::SanitizationError(err));
154 }
155
156 if let Err(err) = validate_ownership_by_unix_user(db_user, unix_user) {
157 return Err(SetPasswordError::OwnershipError(err));
158 }
159
160 match unsafe_user_exists(db_user, &mut *connection).await {
161 Ok(false) => return Err(SetPasswordError::UserDoesNotExist),
162 Err(err) => return Err(SetPasswordError::MySqlError(err.to_string())),
163 _ => {}
164 }
165
166 let result = sqlx::query(
167 format!(
168 "ALTER USER {}@'%' IDENTIFIED BY {}",
169 quote_literal(db_user),
170 quote_literal(password).as_str(),
171 )
172 .as_str(),
173 )
174 .execute(&mut *connection)
175 .await
176 .map(|_| ())
177 .map_err(|err| SetPasswordError::MySqlError(err.to_string()));
178
179 if result.is_err() {
180 log::error!(
181 "Failed to set password for database user '{}': <REDACTED>",
182 &db_user,
183 );
184 }
185
186 result
187}
188
189async fn database_user_is_locked_unsafe(
191 db_user: &str,
192 connection: &mut MySqlConnection,
193) -> Result<bool, sqlx::Error> {
194 let result = sqlx::query(
195 r#"
196 SELECT COALESCE(
197 JSON_EXTRACT(`mysql`.`global_priv`.`priv`, "$.account_locked"),
198 'false'
199 ) != 'false'
200 FROM `mysql`.`global_priv`
201 WHERE `User` = ?
202 AND `Host` = '%'
203 "#,
204 )
205 .bind(db_user)
206 .fetch_one(connection)
207 .await
208 .map(|row| row.get::<bool, _>(0));
209
210 if let Err(err) = &result {
211 log::error!(
212 "Failed to check if database user is locked '{}': {:?}",
213 &db_user,
214 err
215 );
216 }
217
218 result
219}
220
221pub async fn lock_database_users(
222 db_users: Vec<MySQLUser>,
223 unix_user: &UnixUser,
224 connection: &mut MySqlConnection,
225) -> LockUsersResponse {
226 let mut results = BTreeMap::new();
227
228 for db_user in db_users {
229 if let Err(err) = validate_name(&db_user) {
230 results.insert(db_user, Err(LockUserError::SanitizationError(err)));
231 continue;
232 }
233
234 if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
235 results.insert(db_user, Err(LockUserError::OwnershipError(err)));
236 continue;
237 }
238
239 match unsafe_user_exists(&db_user, &mut *connection).await {
240 Ok(true) => {}
241 Ok(false) => {
242 results.insert(db_user, Err(LockUserError::UserDoesNotExist));
243 continue;
244 }
245 Err(err) => {
246 results.insert(db_user, Err(LockUserError::MySqlError(err.to_string())));
247 continue;
248 }
249 }
250
251 match database_user_is_locked_unsafe(&db_user, &mut *connection).await {
252 Ok(false) => {}
253 Ok(true) => {
254 results.insert(db_user, Err(LockUserError::UserIsAlreadyLocked));
255 continue;
256 }
257 Err(err) => {
258 results.insert(db_user, Err(LockUserError::MySqlError(err.to_string())));
259 continue;
260 }
261 }
262
263 let result = sqlx::query(
264 format!("ALTER USER {}@'%' ACCOUNT LOCK", quote_literal(&db_user),).as_str(),
265 )
266 .execute(&mut *connection)
267 .await
268 .map(|_| ())
269 .map_err(|err| LockUserError::MySqlError(err.to_string()));
270
271 if let Err(err) = &result {
272 log::error!("Failed to lock database user '{}': {:?}", &db_user, err);
273 }
274
275 results.insert(db_user, result);
276 }
277
278 results
279}
280
281pub async fn unlock_database_users(
282 db_users: Vec<MySQLUser>,
283 unix_user: &UnixUser,
284 connection: &mut MySqlConnection,
285) -> UnlockUsersResponse {
286 let mut results = BTreeMap::new();
287
288 for db_user in db_users {
289 if let Err(err) = validate_name(&db_user) {
290 results.insert(db_user, Err(UnlockUserError::SanitizationError(err)));
291 continue;
292 }
293
294 if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
295 results.insert(db_user, Err(UnlockUserError::OwnershipError(err)));
296 continue;
297 }
298
299 match unsafe_user_exists(&db_user, &mut *connection).await {
300 Ok(false) => {
301 results.insert(db_user, Err(UnlockUserError::UserDoesNotExist));
302 continue;
303 }
304 Err(err) => {
305 results.insert(db_user, Err(UnlockUserError::MySqlError(err.to_string())));
306 continue;
307 }
308 _ => {}
309 }
310
311 match database_user_is_locked_unsafe(&db_user, &mut *connection).await {
312 Ok(false) => {
313 results.insert(db_user, Err(UnlockUserError::UserIsAlreadyUnlocked));
314 continue;
315 }
316 Err(err) => {
317 results.insert(db_user, Err(UnlockUserError::MySqlError(err.to_string())));
318 continue;
319 }
320 _ => {}
321 }
322
323 let result = sqlx::query(
324 format!("ALTER USER {}@'%' ACCOUNT UNLOCK", quote_literal(&db_user),).as_str(),
325 )
326 .execute(&mut *connection)
327 .await
328 .map(|_| ())
329 .map_err(|err| UnlockUserError::MySqlError(err.to_string()));
330
331 if let Err(err) = &result {
332 log::error!("Failed to unlock database user '{}': {:?}", &db_user, err);
333 }
334
335 results.insert(db_user, result);
336 }
337
338 results
339}
340
341#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
344pub struct DatabaseUser {
345 pub user: MySQLUser,
346 #[serde(skip)]
347 pub host: String,
348 pub has_password: bool,
349 pub is_locked: bool,
350 pub databases: Vec<String>,
351}
352
353impl FromRow<'_, sqlx::mysql::MySqlRow> for DatabaseUser {
354 fn from_row(row: &sqlx::mysql::MySqlRow) -> Result<Self, sqlx::Error> {
355 Ok(Self {
356 user: try_get_with_binary_fallback(row, "User")?.into(),
357 host: try_get_with_binary_fallback(row, "Host")?,
358 has_password: row.try_get("has_password")?,
359 is_locked: row.try_get("is_locked")?,
360 databases: Vec::new(),
361 })
362 }
363}
364
365const DB_USER_SELECT_STATEMENT: &str = r#"
366SELECT
367 `user`.`User`,
368 `user`.`Host`,
369 `user`.`Password` != '' OR `user`.`authentication_string` != '' AS `has_password`,
370 COALESCE(
371 JSON_EXTRACT(`global_priv`.`priv`, "$.account_locked"),
372 'false'
373 ) != 'false' AS `is_locked`
374FROM `user`
375JOIN `global_priv` ON
376 `user`.`User` = `global_priv`.`User`
377 AND `user`.`Host` = `global_priv`.`Host`
378"#;
379
380pub async fn list_database_users(
381 db_users: Vec<MySQLUser>,
382 unix_user: &UnixUser,
383 connection: &mut MySqlConnection,
384) -> ListUsersResponse {
385 let mut results = BTreeMap::new();
386
387 for db_user in db_users {
388 if let Err(err) = validate_name(&db_user) {
389 results.insert(db_user, Err(ListUsersError::SanitizationError(err)));
390 continue;
391 }
392
393 if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
394 results.insert(db_user, Err(ListUsersError::OwnershipError(err)));
395 continue;
396 }
397
398 let mut result = sqlx::query_as::<_, DatabaseUser>(
399 &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` = ?"),
400 )
401 .bind(db_user.as_str())
402 .fetch_optional(&mut *connection)
403 .await;
404
405 if let Err(err) = &result {
406 log::error!("Failed to list database user '{}': {:?}", &db_user, err);
407 }
408
409 if let Ok(Some(user)) = result.as_mut() {
410 append_databases_where_user_has_privileges(user, &mut *connection).await;
411 }
412
413 match result {
414 Ok(Some(user)) => results.insert(db_user, Ok(user)),
415 Ok(None) => results.insert(db_user, Err(ListUsersError::UserDoesNotExist)),
416 Err(err) => results.insert(db_user, Err(ListUsersError::MySqlError(err.to_string()))),
417 };
418 }
419
420 results
421}
422
423pub async fn list_all_database_users_for_unix_user(
424 unix_user: &UnixUser,
425 connection: &mut MySqlConnection,
426) -> ListAllUsersResponse {
427 let mut result = sqlx::query_as::<_, DatabaseUser>(
428 &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `user`.`User` REGEXP ?"),
429 )
430 .bind(create_user_group_matching_regex(unix_user))
431 .fetch_all(&mut *connection)
432 .await
433 .map_err(|err| ListAllUsersError::MySqlError(err.to_string()));
434
435 if let Err(err) = &result {
436 log::error!("Failed to list all database users: {:?}", err);
437 }
438
439 if let Ok(users) = result.as_mut() {
440 for user in users {
441 append_databases_where_user_has_privileges(user, &mut *connection).await;
442 }
443 }
444
445 result
446}
447
448pub async fn append_databases_where_user_has_privileges(
449 db_user: &mut DatabaseUser,
450 connection: &mut MySqlConnection,
451) {
452 let database_list = sqlx::query(
453 formatdoc!(
454 r#"
455 SELECT `Db` AS `database`
456 FROM `db`
457 WHERE `User` = ? AND ({})
458 "#,
459 DATABASE_PRIVILEGE_FIELDS
460 .iter()
461 .map(|field| format!("`{}` = 'Y'", field))
462 .join(" OR "),
463 )
464 .as_str(),
465 )
466 .bind(db_user.user.as_str())
467 .fetch_all(&mut *connection)
468 .await;
469
470 if let Err(err) = &database_list {
471 log::error!(
472 "Failed to list databases for user '{}': {:?}",
473 &db_user.user,
474 err
475 );
476 }
477
478 db_user.databases = database_list
479 .map(|rows| {
480 rows.into_iter()
481 .map(|row| try_get_with_binary_fallback(&row, "database").unwrap())
482 .collect()
483 })
484 .unwrap_or_default();
485}