1
//! This module contains serialization and deserialization logic for
2
//! database privileges related CLI commands.
3

            
4
use itertools::Itertools;
5

            
6
use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff};
7
use crate::core::types::{MySQLDatabase, MySQLUser};
8

            
9
const VALID_PRIVILEGE_EDIT_CHARS: &[char] = &[
10
    's', 'i', 'u', 'd', 'c', 'D', 'a', 'I', 't', 'l', 'r', 'v', 'V', 'T', 'A',
11
];
12

            
13
/// This enum represents a part of a CLI argument for editing database privileges,
14
/// indicating whether privileges are to be added, set, or removed.
15
#[derive(Debug, Clone, PartialEq, Eq)]
16
pub enum DatabasePrivilegeEditEntryType {
17
    Add,
18
    Set,
19
    Remove,
20
}
21

            
22
#[derive(Debug, Clone, PartialEq, Eq)]
23
pub struct DatabasePrivilegeEdit {
24
    pub type_: DatabasePrivilegeEditEntryType,
25
    pub privileges: Vec<char>,
26
}
27

            
28
impl DatabasePrivilegeEdit {
29
6
    pub fn parse_from_str(input: &str) -> anyhow::Result<Self> {
30
6
        let (edit_type, privs_str) = if let Some(privs_str) = input.strip_prefix('+') {
31
1
            (DatabasePrivilegeEditEntryType::Add, privs_str)
32
5
        } else if let Some(privs_str) = input.strip_prefix('-') {
33
1
            (DatabasePrivilegeEditEntryType::Remove, privs_str)
34
        } else {
35
4
            (DatabasePrivilegeEditEntryType::Set, input)
36
        };
37

            
38
6
        let privileges: Vec<char> = privs_str.chars().collect();
39

            
40
6
        if privileges
41
6
            .iter()
42
14
            .any(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c))
43
        {
44
1
            let invalid_chars: String = privileges
45
1
                .iter()
46
1
                .filter(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c))
47
1
                .map(|c| format!("'{c}'"))
48
1
                .join(", ");
49
1
            let valid_characters: String = VALID_PRIVILEGE_EDIT_CHARS
50
1
                .iter()
51
15
                .map(|c| format!("'{c}'"))
52
1
                .join(", ");
53
1
            anyhow::bail!(
54
                "Invalid character(s) in privilege edit entry: {invalid_chars}\n\nValid characters are: {valid_characters}",
55
            );
56
5
        }
57

            
58
5
        Ok(DatabasePrivilegeEdit {
59
5
            type_: edit_type,
60
5
            privileges,
61
5
        })
62
6
    }
63
}
64

            
65
impl std::fmt::Display for DatabasePrivilegeEdit {
66
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67
        match self.type_ {
68
            DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
69
            DatabasePrivilegeEditEntryType::Set => {}
70
            DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
71
        }
72
        for priv_char in &self.privileges {
73
            write!(f, "{priv_char}")?;
74
        }
75

            
76
        Ok(())
77
    }
