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, ListAllPrivilegesError, ListAllPrivilegesResponse,
32
            ListPrivilegesError, ListPrivilegesResponse, ModifyDatabasePrivilegesError,
33
            ModifyPrivilegesResponse,
34
            request_validation::{GroupDenylist, validate_db_or_user_request},
35
        },
36
        types::{DbOrUser, MySQLDatabase, MySQLUser},
37
    },
38
    server::{
39
        common::{create_user_group_matching_regex, try_get_with_binary_fallback},
40
        sql::{
41
            database_operations::unsafe_database_exists, quote_identifier,
42
            user_operations::unsafe_user_exists,
43
        },
44
    },
45
};
46

            
47
// TODO: get by name instead of row tuple position
48

            
49
#[inline]
50
fn get_mysql_row_priv_field(row: &MySqlRow, position: usize) -> Result<bool, sqlx::Error> {
51
    let field = DATABASE_PRIVILEGE_FIELDS[position];
52
    let value = row.try_get(position)?;
53
    if let Some(val) = rev_yn(value) {
54
        Ok(val)
55
    } else {
56
        tracing::warn!(r#"Invalid value for privilege "{}": '{}'"#, field, value);
57
        Ok(false)
58
    }
59
}
60

            
61
impl FromRow<'_, MySqlRow> for DatabasePrivilegeRow {
62
    fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
63
        Ok(Self {
64
            db: try_get_with_binary_fallback(row, "Db")?.into(),
65
            user: try_get_with_binary_fallback(row, "User")?.into(),
66
            select_priv: get_mysql_row_priv_field(row, 2)?,
67
            insert_priv: get_mysql_row_priv_field(row, 3)?,
68
            update_priv: get_mysql_row_priv_field(row, 4)?,
69
            delete_priv: get_mysql_row_priv_field(row, 5)?,
70
            create_priv: get_mysql_row_priv_field(row, 6)?,
71
            drop_priv: get_mysql_row_priv_field(row, 7)?,
72
            alter_priv: get_mysql_row_priv_field(row, 8)?,
73
            index_priv: get_mysql_row_priv_field(row, 9)?,
74
            create_tmp_table_priv: get_mysql_row_priv_field(row, 10)?,
75
            lock_tables_priv: get_mysql_row_priv_field(row, 11)?,
76
            references_priv: get_mysql_row_priv_field(row, 12)?,
77
        })
78
    }
79
}
80

            
81
// NOTE: this function is unsafe because it does no input validation.
82
/// Get all users + privileges for a single database.
83
async fn unsafe_get_database_privileges(
84
    database_name: &str,
85
    connection: &mut MySqlConnection,
86
) -> Result<Vec<DatabasePrivilegeRow>, sqlx::Error> {
87
    let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
88
        "SELECT {} FROM `db` WHERE `Db` = ?",
89
        DATABASE_PRIVILEGE_FIELDS
90
            .iter()
91
            .map(|field| quote_identifier(field))
92
            .join(","),
93
    ))
94
    .bind(database_name)
95
    .fetch_all(connection)
96
    .await;
97

            
98
    if let Err(e) = &result {
99
        tracing::error!(
100
            "Failed to get database privileges for '{}': {}",
101
            &database_name,
102
            e
103
        );
104
    }
105

            
106
    result
107
}
108

            
109
// NOTE: this function is unsafe because it does no input validation.
110
/// Get all users + privileges for a single database-user pair.
111
pub async fn unsafe_get_database_privileges_for_db_user_pair(
112
    database_name: &MySQLDatabase,
113
    user_name: &MySQLUser,
114
    connection: &mut MySqlConnection,
115
) -> Result<Option<DatabasePrivilegeRow>, sqlx::Error> {
116
    let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
117
        "SELECT {} FROM `db` WHERE `Db` = ? AND `User` = ?",
118
        DATABASE_PRIVILEGE_FIELDS
119
            .iter()
120
            .map(|field| quote_identifier(field))
121
            .join(","),
122
    ))
123
    .bind(database_name.as_str())
124
    .bind(user_name.as_str())
125
    .fetch_optional(connection)
126
    .await;
