1
//! This module contains datastructures and logic for comparing database privileges,
2
//! generating, validating and reducing diffs between two sets of database privileges.
3

            
4
use super::base::{DatabasePrivilegeRow, db_priv_field_human_readable_name};
5
use crate::core::types::{MySQLDatabase, MySQLUser};
6
use prettytable::Table;
7
use serde::{Deserialize, Serialize};
8
use std::{
9
    collections::{BTreeSet, HashMap, hash_map::Entry},
10
    fmt,
11
};
12

            
13
/// This enum represents a change for a single privilege.
14
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
15
pub enum DatabasePrivilegeChange {
16
    YesToNo,
17
    NoToYes,
18
}
19

            
20
impl DatabasePrivilegeChange {
21
    #[must_use]
22
32
    pub fn new(p1: bool, p2: bool) -> Option<DatabasePrivilegeChange> {
23
32
        match (p1, p2) {
24
4
            (true, false) => Some(DatabasePrivilegeChange::YesToNo),
25
3
            (false, true) => Some(DatabasePrivilegeChange::NoToYes),
26
25
            _ => None,
27
        }
28
32
    }
29
}
30

            
31
/// This struct encapsulates the before and after states of the
32
/// access privileges for a single user on a single database.
33
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Default)]
34
pub struct DatabasePrivilegeRowDiff {
35
    // TODO: don't store the db and user here, let the type be stored in a mapping
36
    pub db: MySQLDatabase,
37
    pub user: MySQLUser,
38
    pub select_priv: Option<DatabasePrivilegeChange>,
39
    pub insert_priv: Option<DatabasePrivilegeChange>,
40
    pub update_priv: Option<DatabasePrivilegeChange>,
41
    pub delete_priv: Option<DatabasePrivilegeChange>,
42
    pub create_priv: Option<DatabasePrivilegeChange>,
43
    pub drop_priv: Option<DatabasePrivilegeChange>,
44
    pub alter_priv: Option<DatabasePrivilegeChange>,
45
    pub index_priv: Option<DatabasePrivilegeChange>,
46
    pub create_tmp_table_priv: Option<DatabasePrivilegeChange>,
47
    pub lock_tables_priv: Option<DatabasePrivilegeChange>,
48
    pub references_priv: Option<DatabasePrivilegeChange>,
49
    pub create_view_priv: Option<DatabasePrivilegeChange>,
50
    pub show_view_priv: Option<DatabasePrivilegeChange>,
51
    pub trigger_priv: Option<DatabasePrivilegeChange>,
52
}
53

            
54
impl DatabasePrivilegeRowDiff {
55
    /// Calculates the difference between two [`DatabasePrivilegeRow`] instances.
56
    #[must_use]
57
2
    pub fn from_rows(
58
2
        row1: &DatabasePrivilegeRow,
59
2
        row2: &DatabasePrivilegeRow,
60
2
    ) -> DatabasePrivilegeRowDiff {
61
2
        debug_assert!(row1.db == row2.db && row1.user == row2.user);
62

            
63
2
        DatabasePrivilegeRowDiff {
64
2
            db: row1.db.clone(),
65
2
            user: row1.user.clone(),
66
2
            select_priv: DatabasePrivilegeChange::new(row1.select_priv, row2.select_priv),
67
2
            insert_priv: DatabasePrivilegeChange::new(row1.insert_priv, row2.insert_priv),
68
2
            update_priv: DatabasePrivilegeChange::new(row1.update_priv, row2.update_priv),
69
2
            delete_priv: DatabasePrivilegeChange::new(row1.delete_priv, row2.delete_priv),
70
2
            create_priv: DatabasePrivilegeChange::new(row1.create_priv, row2.create_priv),
71
2
            drop_priv: DatabasePrivilegeChange::new(row1.drop_priv, row2.drop_priv),
72
2
            alter_priv: DatabasePrivilegeChange::new(row1.alter_priv, row2.alter_priv),
73
2
            index_priv: DatabasePrivilegeChange::new(row1.index_priv, row2.index_priv),
74
2
            create_tmp_table_priv: DatabasePrivilegeChange::new(
75
2
                row1.create_tmp_table_priv,
76
2
                row2.create_tmp_table_priv,
77
2
            ),
78
2
            lock_tables_priv: DatabasePrivilegeChange::new(
79
2
                row1.lock_tables_priv,
80
2
                row2.lock_tables_priv,
81
2
            ),
82
2
            references_priv: DatabasePrivilegeChange::new(
83
2
                row1.references_priv,
84
2
                row2.references_priv,
85
2
            ),
86
2
            create_view_priv: DatabasePrivilegeChange::new(
87
2
                row1.create_view_priv,
88
2
                row2.create_view_priv,
89
2
            ),
90
2
            show_view_priv: DatabasePrivilegeChange::new(row1.show_view_priv, row2.show_view_priv),
91
2
            trigger_priv: DatabasePrivilegeChange::new(row1.trigger_priv, row2.trigger_priv),
92
2
        }
93
2
    }
94

            
95
    /// Returns true if there are no changes in this diff.
96
    #[must_use]
97
3
    pub fn is_empty(&self) -> bool {
98
3
        self.select_priv.is_none()
99
1
            && self.insert_priv.is_none()
100
1
            && self.update_priv.is_none()
101
1
            && self.delete_priv.is_none()
102
1
            && self.create_priv.is_none()
103
1
            && self.drop_priv.is_none()
104
1
            && self.alter_priv.is_none()
105
1
            && self.index_priv.is_none()
106
1
            && self.create_tmp_table_priv.is_none()
107
1
            && self.lock_tables_priv.is_none()
108
1
            && self.references_priv.is_none()
109
1
            && self.create_view_priv.is_none()
110
1
            && self.show_view_priv.is_none()
111
1
            && self.trigger_priv.is_none()
112
3
    }
113

            
114
    /// Retrieves the privilege change for a given privilege name.
115
    pub fn get_privilege_change_by_name(
116
        &self,
117
        privilege_name: &str,
118
    ) -> anyhow::Result<Option<DatabasePrivilegeChange>> {
119
        match privilege_name {
120
            "select_priv" => Ok(self.select_priv),
121
            "insert_priv" => Ok(self.insert_priv),
122
            "update_priv" => Ok(self.update_priv),
123
            "delete_priv" => Ok(self.delete_priv),
124
            "create_priv" => Ok(self.create_priv),
125
            "drop_priv" => Ok(self.drop_priv),
126
            "alter_priv" => Ok(self.alter_priv),
127
            "index_priv" => Ok(self.index_priv),
128
            "create_tmp_table_priv" => Ok(self.create_tmp_table_priv),
129
            "lock_tables_priv" => Ok(self.lock_tables_priv),
130
            "references_priv" => Ok(self.references_priv),
131
            "create_view_priv" => Ok(self.create_view_priv),
132
            "show_view_priv" => Ok(self.show_view_priv),
133
            "trigger_priv" => Ok(self.trigger_priv),
134
            _ => anyhow::bail!("Unknown privilege name: {privilege_name}"),
135
        }
136
    }
137

            
138
    /// Merges another diff into this one, combining them in a sequential manner.
139
    fn mappend(&mut self, other: &DatabasePrivilegeRowDiff) {
140
        debug_assert!(self.db == other.db && self.user == other.user);
141

            
142
        if other.select_priv.is_some() {
143
            self.select_priv = other.select_priv;
144
        }
145
        if other.insert_priv.is_some() {
146
            self.insert_priv = other.insert_priv;
147
        }
148
        if other.update_priv.is_some() {
149
            self.update_priv = other.update_priv;
150
        }
151
        if other.delete_priv.is_some() {
152
            self.delete_priv = other.delete_priv;
153
        }
154
        if other.create_priv.is_some() {
155
            self.create_priv = other.create_priv;
156
        }
157
        if other.drop_priv.is_some() {
158
            self.drop_priv = other.drop_priv;
159
        }
160
        if other.alter_priv.is_some() {
161
            self.alter_priv = other.alter_priv;
162
        }
163
        if other.index_priv.is_some() {
164
            self.index_priv = other.index_priv;
165
        }
166
        if other.create_tmp_table_priv.is_some() {
167
            self.create_tmp_table_priv = other.create_tmp_table_priv;
168
        }
169
        if other.lock_tables_priv.is_some() {
170
            self.lock_tables_priv = other.lock_tables_priv;
171
        }
172
        if other.references_priv.is_some() {
173
            self.references_priv = other.references_priv;
174
        }
175
        if other.create_view_priv.is_some() {
176
            self.create_view_priv = other.create_view_priv;
177
        }
178
        if other.show_view_priv.is_some() {
179
            self.show_view_priv = other.show_view_priv;
180
        }
181
        if other.trigger_priv.is_some() {
182
            self.trigger_priv = other.trigger_priv;
183
        }
184
    }
185

            
186
    /// Removes any no-op changes from the diff, based on the original privilege row.
187
    fn remove_noops(&mut self, from: &DatabasePrivilegeRow) {
188
        fn new_value(
189
            change: Option<&DatabasePrivilegeChange>,
190
            from_value: bool,
191
        ) -> Option<DatabasePrivilegeChange> {
192
            change.as_ref().and_then(|c| match c {
193
                DatabasePrivilegeChange::YesToNo if from_value => {
194
                    Some(DatabasePrivilegeChange::YesToNo)
195
                }
196
                DatabasePrivilegeChange::NoToYes if !from_value => {
197
                    Some(DatabasePrivilegeChange::NoToYes)
198
                }
199
                _ => None,
200
            })
201
        }
202

            
203
        self.select_priv = new_value(self.select_priv.as_ref(), from.select_priv);
204
        self.insert_priv = new_value(self.insert_priv.as_ref(), from.insert_priv);
205
        self.update_priv = new_value(self.update_priv.as_ref(), from.update_priv);
206
        self.delete_priv = new_value(self.delete_priv.as_ref(), from.delete_priv);
207
        self.create_priv = new_value(self.create_priv.as_ref(), from.create_priv);
208
        self.drop_priv = new_value(self.drop_priv.as_ref(), from.drop_priv);
209
        self.alter_priv = new_value(self.alter_priv.as_ref(), from.alter_priv);
210
        self.index_priv = new_value(self.index_priv.as_ref(), from.index_priv);
211
        self.create_tmp_table_priv = new_value(
212
            self.create_tmp_table_priv.as_ref(),
213
            from.create_tmp_table_priv,
214
        );
215
        self.lock_tables_priv = new_value(self.lock_tables_priv.as_ref(), from.lock_tables_priv);
216
        self.references_priv = new_value(self.references_priv.as_ref(), from.references_priv);
217
        self.create_view_priv = new_value(self.create_view_priv.as_ref(), from.create_view_priv);
218
        self.show_view_priv = new_value(self.show_view_priv.as_ref(), from.show_view_priv);
219
        self.trigger_priv = new_value(self.trigger_priv.as_ref(), from.trigger_priv);
220
    }
221

            
222
    fn apply(&self, base: &mut DatabasePrivilegeRow) {
223
        fn apply_change(change: Option<&DatabasePrivilegeChange>, target: &mut bool) {
224
            match change {
225
                Some(DatabasePrivilegeChange::YesToNo) => *target = false,
226
                Some(DatabasePrivilegeChange::NoToYes) => *target = true,
227
                None => {}
228
            }
229
        }
230

            
231
        apply_change(self.select_priv.as_ref(), &mut base.select_priv);
232
        apply_change(self.insert_priv.as_ref(), &mut base.insert_priv);
233
        apply_change(self.update_priv.as_ref(), &mut base.update_priv);
234
        apply_change(self.delete_priv.as_ref(), &mut base.delete_priv);
235
        apply_change(self.create_priv.as_ref(), &mut base.create_priv);
236
        apply_change(self.drop_priv.as_ref(), &mut base.drop_priv);
237
        apply_change(self.alter_priv.as_ref(), &mut base.alter_priv);
238
        apply_change(self.index_priv.as_ref(), &mut base.index_priv);
239
        apply_change(
240
            self.create_tmp_table_priv.as_ref(),
241
            &mut base.create_tmp_table_priv,
242
        );
243
        apply_change(self.lock_tables_priv.as_ref(), &mut base.lock_tables_priv);
244
        apply_change(self.references_priv.as_ref(), &mut base.references_priv);
245
        apply_change(self.create_view_priv.as_ref(), &mut base.create_view_priv);
246
        apply_change(self.show_view_priv.as_ref(), &mut base.show_view_priv);
247
        apply_change(self.trigger_priv.as_ref(), &mut base.trigger_priv);
248
    }
249
}
250

            
251
impl fmt::Display for DatabasePrivilegeRowDiff {
252
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253
        fn format_change(
254
            f: &mut fmt::Formatter<'_>,
255
            change: Option<DatabasePrivilegeChange>,
256
            field_name: &str,
257
        ) -> fmt::Result {
258
            if let Some(change) = change {
259
                match change {
260
                    DatabasePrivilegeChange::YesToNo => f.write_fmt(format_args!(
261
                        "{}: Y -> N\n",
262
                        db_priv_field_human_readable_name(field_name)
263
                    )),
264
                    DatabasePrivilegeChange::NoToYes => f.write_fmt(format_args!(
265
                        "{}: N -> Y\n",
266
                        db_priv_field_human_readable_name(field_name)
267
                    )),
268
                }
269
            } else {
270
                Ok(())
271
            }
272
        }
273

            
274
        format_change(f, self.select_priv, "select_priv")?;
275
        format_change(f, self.insert_priv, "insert_priv")?;
276
        format_change(f, self.update_priv, "update_priv")?;
277
        format_change(f, self.delete_priv, "delete_priv")?;
278
        format_change(f, self.create_priv, "create_priv")?;
279
        format_change(f, self.drop_priv, "drop_priv")?;
280
        format_change(f, self.alter_priv, "alter_priv")?;
281
        format_change(f, self.index_priv, "index_priv")?;
282
        format_change(f, self.create_tmp_table_priv, "create_tmp_table_priv")?;
283
        format_change(f, self.lock_tables_priv, "lock_tables_priv")?;
284
        format_change(f, self.references_priv, "references_priv")?;
285
        format_change(f, self.create_view_priv, "create_view_priv")?;
286
        format_change(f, self.show_view_priv, "show_view_priv")?;
287
        format_change(f, self.trigger_priv, "trigger_priv")?;
288

            
289
        Ok(())
290
    }
