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

            
17
use std::collections::{BTreeMap, BTreeSet};
18

            
19
use indoc::indoc;
20
use itertools::Itertools;
21
use sqlx::{MySqlConnection, mysql::MySqlRow, prelude::*};
22

            
23
use 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]
47
fn 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

            
59
impl 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.
81
async 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.
109
pub 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

            
138
pub 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.
186
pub 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

            
215
async 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

            
307
async 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.
392
pub 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
}