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', 'A', 'I', 't', 'l', 'r', '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
13
                .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: siudcDaAItlrA
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
                };
145
                for priv_char in &self.privilege_edit.privileges {
146
                    match priv_char {
147
                        's' => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
148
                        'i' => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
149
                        'u' => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
150
                        'd' => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
151
                        'c' => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
152
                        'D' => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
153
                        'a' => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
154
                        'I' => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
155
                        't' => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
156
                        'l' => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
157
                        'r' => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
158
                        'A' => {
159
                            diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
160
                            diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
161
                            diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
162
                            diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes);
163
                            diff.create_priv = Some(DatabasePrivilegeChange::NoToYes);
164
                            diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes);
165
                            diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes);
166
                            diff.index_priv = Some(DatabasePrivilegeChange::NoToYes);
167
                            diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes);
168
                            diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes);
169
                            diff.references_priv = Some(DatabasePrivilegeChange::NoToYes);
170
                        }
171
                        _ => unreachable!(),
172
                    }
173
                }
174
            }
175
            DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
176
                diff = DatabasePrivilegeRowDiff {
177
                    db: self.database.clone(),
178
                    user: self.user.clone(),
179
                    select_priv: None,
180
                    insert_priv: None,
181
                    update_priv: None,
182
                    delete_priv: None,
183
                    create_priv: None,
184
                    drop_priv: None,
185
                    alter_priv: None,
186
                    index_priv: None,
187
                    create_tmp_table_priv: None,
188
                    lock_tables_priv: None,
189
                    references_priv: None,
190
                };
191
                let value = match self.privilege_edit.type_ {
192
                    DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
193
                    DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
194
                    _ => unreachable!(),
195
                };
196
                for priv_char in &self.privilege_edit.privileges {
197
                    match priv_char {
198
                        's' => diff.select_priv = Some(value),
199
                        'i' => diff.insert_priv = Some(value),
200
                        'u' => diff.update_priv = Some(value),
201
                        'd' => diff.delete_priv = Some(value),
202
                        'c' => diff.create_priv = Some(value),
203
                        'D' => diff.drop_priv = Some(value),
204
                        'a' => diff.alter_priv = Some(value),
205
                        'I' => diff.index_priv = Some(value),
206
                        't' => diff.create_tmp_table_priv = Some(value),
207
                        'l' => diff.lock_tables_priv = Some(value),
208
                        'r' => diff.references_priv = Some(value),
209
                        'A' => {
210
                            diff.select_priv = Some(value);
211
                            diff.insert_priv = Some(value);
212
                            diff.update_priv = Some(value);
213
                            diff.delete_priv = Some(value);
214
                            diff.create_priv = Some(value);
215
                            diff.drop_priv = Some(value);
216
                            diff.alter_priv = Some(value);
217
                            diff.index_priv = Some(value);
218
                            diff.create_tmp_table_priv = Some(value);
219
                            diff.lock_tables_priv = Some(value);
220
                            diff.references_priv = Some(value);
221
                        }
222
                        _ => unreachable!(),
223
                    }
224
                }
225
            }
226
        }
227

            
228
        Ok(diff)
229
    }
230
}
231

            
232
impl std::fmt::Display for DatabasePrivilegeEditEntry {
233
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234
        write!(f, "{}:, ", self.database)?;
235
        write!(f, "{}: ", self.user)?;
236
        write!(f, "{}", self.privilege_edit)?;
237
        Ok(())
238
    }
239
}
240

            
241
#[cfg(test)]
242
mod tests {
243
    use super::*;
244

            
245
    #[test]
246
1
    fn test_cli_arg_parse_set_db_user_all() {
247
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:A");
248
1
        assert_eq!(
249
1
            result.ok(),
250
1
            Some(DatabasePrivilegeEditEntry {
251
1
                database: "db".into(),
252
1
                user: "user".into(),
253
1
                privilege_edit: DatabasePrivilegeEdit {
254
1
                    type_: DatabasePrivilegeEditEntryType::Set,
255
1
                    privileges: vec!['A'],
256
1
                },
257
1
            })
258
        );
259
1
    }
260

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

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

            
293
    #[test]
294
1
    fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
295
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
296
1
        assert!(result.is_err());
297
1
    }
298

            
299
    #[test]
300
1
    fn test_cli_arg_parse_set_user_empty_string() {
301
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("::");
302
1
        assert!(result.is_err());
303
1
    }
304

            
305
    #[test]
306
1
    fn test_cli_arg_parse_set_db_user_empty_string() {
307
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db::");
308
1
        assert!(result.is_err());
309
1
    }
310

            
311
    #[test]
312
1
    fn test_cli_arg_parse_add_db_user_misc() {
313
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:+siud");
314
1
        assert_eq!(
315
1
            result.ok(),
316
1
            Some(DatabasePrivilegeEditEntry {
317
1
                database: "db".into(),
318
1
                user: "user".into(),
319
1
                privilege_edit: DatabasePrivilegeEdit {
320
1
                    type_: DatabasePrivilegeEditEntryType::Add,
321
1
                    privileges: vec!['s', 'i', 'u', 'd'],
322
1
                },
323
1
            })
324
        );
325
1
    }
326

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