78
}
79

            
80
/// This struct represents a single CLI argument for editing database privileges.
81
///
82
/// This is typically parsed from a string looking like:
83
///
84
///   `database_name:username:[+|-]privileges`
85
#[derive(Debug, Clone, PartialEq, Eq)]
86
pub struct DatabasePrivilegeEditEntry {
87
    pub database: MySQLDatabase,
88
    pub user: MySQLUser,
89
    pub privilege_edit: DatabasePrivilegeEdit,
90
}
91

            
92
impl DatabasePrivilegeEditEntry {
93
    /// Parses a privilege edit entry from a string.
94
    ///
95
    /// The expected format is:
96
    ///
97
    ///   `database_name:username:[+|-]privileges`
98
    ///
99
    /// where:
100
    /// - `database_name` is the name of the database to edit privileges for
101
    /// - username is the name of the user to edit privileges for
102
    /// - privileges is a string of characters representing the privileges to add, set or remove
103
    /// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly
104
    /// - privileges characters are: siudcDaItlrvVTA
105
8
    pub fn parse_from_str(arg: &str) -> anyhow::Result<Self> {
106
8
        let parts: Vec<&str> = arg.split(':').collect();
107
8
        if parts.len() != 3 {
108
            anyhow::bail!("Invalid privilege edit entry format: {arg}");
109
8
        }
110

            
111
8
        let (database, user, user_privs) = (parts[0].to_string(), parts[1].to_string(), parts[2]);
112

            
113
8
        if user.is_empty() {
114
2
            anyhow::bail!("Username cannot be empty in privilege edit entry: {arg}");
115
6
        }
116

            
117
6
        let privilege_edit = DatabasePrivilegeEdit::parse_from_str(user_privs)?;
118

            
119
5
        Ok(DatabasePrivilegeEditEntry {
120
5
            database: MySQLDatabase::from(database),
121
5
            user: MySQLUser::from(user),
122
5
            privilege_edit,
123
5
        })
124
8
    }
125

            
126
    pub fn as_database_privileges_diff(&self) -> anyhow::Result<DatabasePrivilegeRowDiff> {
127
        let mut diff;
128
        match self.privilege_edit.type_ {
129
            DatabasePrivilegeEditEntryType::Set => {
130
                diff = DatabasePrivilegeRowDiff {
131
                    db: self.database.clone(),
132
                    user: self.user.clone(),
133
                    select_priv: Some(DatabasePrivilegeChange::YesToNo),
134
                    insert_priv: Some(DatabasePrivilegeChange::YesToNo),
135
                    update_priv: Some(DatabasePrivilegeChange::YesToNo),
136
                    delete_priv: Some(DatabasePrivilegeChange::YesToNo),
137
                    create_priv: Some(DatabasePrivilegeChange::YesToNo),
138
                    drop_priv: Some(DatabasePrivilegeChange::YesToNo),
139
                    alter_priv: Some(DatabasePrivilegeChange::YesToNo),
140
                    index_priv: Some(DatabasePrivilegeChange::YesToNo),
141
                    create_tmp_table_priv: Some(DatabasePrivilegeChange::YesToNo),
142
                    lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo),
143
                    references_priv: Some(DatabasePrivilegeChange::YesToNo),
144
                    create_view_priv: Some(DatabasePrivilegeChange::YesToNo),
145
                    show_view_priv: Some(DatabasePrivilegeChange::YesToNo),
146
                    trigger_priv: Some(DatabasePrivilegeChange::YesToNo),
147
                };
148
                for priv_char in &self.privilege_edit.privileges {
149
                    match priv_char {
150
                        's' => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
151
                        'i' => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
152
                        'u' => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
153
                        'd' => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
154
                        'c' => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
155
                        'D' => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
156
                        'a' => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
157
                        'I' => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
158
                        't' => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
159
                        'l' => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
160
                        'r' => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
161
                        'v' => diff.create_view_priv = Some(DatabasePrivilegeChange::NoToYes),
162
                        'V' => diff.show_view_priv = Some(DatabasePrivilegeChange::NoToYes),
163
                        'T' => diff.trigger_priv = Some(DatabasePrivilegeChange::NoToYes),
164
                        'A' => {
165
                            diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
166
                            diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
167
                            diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
168
                            diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes);
169
                            diff.create_priv = Some(DatabasePrivilegeChange::NoToYes);
170
                            diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes);
171
                            diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes);
172
                            diff.index_priv = Some(DatabasePrivilegeChange::NoToYes);
173
                            diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes);
174
                            diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes);
175
                            diff.references_priv = Some(DatabasePrivilegeChange::NoToYes);
176
                            diff.create_view_priv = Some(DatabasePrivilegeChange::NoToYes);
177
                            diff.show_view_priv = Some(DatabasePrivilegeChange::NoToYes);
178
                            diff.trigger_priv = Some(DatabasePrivilegeChange::NoToYes);
179
                        }
180
                        _ => unreachable!(),
181
                    }
182
                }
183
            }
184
            DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
185
                diff = DatabasePrivilegeRowDiff {
186
                    db: self.database.clone(),
187
                    user: self.user.clone(),
188
                    select_priv: None,
189
                    insert_priv: None,
190
                    update_priv: None,
191
                    delete_priv: None,
192
                    create_priv: None,
193
                    drop_priv: None,
194
                    alter_priv: None,
195
                    index_priv: None,
196
                    create_tmp_table_priv: None,
197
                    lock_tables_priv: None,
198
                    references_priv: None,
199
                    create_view_priv: None,
200
                    show_view_priv: None,
201
                    trigger_priv: None,
202
                };
203
                let value = match self.privilege_edit.type_ {
204
                    DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
205
                    DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
206
                    _ => unreachable!(),
207
                };