291
}
292

            
293
/// This enum encapsulates whether a [`DatabasePrivilegeRow`] was introduced, modified or deleted.
294
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
295
pub enum DatabasePrivilegesDiff {
296
    New(DatabasePrivilegeRow),
297
    Modified(DatabasePrivilegeRowDiff),
298
    Deleted(DatabasePrivilegeRow),
299
    Noop { db: MySQLDatabase, user: MySQLUser },
300
}
301

            
302
impl DatabasePrivilegesDiff {
303
    #[must_use]
304
    pub fn get_database_name(&self) -> &MySQLDatabase {
305
        match self {
306
            DatabasePrivilegesDiff::New(p) => &p.db,
307
            DatabasePrivilegesDiff::Modified(p) => &p.db,
308
            DatabasePrivilegesDiff::Deleted(p) => &p.db,
309
            DatabasePrivilegesDiff::Noop { db, .. } => db,
310
        }
311
    }
312

            
313
    #[must_use]
314
    pub fn get_user_name(&self) -> &MySQLUser {
315
        match self {
316
            DatabasePrivilegesDiff::New(p) => &p.user,
317
            DatabasePrivilegesDiff::Modified(p) => &p.user,
318
            DatabasePrivilegesDiff::Deleted(p) => &p.user,
319
            DatabasePrivilegesDiff::Noop { user, .. } => user,
320
        }
321
    }
322

            
323
    /// Merges another [`DatabasePrivilegesDiff`] into this one, combining them in a sequential manner.
324
    /// For example, if this diff represents a creation and the other represents a modification,
325
    /// the result will be a creation with the modifications applied.
326
    pub fn mappend(&mut self, other: &DatabasePrivilegesDiff) -> anyhow::Result<()> {
327
        debug_assert!(
328
            self.get_database_name() == other.get_database_name()
329
                && self.get_user_name() == other.get_user_name()
330
        );
331

            
332
        if matches!(self, DatabasePrivilegesDiff::Deleted(_))
333
            && (matches!(other, DatabasePrivilegesDiff::Modified(_)))
334
        {
335
            anyhow::bail!("Cannot modify a deleted database privilege row");
336
        }
337

            
338
        if matches!(self, DatabasePrivilegesDiff::New(_))
339
            && (matches!(other, DatabasePrivilegesDiff::New(_)))
340
        {
341
            anyhow::bail!("Cannot create an already existing database privilege row");
342
        }
343

            
344
        if matches!(self, DatabasePrivilegesDiff::Modified(_))
345
            && (matches!(other, DatabasePrivilegesDiff::New(_)))
346
        {
347
            anyhow::bail!("Cannot create an already existing database privilege row");
348
        }
349

            
350
        if matches!(self, DatabasePrivilegesDiff::Noop { .. }) {
351
            other.clone_into(self);
352
            return Ok(());
353
        } else if matches!(other, DatabasePrivilegesDiff::Noop { .. }) {
354
            return Ok(());
355
        }
356

            
357
        match (&self, other) {
358
            (DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Modified(modified)) => {
359
                let inner_row = match self {
360
                    DatabasePrivilegesDiff::New(r) => r,
361
                    _ => unreachable!(),
362
                };
363
                modified.apply(inner_row);
364
            }
365
            (DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Modified(modified)) => {
366
                let inner_diff = match self {
367
                    DatabasePrivilegesDiff::Modified(r) => r,
368
                    _ => unreachable!(),
369
                };
370
                inner_diff.mappend(modified);
371

            
372
                if inner_diff.is_empty() {
373
                    let db = inner_diff.db.clone();
374
                    let user = inner_diff.user.clone();
375
                    *self = DatabasePrivilegesDiff::Noop { db, user };
376
                }
377
            }
378
            (DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Deleted(deleted)) => {
379
                *self = DatabasePrivilegesDiff::Deleted(deleted.to_owned());
380
            }
381
            (DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Deleted(_)) => {
382
                let db = self.get_database_name().to_owned();
383
                let user = self.get_user_name().to_owned();
384
                *self = DatabasePrivilegesDiff::Noop { db, user };
385
            }
386
            _ => {}
387
        }
388

            
389
        Ok(())
390
    }
391
}
392

            
393
pub type DatabasePrivilegeState<'a> = &'a [DatabasePrivilegeRow];
394

            
395
/// This function calculates the differences between two sets of database privileges.
396
/// It returns a set of [`DatabasePrivilegesDiff`] that can be used to display or
397
/// apply a set of privilege modifications to the database.
398
#[must_use]
399
1
pub fn diff_privileges(
400
1
    from: DatabasePrivilegeState<'_>,
401
1
    to: &[DatabasePrivilegeRow],
402
1
) -> BTreeSet<DatabasePrivilegesDiff> {
403
1
    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
404
1
        .iter()
405
1
        .cloned()
406
2
        .map(|p| ((p.db.clone(), p.user.clone()), p))
407
1
        .collect();
408

            
409
1
    let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = to
410
1
        .iter()
411
1
        .cloned()
412
2
        .map(|p| ((p.db.clone(), p.user.clone()), p))
413
1
        .collect();
414

            
415
1
    let mut result = BTreeSet::new();
416

            
417
2
    for p in to {
418
2
        if let Some(old_p) = from_lookup_table.get(&(p.db.clone(), p.user.clone())) {
419
1
            let diff = DatabasePrivilegeRowDiff::from_rows(old_p, p);
420
1
            if !diff.is_empty() {
421
1
                result.insert(DatabasePrivilegesDiff::Modified(diff));
422
1
            }
423
1
        } else {
424
1
            result.insert(DatabasePrivilegesDiff::New(p.to_owned()));
425
1
        }
426
    }
427

            
428
2
    for p in from {
429
2
        if !to_lookup_table.contains_key(&(p.db.clone(), p.user.clone())) {
430
1
            result.insert(DatabasePrivilegesDiff::Deleted(p.to_owned()));
431
1
        }
432
    }
433

            
434
1
    result
435
1
}
436

            
437
/// Converts a set of [`DatabasePrivilegeRowDiff`] into a set of [`DatabasePrivilegesDiff`],
438
/// representing either creating new privilege rows, or modifying the existing ones.
439
///
440
/// This is particularly useful for processing CLI arguments.
441
pub fn create_or_modify_privilege_rows(
442
    from: DatabasePrivilegeState<'_>,
443
    to: &BTreeSet<DatabasePrivilegeRowDiff>,
444
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
445
    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
446
        .iter()
447
        .cloned()
448
        .map(|p| ((p.db.clone(), p.user.clone()), p))
449
        .collect();
450

            
451
    let mut result = BTreeSet::new();
452

            
453
    for diff in to {
454
        if let Some(old_p) = from_lookup_table.get(&(diff.db.clone(), diff.user.clone())) {
455
            let mut modified_diff = diff.to_owned();
456
            modified_diff.remove_noops(old_p);
457
            if !modified_diff.is_empty() {
458
                result.insert(DatabasePrivilegesDiff::Modified(modified_diff));
459
            }
460
        } else {
461
            let mut new_row = DatabasePrivilegeRow {
462
                db: diff.db.clone(),
463
                user: diff.user.clone(),
464
                select_priv: false,
465
                insert_priv: false,
466
                update_priv: false,
467
                delete_priv: false,
468
                create_priv: false,
469
                drop_priv: false,
470
                alter_priv: false,
471
                index_priv: false,
472
                create_tmp_table_priv: false,
473
                lock_tables_priv: false,
474
                references_priv: false,
475
                create_view_priv: false,
476
                show_view_priv: false,
477
                trigger_priv: false,
478
            };
479
            diff.apply(&mut new_row);
480
            result.insert(DatabasePrivilegesDiff::New(new_row));
481
        }
482
    }
483

            
484
    Ok(result)
485
}
486

            
487
/// Reduces a set of [`DatabasePrivilegesDiff`] by removing any modifications that would be no-ops.
488
/// For example, if a privilege is changed from Yes to No, but it was already No, that change
489
/// is removed from the diff.
490
///
491
/// The `from` parameter is used to determine the current state of the privileges.
492
/// The `to` parameter is the set of diffs to be reduced.
493
pub fn reduce_privilege_diffs(
494
    from: DatabasePrivilegeState<'_>,
495
    to: BTreeSet<DatabasePrivilegesDiff>,
496
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
497
    let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
498
        .iter()
499
        .cloned()
500
        .map(|p| ((p.db.clone(), p.user.clone()), p))
501
        .collect();
502

            
503
    let mut result: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegesDiff> = from_lookup_table
504
        .iter()
505
        .map(|((db, user), _)| {
506
            (
507
                (db.to_owned(), user.to_owned()),
508
                DatabasePrivilegesDiff::Noop {
509
                    db: db.to_owned(),
510
                    user: user.to_owned(),
511
                },
512
            )
513
        })
514
        .collect();
515

            
516
    for diff in to {
517
        let entry = result.entry((
518
            diff.get_database_name().to_owned(),
519
            diff.get_user_name().to_owned(),
520
        ));
521
        match entry {
522
            Entry::Occupied(mut occupied_entry) => {
523
                let existing_diff = occupied_entry.get_mut();
524
                existing_diff.mappend(&diff)?;
525
            }
526
            Entry::Vacant(vacant_entry) => {
527
                vacant_entry.insert(diff.clone());
528
            }
529
        }
530
    }
531

            
532
    for (key, diff) in &mut result {
533
        if let Some(from_row) = from_lookup_table.get(key)
534
            && let DatabasePrivilegesDiff::Modified(modified_diff) = diff
535
        {
536
            modified_diff.remove_noops(from_row);
537
            if modified_diff.is_empty() {
538
                let db = modified_diff.db.clone();
539
                let user = modified_diff.user.clone();
540
                *diff = DatabasePrivilegesDiff::Noop { db, user };
541
            }
542
        }
543
    }
544

            
545
    Ok(result
546
        .into_values()
547
        .filter(|diff| !matches!(diff, DatabasePrivilegesDiff::Noop { .. }))
548
        .collect::<BTreeSet<DatabasePrivilegesDiff>>())
549
}
550

            
551
/// Renders a set of [`DatabasePrivilegesDiff`] into a human-readable formatted table.
552
#[must_use]
553
pub fn display_privilege_diffs(diffs: &BTreeSet<DatabasePrivilegesDiff>) -> String {
554
    let mut table = Table::new();
555
    table.set_titles(row!["Database", "User", "Privilege diff",]);
556
    for row in diffs {
557
        match row {
558
            DatabasePrivilegesDiff::New(p) => {
559
                table.add_row(row![
560
                    p.db,
561
                    p.user,
562
                    "(Previously unprivileged)\n".to_string() + &p.to_string()
563
                ]);
564
            }
565
            DatabasePrivilegesDiff::Modified(p) => {
566
                table.add_row(row![p.db, p.user, p.to_string(),]);
567
            }
568
            DatabasePrivilegesDiff::Deleted(p) => {
569
                table.add_row(row![p.db, p.user, "Removed".to_string()]);
570
            }
571
            DatabasePrivilegesDiff::Noop { db, user } => {
572
                table.add_row(row![db, user, "No changes".to_string()]);
573
            }
574
        }
575
    }
576

            
577
    table.to_string()
578
}
579

            
580
#[cfg(test)]
581
mod tests {
582
    use super::*;
583

            
584
    #[test]
585
1
    fn test_database_privilege_change_creation() {
586
1
        assert_eq!(
587
1
            DatabasePrivilegeChange::new(true, false),
588
            Some(DatabasePrivilegeChange::YesToNo),
589
        );
590
1
        assert_eq!(
591
1
            DatabasePrivilegeChange::new(false, true),
592
            Some(DatabasePrivilegeChange::NoToYes),
593
        );
594
1
        assert_eq!(DatabasePrivilegeChange::new(true, true), None);
595
1
        assert_eq!(DatabasePrivilegeChange::new(false, false), None);
596
1
    }
597

            
598
    #[test]
599
1
    fn test_database_privilege_row_diff_from_rows() {
600
1
        let row1 = DatabasePrivilegeRow {
601
1
            db: "db".into(),
602
1
            user: "user".into(),
603
1

            
604
1
            select_priv: true,
605
1
            insert_priv: false,
606
1
            update_priv: true,
607
1
            delete_priv: false,
608
1

            
609
1
            create_priv: false,
610
1
            drop_priv: false,
611
1
            alter_priv: false,
612
1
            index_priv: false,
613
1
            create_tmp_table_priv: false,
614
1
            lock_tables_priv: false,
615
1
            references_priv: false,
616
1
            create_view_priv: false,
617
1
            show_view_priv: false,
618
1
            trigger_priv: false,
619
1
        };
620
1
        let row2 = DatabasePrivilegeRow {
621
1
            db: "db".into(),
622
1
            user: "user".into(),
623
1

            
624
1
            select_priv: true,
625
1
            insert_priv: true,
626
1
            update_priv: false,
627
1
            delete_priv: false,
628
1

            
629
1
            create_priv: false,
630
1
            drop_priv: false,
631
1
            alter_priv: false,
632
1
            index_priv: false,
633
1
            create_tmp_table_priv: false,
634
1
            lock_tables_priv: false,
635
1
            references_priv: false,
636
1
            create_view_priv: false,
637
1
            show_view_priv: false,
638
1
            trigger_priv: false,
639
1
        };
640

            
641
1
        let diff = DatabasePrivilegeRowDiff::from_rows(&row1, &row2);
642
1
        assert_eq!(
643
            diff,
644
1
            DatabasePrivilegeRowDiff {
645
1
                db: "db".into(),
646
1
                user: "user".into(),
647
1
                select_priv: None,
648
1
                insert_priv: Some(DatabasePrivilegeChange::NoToYes),
649
1
                update_priv: Some(DatabasePrivilegeChange::YesToNo),
650
1
                delete_priv: None,
651
1
                ..Default::default()
652
1
            },
653
        );
654
1
    }
655

            
656
    #[test]
657
1
    fn test_database_privilege_row_diff_is_empty() {
658
1
        let empty_diff = DatabasePrivilegeRowDiff {
659
1
            db: "db".into(),
660
1
            user: "user".into(),
661
1
            ..Default::default()
662
1
        };
663

            
664
1
        assert!(empty_diff.is_empty());
665

            
666
1
        let non_empty_diff = DatabasePrivilegeRowDiff {
667
1
            db: "db".into(),
668
1
            user: "user".into(),
669
1
            select_priv: Some(DatabasePrivilegeChange::YesToNo),
670
1
            ..Default::default()
671
1
        };
672

            
673
1
        assert!(!non_empty_diff.is_empty());
674
1
    }
675

            
676
    // TODO: test in isolation:
677
    // DatabasePrivilegeRowDiff::mappend
678
    // DatabasePrivilegeRowDiff::remove_noops
679
    // DatabasePrivilegeRowDiff::apply
680
    //
681
    // DatabasePrivilegesDiff::mappend
682
    //
683
    // reduce_privilege_diffs
684

            
685
    #[test]
686
1
    fn test_diff_privileges() {
687
1
        let row_to_be_modified = DatabasePrivilegeRow {
688
1
            db: "db".into(),
689
1
            user: "user".into(),
690
1
            select_priv: true,
691
1
            insert_priv: true,
692
1
            update_priv: true,
693
1
            delete_priv: true,
694
1
            create_priv: true,
695
1
            drop_priv: true,
696
1
            alter_priv: true,
697
1
            index_priv: false,
698
1
            create_tmp_table_priv: true,
699
1
            lock_tables_priv: true,
700
1
            references_priv: false,
701
1
            create_view_priv: false,
702
1
            show_view_priv: false,
703
1
            trigger_priv: false,
704
1
        };
705

            
706
1
        let mut row_to_be_deleted = row_to_be_modified.to_owned();
707
1
        "user2".clone_into(&mut row_to_be_deleted.user);
708

            
709
1
        let from = vec![row_to_be_modified.to_owned(), row_to_be_deleted.to_owned()];
710

            
711
1
        let mut modified_row = row_to_be_modified.to_owned();
712
1
        modified_row.select_priv = false;
713
1
        modified_row.insert_priv = false;
714
1
        modified_row.index_priv = true;
715

            
716
1
        let mut new_row = row_to_be_modified.to_owned();
717
1
        "user3".clone_into(&mut new_row.user);
718

            
719
1
        let to = vec![modified_row.to_owned(), new_row.to_owned()];
720

            
721
1
        let diffs = diff_privileges(&from, &to);
722

            
723
1
        assert_eq!(
724
            diffs,
725
1
            BTreeSet::from_iter(vec![
726
1
                DatabasePrivilegesDiff::Deleted(row_to_be_deleted),
727
1
                DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
728
1
                    db: "db".into(),
729
1
                    user: "user".into(),
730
1
                    select_priv: Some(DatabasePrivilegeChange::YesToNo),
731
1
                    insert_priv: Some(DatabasePrivilegeChange::YesToNo),
732
1
                    index_priv: Some(DatabasePrivilegeChange::NoToYes),
733
1
                    ..Default::default()
734
1
                }),
735
1
                DatabasePrivilegesDiff::New(new_row),
736
            ])
737
        );
738
1
    }
739
}