mysqladm/server/sql/
database_privilege_operations.rs

1// TODO: fix comment
2//! Database privilege operations
3//!
4//! This module contains functions for querying, modifying,
5//! displaying and comparing database privileges.
6//!
7//! A lot of the complexity comes from two core components:
8//!
9//! - The privilege editor that needs to be able to print
10//!   an editable table of privileges and reparse the content
11//!   after the user has made manual changes.
12//!
13//! - The comparison functionality that tells the user what
14//!   changes will be made when applying a set of changes
15//!   to the list of database privileges.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use indoc::indoc;
20use itertools::Itertools;
21use sqlx::{MySqlConnection, mysql::MySqlRow, prelude::*};
22
23use crate::{
24    core::{
25        common::{UnixUser, rev_yn, yn},
26        database_privileges::{
27            DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeChange, DatabasePrivilegeRow,
28            DatabasePrivilegesDiff,
29        },
30        protocol::{
31            DiffDoesNotApplyError, GetAllDatabasesPrivilegeDataError,
32            GetDatabasesPrivilegeDataError, ListAllPrivilegesResponse, ListPrivilegesResponse,
33            ModifyDatabasePrivilegesError, ModifyPrivilegesResponse,
34        },
35        types::{MySQLDatabase, MySQLUser},
36    },
37    server::{
38        common::{create_user_group_matching_regex, try_get_with_binary_fallback},
39        input_sanitization::{quote_identifier, validate_name, validate_ownership_by_unix_user},
40        sql::database_operations::unsafe_database_exists,
41    },
42};
43
44// TODO: get by name instead of row tuple position
45
46#[inline]
47fn get_mysql_row_priv_field(row: &MySqlRow, position: usize) -> Result<bool, sqlx::Error> {
48    let field = DATABASE_PRIVILEGE_FIELDS[position];
49    let value = row.try_get(position)?;
50    match rev_yn(value) {
51        Some(val) => Ok(val),
52        _ => {
53            log::warn!(r#"Invalid value for privilege "{}": '{}'"#, field, value);
54            Ok(false)
55        }
56    }
57}
58
59impl FromRow<'_, MySqlRow> for DatabasePrivilegeRow {
60    fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
61        Ok(Self {
62            db: try_get_with_binary_fallback(row, "Db")?.into(),
63            user: try_get_with_binary_fallback(row, "User")?.into(),
64            select_priv: get_mysql_row_priv_field(row, 2)?,
65            insert_priv: get_mysql_row_priv_field(row, 3)?,
66            update_priv: get_mysql_row_priv_field(row, 4)?,
67            delete_priv: get_mysql_row_priv_field(row, 5)?,
68            create_priv: get_mysql_row_priv_field(row, 6)?,
69            drop_priv: get_mysql_row_priv_field(row, 7)?,
70            alter_priv: get_mysql_row_priv_field(row, 8)?,
71            index_priv: get_mysql_row_priv_field(row, 9)?,
72            create_tmp_table_priv: get_mysql_row_priv_field(row, 10)?,
73            lock_tables_priv: get_mysql_row_priv_field(row, 11)?,
74            references_priv: get_mysql_row_priv_field(row, 12)?,
75        })
76    }
77}
78
79// NOTE: this function is unsafe because it does no input validation.
80/// Get all users + privileges for a single database.
81async fn unsafe_get_database_privileges(
82    database_name: &str,
83    connection: &mut MySqlConnection,
84) -> Result<Vec<DatabasePrivilegeRow>, sqlx::Error> {
85    let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
86        "SELECT {} FROM `db` WHERE `Db` = ?",
87        DATABASE_PRIVILEGE_FIELDS
88            .iter()
89            .map(|field| quote_identifier(field))
90            .join(","),
91    ))
92    .bind(database_name)
93    .fetch_all(connection)
94    .await;
95
96    if let Err(e) = &result {
97        log::error!(
98            "Failed to get database privileges for '{}': {}",
99            &database_name,
100            e
101        );
102    }
103
104    result
105}
106
107// NOTE: this function is unsafe because it does no input validation.
108/// Get all users + privileges for a single database-user pair.
109pub async fn unsafe_get_database_privileges_for_db_user_pair(
110    database_name: &MySQLDatabase,
111    user_name: &MySQLUser,
112    connection: &mut MySqlConnection,
113) -> Result<Option<DatabasePrivilegeRow>, sqlx::Error> {
114    let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
115        "SELECT {} FROM `db` WHERE `Db` = ? AND `User` = ?",
116        DATABASE_PRIVILEGE_FIELDS
117            .iter()
118            .map(|field| quote_identifier(field))
119            .join(","),
120    ))
121    .bind(database_name.as_str())
122    .bind(user_name.as_str())
123    .fetch_optional(connection)
124    .await;
125
126    if let Err(e) = &result {
127        log::error!(
128            "Failed to get database privileges for '{}.{}': {}",
129            &database_name,
130            &user_name,
131            e
132        );
133    }
134
135    result
136}
137
138pub async fn get_databases_privilege_data(
139    database_names: Vec<MySQLDatabase>,
140    unix_user: &UnixUser,
141    connection: &mut MySqlConnection,
142) -> ListPrivilegesResponse {
143    let mut results = BTreeMap::new();
144
145    for database_name in database_names.iter() {
146        if let Err(err) = validate_name(database_name) {
147            results.insert(
148                database_name.to_owned(),
149                Err(GetDatabasesPrivilegeDataError::SanitizationError(err)),
150            );
151            continue;
152        }
153
154        if let Err(err) = validate_ownership_by_unix_user(database_name, unix_user) {
155            results.insert(
156                database_name.to_owned(),
157                Err(GetDatabasesPrivilegeDataError::OwnershipError(err)),
158            );
159            continue;
160        }
161
162        if !unsafe_database_exists(database_name, connection)
163            .await
164            .unwrap()
165        {
166            results.insert(
167                database_name.to_owned(),
168                Err(GetDatabasesPrivilegeDataError::DatabaseDoesNotExist),
169            );
170            continue;
171        }
172
173        let result = unsafe_get_database_privileges(database_name, connection)
174            .await
175            .map_err(|e| GetDatabasesPrivilegeDataError::MySqlError(e.to_string()));
176
177        results.insert(database_name.to_owned(), result);
178    }
179
180    debug_assert!(database_names.len() == results.len());
181
182    results
183}
184
185/// Get all database + user + privileges pairs that are owned by the current user.
186pub async fn get_all_database_privileges(
187    unix_user: &UnixUser,
188    connection: &mut MySqlConnection,
189) -> ListAllPrivilegesResponse {
190    let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
191        indoc! {r#"
192          SELECT {} FROM `db` WHERE `db` IN
193          (SELECT DISTINCT `SCHEMA_NAME` AS `database`
194            FROM `information_schema`.`SCHEMATA`
195            WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
196              AND `SCHEMA_NAME` REGEXP ?)
197        "#},
198        DATABASE_PRIVILEGE_FIELDS
199            .iter()
200            .map(|field| quote_identifier(field))
201            .join(","),
202    ))
203    .bind(create_user_group_matching_regex(unix_user))
204    .fetch_all(connection)
205    .await
206    .map_err(|e| GetAllDatabasesPrivilegeDataError::MySqlError(e.to_string()));
207
208    if let Err(e) = &result {
209        log::error!("Failed to get all database privileges: {:?}", e);
210    }
211
212    result
213}
214
215async fn unsafe_apply_privilege_diff(
216    database_privilege_diff: &DatabasePrivilegesDiff,
217    connection: &mut MySqlConnection,
218) -> Result<(), sqlx::Error> {
219    let result = match database_privilege_diff {
220        DatabasePrivilegesDiff::New(p) => {
221            let tables = DATABASE_PRIVILEGE_FIELDS
222                .iter()
223                .map(|field| quote_identifier(field))
224                .join(",");
225
226            let question_marks =
227                std::iter::repeat_n("?", DATABASE_PRIVILEGE_FIELDS.len()).join(",");
228
229            sqlx::query(
230                format!("INSERT INTO `db` ({}) VALUES ({})", tables, question_marks).as_str(),
231            )
232            .bind(p.db.to_string())
233            .bind(p.user.to_string())
234            .bind(yn(p.select_priv))
235            .bind(yn(p.insert_priv))
236            .bind(yn(p.update_priv))
237            .bind(yn(p.delete_priv))
238            .bind(yn(p.create_priv))
239            .bind(yn(p.drop_priv))
240            .bind(yn(p.alter_priv))
241            .bind(yn(p.index_priv))
242            .bind(yn(p.create_tmp_table_priv))
243            .bind(yn(p.lock_tables_priv))
244            .bind(yn(p.references_priv))
245            .execute(connection)
246            .await
247            .map(|_| ())
248        }
249        DatabasePrivilegesDiff::Modified(p) => {
250            let changes = DATABASE_PRIVILEGE_FIELDS
251                .iter()
252                .skip(2) // Skip Db and User fields
253                .map(|field| {
254                    format!(
255                        "{} = COALESCE(?, {})",
256                        quote_identifier(field),
257                        quote_identifier(field)
258                    )
259                })
260                .join(",");
261
262            fn change_to_yn(change: DatabasePrivilegeChange) -> &'static str {
263                match change {
264                    DatabasePrivilegeChange::YesToNo => "N",
265                    DatabasePrivilegeChange::NoToYes => "Y",
266                }
267            }
268
269            sqlx::query(
270                format!("UPDATE `db` SET {} WHERE `Db` = ? AND `User` = ?", changes).as_str(),
271            )
272            .bind(p.select_priv.map(change_to_yn))
273            .bind(p.insert_priv.map(change_to_yn))
274            .bind(p.update_priv.map(change_to_yn))
275            .bind(p.delete_priv.map(change_to_yn))
276            .bind(p.create_priv.map(change_to_yn))
277            .bind(p.drop_priv.map(change_to_yn))
278            .bind(p.alter_priv.map(change_to_yn))
279            .bind(p.index_priv.map(change_to_yn))
280            .bind(p.create_tmp_table_priv.map(change_to_yn))
281            .bind(p.lock_tables_priv.map(change_to_yn))
282            .bind(p.references_priv.map(change_to_yn))
283            .bind(p.db.to_string())
284            .bind(p.user.to_string())
285            .execute(connection)
286            .await
287            .map(|_| ())
288        }
289        DatabasePrivilegesDiff::Deleted(p) => {
290            sqlx::query("DELETE FROM `db` WHERE `Db` = ? AND `User` = ?")
291                .bind(p.db.to_string())
292                .bind(p.user.to_string())
293                .execute(connection)
294                .await
295                .map(|_| ())
296        }
297        DatabasePrivilegesDiff::Noop { .. } => Ok(()),
298    };
299
300    if let Err(e) = &result {
301        log::error!("Failed to apply database privilege diff: {}", e);
302    }
303
304    result
305}
306
307async fn validate_diff(
308    diff: &DatabasePrivilegesDiff,
309    connection: &mut MySqlConnection,
310) -> Result<(), ModifyDatabasePrivilegesError> {
311    let privilege_row = unsafe_get_database_privileges_for_db_user_pair(
312        diff.get_database_name(),
313        diff.get_user_name(),
314        connection,
315    )
316    .await;
317
318    let privilege_row = match privilege_row {
319        Ok(privilege_row) => privilege_row,
320        Err(e) => return Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
321    };
322
323    match diff {
324        DatabasePrivilegesDiff::New(_) => {
325            if privilege_row.is_some() {
326                Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
327                    DiffDoesNotApplyError::RowAlreadyExists(
328                        diff.get_database_name().to_owned(),
329                        diff.get_user_name().to_owned(),
330                    ),
331                ))
332            } else {
333                Ok(())
334            }
335        }
336        DatabasePrivilegesDiff::Modified(_) if privilege_row.is_none() => {
337            Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
338                DiffDoesNotApplyError::RowDoesNotExist(
339                    diff.get_database_name().to_owned(),
340                    diff.get_user_name().to_owned(),
341                ),
342            ))
343        }
344        DatabasePrivilegesDiff::Modified(row_diff) => {
345            let row = privilege_row.unwrap();
346
347            let error_exists = DATABASE_PRIVILEGE_FIELDS
348                .iter()
349                .skip(2) // Skip Db and User fields
350                .any(
351                    |field| match row_diff.get_privilege_change_by_name(field).unwrap() {
352                        Some(DatabasePrivilegeChange::YesToNo) => {
353                            !row.get_privilege_by_name(field).unwrap()
354                        }
355                        Some(DatabasePrivilegeChange::NoToYes) => {
356                            row.get_privilege_by_name(field).unwrap()
357                        }
358                        None => false,
359                    },
360                );
361
362            if error_exists {
363                Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
364                    DiffDoesNotApplyError::RowPrivilegeChangeDoesNotApply(row_diff.to_owned(), row),
365                ))
366            } else {
367                Ok(())
368            }
369        }
370        DatabasePrivilegesDiff::Deleted(_) => {
371            if privilege_row.is_none() {
372                Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
373                    DiffDoesNotApplyError::RowDoesNotExist(
374                        diff.get_database_name().to_owned(),
375                        diff.get_user_name().to_owned(),
376                    ),
377                ))
378            } else {
379                Ok(())
380            }
381        }
382        DatabasePrivilegesDiff::Noop { .. } => {
383            log::warn!(
384                "Server got sent a noop database privilege diff to validate, is the client buggy?"
385            );
386            Ok(())
387        }
388    }
389}
390
391/// Uses the result of [`diff_privileges`] to modify privileges in the database.
392pub async fn apply_privilege_diffs(
393    database_privilege_diffs: BTreeSet<DatabasePrivilegesDiff>,
394    unix_user: &UnixUser,
395    connection: &mut MySqlConnection,
396) -> ModifyPrivilegesResponse {
397    let mut results: BTreeMap<(MySQLDatabase, MySQLUser), _> = BTreeMap::new();
398
399    for diff in database_privilege_diffs {
400        let key = (
401            diff.get_database_name().to_owned(),
402            diff.get_user_name().to_owned(),
403        );
404        if let Err(err) = validate_name(diff.get_database_name()) {
405            results.insert(
406                key,
407                Err(ModifyDatabasePrivilegesError::DatabaseSanitizationError(
408                    err,
409                )),
410            );
411            continue;
412        }
413
414        if let Err(err) = validate_ownership_by_unix_user(diff.get_database_name(), unix_user) {
415            results.insert(
416                key,
417                Err(ModifyDatabasePrivilegesError::DatabaseOwnershipError(err)),
418            );
419            continue;
420        }
421
422        if let Err(err) = validate_name(diff.get_user_name()) {
423            results.insert(
424                key,
425                Err(ModifyDatabasePrivilegesError::UserSanitizationError(err)),
426            );
427            continue;
428        }
429
430        if let Err(err) = validate_ownership_by_unix_user(diff.get_user_name(), unix_user) {
431            results.insert(
432                key,
433                Err(ModifyDatabasePrivilegesError::UserOwnershipError(err)),
434            );
435            continue;
436        }
437
438        if !unsafe_database_exists(diff.get_database_name(), connection)
439            .await
440            .unwrap()
441        {
442            results.insert(
443                key,
444                Err(ModifyDatabasePrivilegesError::DatabaseDoesNotExist),
445            );
446            continue;
447        }
448
449        if let Err(err) = validate_diff(&diff, connection).await {
450            results.insert(key, Err(err));
451            continue;
452        }
453
454        let result = unsafe_apply_privilege_diff(&diff, connection)
455            .await
456            .map_err(|e| ModifyDatabasePrivilegesError::MySqlError(e.to_string()));
457
458        results.insert(key, result);
459    }
460
461    results
462}