208
                for priv_char in &self.privilege_edit.privileges {
209
                    match priv_char {
210
                        's' => diff.select_priv = Some(value),
211
                        'i' => diff.insert_priv = Some(value),
212
                        'u' => diff.update_priv = Some(value),
213
                        'd' => diff.delete_priv = Some(value),
214
                        'c' => diff.create_priv = Some(value),
215
                        'D' => diff.drop_priv = Some(value),
216
                        'a' => diff.alter_priv = Some(value),
217
                        'I' => diff.index_priv = Some(value),
218
                        't' => diff.create_tmp_table_priv = Some(value),
219
                        'l' => diff.lock_tables_priv = Some(value),
220
                        'r' => diff.references_priv = Some(value),
221
                        'v' => diff.create_view_priv = Some(value),
222
                        'V' => diff.show_view_priv = Some(value),
223
                        'T' => diff.trigger_priv = Some(value),
224
                        'A' => {
225
                            diff.select_priv = Some(value);
226
                            diff.insert_priv = Some(value);
227
                            diff.update_priv = Some(value);
228
                            diff.delete_priv = Some(value);
229
                            diff.create_priv = Some(value);
230
                            diff.drop_priv = Some(value);
231
                            diff.alter_priv = Some(value);
232
                            diff.index_priv = Some(value);
233
                            diff.create_tmp_table_priv = Some(value);
234
                            diff.lock_tables_priv = Some(value);
235
                            diff.references_priv = Some(value);
236
                            diff.create_view_priv = Some(value);
237
                            diff.show_view_priv = Some(value);
238
                            diff.trigger_priv = Some(value);
239
                        }
240
                        _ => unreachable!(),
241
                    }
242
                }
243
            }
244
        }
245

            
246
        Ok(diff)
247
    }
248
}
249

            
250
impl std::fmt::Display for DatabasePrivilegeEditEntry {
251
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
252
        write!(f, "{}:, ", self.database)?;
253
        write!(f, "{}: ", self.user)?;
254
        write!(f, "{}", self.privilege_edit)?;
255
        Ok(())
256
    }
257
}
258

            
259
#[cfg(test)]
260
mod tests {
261
    use super::*;
262

            
263
    #[test]
264
1
    fn test_cli_arg_parse_set_db_user_all() {
265
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:A");
266
1
        assert_eq!(
267
1
            result.ok(),
268
1
            Some(DatabasePrivilegeEditEntry {
269
1
                database: "db".into(),
270
1
                user: "user".into(),
271
1
                privilege_edit: DatabasePrivilegeEdit {
272
1
                    type_: DatabasePrivilegeEditEntryType::Set,
273
1
                    privileges: vec!['A'],
274
1
                },
275
1
            })
276
        );
277
1
    }
278

            
279
    #[test]
280
1
    fn test_cli_arg_parse_set_db_user_none() {
281
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:");
282
1
        assert_eq!(
283
1
            result.ok(),
284
1
            Some(DatabasePrivilegeEditEntry {
285
1
                database: "db".into(),
286
1
                user: "user".into(),
287
1
                privilege_edit: DatabasePrivilegeEdit {
288
1
                    type_: DatabasePrivilegeEditEntryType::Set,
289
1
                    privileges: vec![],
290
1
                },
291
1
            })
292
        );
293
1
    }
294

            
295
    #[test]
296
1
    fn test_cli_arg_parse_set_db_user_misc() {
297
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:siud");
298
1
        assert_eq!(
299
1
            result.ok(),
300
1
            Some(DatabasePrivilegeEditEntry {
301
1
                database: "db".into(),
302
1
                user: "user".into(),
303
1
                privilege_edit: DatabasePrivilegeEdit {
304
1
                    type_: DatabasePrivilegeEditEntryType::Set,
305
1
                    privileges: vec!['s', 'i', 'u', 'd'],
306
1
                },
307
1
            })
308
        );
309
1
    }
310

            
311
    #[test]
312
1
    fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
313
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
314
1
        assert!(result.is_err());
315
1
    }
316

            
317
    #[test]
318
1
    fn test_cli_arg_parse_set_user_empty_string() {
319
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("::");
320
1
        assert!(result.is_err());
321
1
    }
322

            
323
    #[test]
324
1
    fn test_cli_arg_parse_set_db_user_empty_string() {
325
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db::");
326
1
        assert!(result.is_err());
327
1
    }
328

            
329
    #[test]
330
1
    fn test_cli_arg_parse_add_db_user_misc() {
331
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:+siud");
332
1
        assert_eq!(
333
1
            result.ok(),
334
1
            Some(DatabasePrivilegeEditEntry {
335
1
                database: "db".into(),
336
1
                user: "user".into(),
337
1
                privilege_edit: DatabasePrivilegeEdit {
338
1
                    type_: DatabasePrivilegeEditEntryType::Add,
339
1
                    privileges: vec!['s', 'i', 'u', 'd'],
340
1
                },
341
1
            })
342
        );
343
1
    }
344

            
345
    #[test]
346
1
    fn test_cli_arg_parse_remove_db_user_misc() {
347
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:-siud");
348
1
        assert_eq!(
349
1
            result.ok(),
350
1
            Some(DatabasePrivilegeEditEntry {
351
1
                database: "db".into(),
352
1
                user: "user".into(),
353
1
                privilege_edit: DatabasePrivilegeEdit {
354
1
                    type_: DatabasePrivilegeEditEntryType::Remove,
355
1
                    privileges: vec!['s', 'i', 'u', 'd'],
356
1
                },
357
1
            }),
358
        );
359
1
    }
360
}