127

            
128
    if let Err(e) = &result {
129
        tracing::error!(
130
            "Failed to get database privileges for '{}.{}': {}",
131
            &database_name,
132
            &user_name,
133
            e
134
        );
135
    }
136

            
137
    result
138
}
139

            
140
pub async fn get_databases_privilege_data(
141
    database_names: &[MySQLDatabase],
142
    unix_user: &UnixUser,
143
    connection: &mut MySqlConnection,
144
    _db_is_mariadb: bool,
145
    group_denylist: &GroupDenylist,
146
) -> ListPrivilegesResponse {
147
    let mut results = BTreeMap::new();
148

            
149
    for database_name in database_names.iter().cloned() {
150
        if let Err(err) = validate_db_or_user_request(
151
            &DbOrUser::Database(database_name.to_owned()),
152
            unix_user,
153
            group_denylist,
154
        )
155
        .map_err(ListPrivilegesError::ValidationError)
156
        {
157
            results.insert(database_name, Err(err));
158
            continue;
159
        }
160

            
161
        match unsafe_database_exists(&database_name, connection).await {
162
            Ok(false) => {
163
                results.insert(
164
                    database_name.to_owned(),
165
                    Err(ListPrivilegesError::DatabaseDoesNotExist),
166
                );
167
                continue;
168
            }
169
            Err(e) => {
170
                results.insert(
171
                    database_name.to_owned(),
172
                    Err(ListPrivilegesError::MySqlError(e.to_string())),
173
                );
174
                continue;
175
            }
176
            Ok(true) => {}
177
        }
178

            
179
        let result = unsafe_get_database_privileges(&database_name, connection)
180
            .await
181
            .map_err(|e| ListPrivilegesError::MySqlError(e.to_string()));
182

            
183
        results.insert(database_name.to_owned(), result);
184
    }
185

            
186
    debug_assert!(database_names.len() == results.len());
187

            
188
    results
189
}
190

            
191
/// TODO: make this constant
192
fn get_all_db_privs_query() -> String {
193
    format!(
194
        indoc! {r"
195
            SELECT {} FROM `db` WHERE `db` IN
196
            (SELECT DISTINCT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database`
197
              FROM `information_schema`.`SCHEMATA`
198
              WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
199
                AND `SCHEMA_NAME` REGEXP ?)
200
        "},
201
        DATABASE_PRIVILEGE_FIELDS
202
            .iter()
203
            .map(|field| quote_identifier(field))
204
            .join(","),
205
    )
206
}
207

            
208
/// Get all database + user + privileges pairs that are owned by the current user.
209
pub async fn get_all_database_privileges(
210
    unix_user: &UnixUser,
211
    connection: &mut MySqlConnection,
212
    _db_is_mariadb: bool,
213
    group_denylist: &GroupDenylist,
214
) -> ListAllPrivilegesResponse {
215
    let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&get_all_db_privs_query())
216
        .bind(create_user_group_matching_regex(unix_user, group_denylist))
217
        .fetch_all(connection)
218
        .await
219
        .map_err(|e| ListAllPrivilegesError::MySqlError(e.to_string()));
220

            
221
    if let Err(e) = &result {
222
        tracing::error!("Failed to get all database privileges: {:?}", e);
223
    }
224

            
225
    result
226
}
227

            
228
// TODO: make these queries constant strings.
229
async fn unsafe_apply_privilege_diff(
230
    database_privilege_diff: &DatabasePrivilegesDiff,
231
    connection: &mut MySqlConnection,
232
) -> Result<(), sqlx::Error> {
233
    let result = match database_privilege_diff {
234
        DatabasePrivilegesDiff::New(p) => {
235
            let tables = DATABASE_PRIVILEGE_FIELDS
236
                .iter()
237
                .map(|field| quote_identifier(field))
238
                .join(",");
239

            
240
            let question_marks =
241
                std::iter::repeat_n("?", DATABASE_PRIVILEGE_FIELDS.len()).join(",");
242

            
243
            sqlx::query(format!("INSERT INTO `db` ({tables}) VALUES ({question_marks})").as_str())
244
                .bind(p.db.to_string())
245
                .bind(p.user.to_string())
246
                .bind(yn(p.select_priv))
247
                .bind(yn(p.insert_priv))
248
                .bind(yn(p.update_priv))
249
                .bind(yn(p.delete_priv))
250
                .bind(yn(p.create_priv))
251
                .bind(yn(p.drop_priv))
252
                .bind(yn(p.alter_priv))
253
                .bind(yn(p.index_priv))
254
                .bind(yn(p.create_tmp_table_priv))
255
                .bind(yn(p.lock_tables_priv))
256
                .bind(yn(p.references_priv))
257
                .execute(connection)
258
                .await
259
                .map(|_| ())
260
        }
261
        DatabasePrivilegesDiff::Modified(p) => {
262
            let changes = DATABASE_PRIVILEGE_FIELDS
263
                .iter()
264
                .skip(2) // Skip Db and User fields
265
                .map(|field| {
266
                    format!(
267
                        "{} = COALESCE(?, {})",
268
                        quote_identifier(field),
269
                        quote_identifier(field)
270
                    )
271
                })
272
                .join(",");
273

            
274
            fn change_to_yn(change: DatabasePrivilegeChange) -> &'static str {
275
                match change {
276
                    DatabasePrivilegeChange::YesToNo => "N",
277
                    DatabasePrivilegeChange::NoToYes => "Y",
278
                }
279
            }
280

            
281
            sqlx::query(format!("UPDATE `db` SET {changes} WHERE `Db` = ? AND `User` = ?").as_str())
282
                .bind(p.select_priv.map(change_to_yn))
283
                .bind(p.insert_priv.map(change_to_yn))
284
                .bind(p.update_priv.map(change_to_yn))
285
                .bind(p.delete_priv.map(change_to_yn))
286
                .bind(p.create_priv.map(change_to_yn))
287
                .bind(p.drop_priv.map(change_to_yn))
288
                .bind(p.alter_priv.map(change_to_yn))
289
                .bind(p.index_priv.map(change_to_yn))
290
                .bind(p.create_tmp_table_priv.map(change_to_yn))
291
                .bind(p.lock_tables_priv.map(change_to_yn))
292
                .bind(p.references_priv.map(change_to_yn))
293
                .bind(p.db.to_string())
294
                .bind(p.user.to_string())
295
                .execute(connection)
296
                .await
297
                .map(|_| ())
298
        }
299
        DatabasePrivilegesDiff::Deleted(p) => {
300
            sqlx::query("DELETE FROM `db` WHERE `Db` = ? AND `User` = ?")
301
                .bind(p.db.to_string())
302
                .bind(p.user.to_string())
303
                .execute(connection)
304
                .await
305
                .map(|_| ())
306
        }
307
        DatabasePrivilegesDiff::Noop { .. } => Ok(()),
308
    };
309

            
310
    if let Err(e) = &result {
311
        tracing::error!("Failed to apply database privilege diff: {}", e);
312
    }
313

            
314
    result
315
}
316

            
317
async fn validate_diff(
318
    diff: &DatabasePrivilegesDiff,
319
    connection: &mut MySqlConnection,
320
) -> Result<(), ModifyDatabasePrivilegesError> {
321
    let privilege_row = unsafe_get_database_privileges_for_db_user_pair(
322
        diff.get_database_name(),
323
        diff.get_user_name(),
324
        connection,
325
    )
326
    .await;
327

            
328
    let privilege_row = match privilege_row {
329
        Ok(privilege_row) => privilege_row,
330
        Err(e) => return Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
331
    };
332

            
333
    match diff {
334
        DatabasePrivilegesDiff::New(_) => {
335
            if privilege_row.is_some() {
336
                Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
337
                    DiffDoesNotApplyError::RowAlreadyExists(
338
                        diff.get_database_name().to_owned(),
339
                        diff.get_user_name().to_owned(),
340
                    ),
341
                ))
342
            } else {
343
                Ok(())
344
            }
345
        }
346
        DatabasePrivilegesDiff::Modified(_) if privilege_row.is_none() => {
347
            Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
348
                DiffDoesNotApplyError::RowDoesNotExist(
349
                    diff.get_database_name().to_owned(),
350
                    diff.get_user_name().to_owned(),
351
                ),
352
            ))
353
        }
354
        DatabasePrivilegesDiff::Modified(row_diff) => {
355
            let row = privilege_row.unwrap();
356

            
357
            let error_exists = DATABASE_PRIVILEGE_FIELDS
358
                .iter()
359
                .skip(2) // Skip Db and User fields
360
                .any(
361
                    |field| match row_diff.get_privilege_change_by_name(field).unwrap() {
362
                        Some(DatabasePrivilegeChange::YesToNo) => {
363
                            !row.get_privilege_by_name(field).unwrap()
364
                        }
365
                        Some(DatabasePrivilegeChange::NoToYes) => {
366
                            row.get_privilege_by_name(field).unwrap()
367
                        }
368
                        None => false,
369
                    },
370
                );
371

            
372
            if error_exists {
373
                Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
374
                    DiffDoesNotApplyError::RowPrivilegeChangeDoesNotApply(row_diff.to_owned(), row),
375
                ))
376
            } else {
377
                Ok(())
378
            }
379
        }
380
        DatabasePrivilegesDiff::Deleted(_) => {
381
            if privilege_row.is_none() {
382
                Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
383
                    DiffDoesNotApplyError::RowDoesNotExist(
384
                        diff.get_database_name().to_owned(),
385
                        diff.get_user_name().to_owned(),
386
                    ),
387
                ))
388
            } else {
389
                Ok(())
390
            }
391
        }
392
        DatabasePrivilegesDiff::Noop { .. } => {
393
            tracing::warn!(
394
                "Server got sent a noop database privilege diff to validate, is the client buggy?"
395
            );
396
            Ok(())
397
        }
398
    }
399
}
400

            
401
/// Uses the result of [`diff_privileges`] to modify privileges in the database.
402
pub async fn apply_privilege_diffs(
403
    database_privilege_diffs: &BTreeSet<DatabasePrivilegesDiff>,
404
    unix_user: &UnixUser,
405
    connection: &mut MySqlConnection,
406
    _db_is_mariadb: bool,
407
    group_denylist: &GroupDenylist,
408
) -> ModifyPrivilegesResponse {
409
    let mut results: BTreeMap<(MySQLDatabase, MySQLUser), _> = BTreeMap::new();
410

            
411
    for diff in database_privilege_diffs {
412
        let key = (
413
            diff.get_database_name().to_owned(),
414
            diff.get_user_name().to_owned(),
415
        );
416
        if let Err(err) = validate_db_or_user_request(
417
            &DbOrUser::Database(diff.get_database_name().to_owned()),
418
            unix_user,
419
            group_denylist,
420
        )
421
        .map_err(ModifyDatabasePrivilegesError::UserValidationError)
422
        {
423
            results.insert(key, Err(err));
424
            continue;
425
        }
426

            
427
        if let Err(err) = validate_db_or_user_request(
428
            &DbOrUser::User(diff.get_user_name().to_owned()),
429
            unix_user,
430
            group_denylist,
431
        )
432
        .map_err(ModifyDatabasePrivilegesError::UserValidationError)
433
        {
434
            results.insert(key, Err(err));
435
            continue;
436
        }
437

            
438
        match unsafe_database_exists(diff.get_database_name(), connection).await {
439
            Ok(false) => {
440
                results.insert(
441
                    key,
442
                    Err(ModifyDatabasePrivilegesError::DatabaseDoesNotExist),
443
                );
444
                continue;
445
            }
446
            Err(e) => {
447
                results.insert(
448
                    key,
449
                    Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
450
                );
451
                continue;
452
            }
453
            Ok(true) => {}
454
        }
455

            
456
        match unsafe_user_exists(diff.get_user_name(), connection).await {
457
            Ok(false) => {
458
                results.insert(key, Err(ModifyDatabasePrivilegesError::UserDoesNotExist));
459
                continue;
460
            }
461
            Err(e) => {
462
                results.insert(
463
                    key,
464
                    Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
465
                );
466
                continue;
467
            }
468
            Ok(true) => {}
469
        }
470

            
471
        if let Err(err) = validate_diff(diff, connection).await {
472
            results.insert(key, Err(err));
473
            continue;
474
        }
475

            
476
        let result = unsafe_apply_privilege_diff(diff, connection)
477
            .await
478
            .map_err(|e| ModifyDatabasePrivilegesError::MySqlError(e.to_string()));
479

            
480
        results.insert(key, result);
481
    }
482

            
483
    results
484
}