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

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

            
7
/// This enum represents a part of a CLI argument for editing database privileges,
8
/// indicating whether privileges are to be added, set, or removed.
9
#[derive(Debug, Clone, PartialEq, Eq)]
10
pub enum DatabasePrivilegeEditEntryType {
11
    Add,
12
    Set,
13
    Remove,
14
}
15

            
16
/// This struct represents a single CLI argument for editing database privileges.
17
///
18
/// This is typically parsed from a string looking like:
19
///
20
///   `[database_name:]username:[+|-]privileges`
21
#[derive(Debug, Clone, PartialEq, Eq)]
22
pub struct DatabasePrivilegeEditEntry {
23
    pub database: Option<MySQLDatabase>,
24
    pub user: MySQLUser,
25
    pub type_: DatabasePrivilegeEditEntryType,
26
    pub privileges: Vec<String>,
27
}
28

            
29
impl DatabasePrivilegeEditEntry {
30
    /// Parses a privilege edit entry from a string.
31
    ///
32
    /// The expected format is:
33
    ///
34
    ///   `[database_name:]username:[+|-]privileges`
35
    ///
36
    /// where:
37
    /// - database_name is optional, if omitted the entry applies to all databases
38
    /// - username is the name of the user to edit privileges for
39
    /// - privileges is a string of characters representing the privileges to add, set or remove
40
    /// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly
41
    /// - privileges characters are: siudcDaAItlrA
42
9
    pub fn parse_from_str(arg: &str) -> anyhow::Result<DatabasePrivilegeEditEntry> {
43
9
        let parts: Vec<&str> = arg.split(':').collect();
44
9
        if parts.len() < 2 || parts.len() > 3 {
45
            anyhow::bail!("Invalid privilege edit entry format: {}", arg);
46
9
        }
47

            
48
9
        let (database, user, user_privs) = if parts.len() == 3 {
49
8
            (Some(parts[0].to_string()), parts[1].to_string(), parts[2])
50
        } else {
51
1
            (None, parts[0].to_string(), parts[1])
52
        };
53

            
54
9
        if user.is_empty() {
55
2
            anyhow::bail!("Username cannot be empty in privilege edit entry: {}", arg);
56
7
        }
57

            
58
7
        let (edit_type, privs_str) = if let Some(privs_str) = user_privs.strip_prefix('+') {
59
1
            (DatabasePrivilegeEditEntryType::Add, privs_str)
60
6
        } else if let Some(privs_str) = user_privs.strip_prefix('-') {
61
1
            (DatabasePrivilegeEditEntryType::Remove, privs_str)
62
        } else {
63
5
            (DatabasePrivilegeEditEntryType::Set, user_privs)
64
        };
65

            
66
18
        let privileges: Vec<String> = privs_str.chars().map(|c| c.to_string()).collect();
67
18
        if privileges.iter().any(|c| !"siudcDaAItlrA".contains(c)) {
68
1
            let invalid_chars: String = privileges
69
1
                .iter()
70
1
                .filter(|c| !"siudcDaAItlrA".contains(c.as_str()))
71
1
                .cloned()
72
1
                .collect();
73
1
            anyhow::bail!(
74
                "Invalid character(s) in privilege edit entry: {}",
75
                invalid_chars
76
            );
77
6
        }
78

            
79
6
        Ok(DatabasePrivilegeEditEntry {
80
6
            database: database.map(MySQLDatabase::from),
81
6
            user: MySQLUser::from(user),
82
6
            type_: edit_type,
83
6
            privileges,
84
6
        })
85
9
    }
86

            
87
    pub fn as_database_privileges_diff(
88
        &self,
89
        external_database_name: Option<&MySQLDatabase>,
90
    ) -> anyhow::Result<DatabasePrivilegeRowDiff> {
91
        let database = match self.database.as_ref() {
92
            Some(db) => db.clone(),
93
            None => {
94
                if let Some(external_db) = external_database_name {
95
                    external_db.clone()
96
                } else {
97
                    anyhow::bail!(
98
                        "Database name must be specified either in the privilege edit entry or as an external argument."
99
                    );
100
                }
101
            }
102
        };
103
        let mut diff;
104
        match self.type_ {
105
            DatabasePrivilegeEditEntryType::Set => {
106
                diff = DatabasePrivilegeRowDiff {
107
                    db: database,
108
                    user: self.user.clone(),
109
                    select_priv: Some(DatabasePrivilegeChange::YesToNo),
110
                    insert_priv: Some(DatabasePrivilegeChange::YesToNo),
111
                    update_priv: Some(DatabasePrivilegeChange::YesToNo),
112
                    delete_priv: Some(DatabasePrivilegeChange::YesToNo),
113
                    create_priv: Some(DatabasePrivilegeChange::YesToNo),
114
                    drop_priv: Some(DatabasePrivilegeChange::YesToNo),
115
                    alter_priv: Some(DatabasePrivilegeChange::YesToNo),
116
                    index_priv: Some(DatabasePrivilegeChange::YesToNo),
117
                    create_tmp_table_priv: Some(DatabasePrivilegeChange::YesToNo),
118
                    lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo),
119
                    references_priv: Some(DatabasePrivilegeChange::YesToNo),
120
                };
121
                for priv_char in &self.privileges {
122
                    match priv_char.as_str() {
123
                        "s" => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
124
                        "i" => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
125
                        "u" => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
126
                        "d" => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
127
                        "c" => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
128
                        "D" => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
129
                        "a" => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
130
                        "I" => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
131
                        "t" => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
132
                        "l" => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
133
                        "r" => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
134
                        "A" => {
135
                            diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
136
                            diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
137
                            diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
138
                            diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes);
139
                            diff.create_priv = Some(DatabasePrivilegeChange::NoToYes);
140
                            diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes);
141
                            diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes);
142
                            diff.index_priv = Some(DatabasePrivilegeChange::NoToYes);
143
                            diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes);
144
                            diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes);
145
                            diff.references_priv = Some(DatabasePrivilegeChange::NoToYes);
146
                        }
147
                        _ => unreachable!(),
148
                    }
149
                }
150
            }
151
            DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
152
                diff = DatabasePrivilegeRowDiff {
153
                    db: database,
154
                    user: self.user.clone(),
155
                    select_priv: None,
156
                    insert_priv: None,
157
                    update_priv: None,
158
                    delete_priv: None,
159
                    create_priv: None,
160
                    drop_priv: None,
161
                    alter_priv: None,
162
                    index_priv: None,
163
                    create_tmp_table_priv: None,
164
                    lock_tables_priv: None,
165
                    references_priv: None,
166
                };
167
                let value = match self.type_ {
168
                    DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
169
                    DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
170
                    _ => unreachable!(),
171
                };
172
                for priv_char in &self.privileges {
173
                    match priv_char.as_str() {
174
                        "s" => diff.select_priv = Some(value),
175
                        "i" => diff.insert_priv = Some(value),
176
                        "u" => diff.update_priv = Some(value),
177
                        "d" => diff.delete_priv = Some(value),
178
                        "c" => diff.create_priv = Some(value),
179
                        "D" => diff.drop_priv = Some(value),
180
                        "a" => diff.alter_priv = Some(value),
181
                        "I" => diff.index_priv = Some(value),
182
                        "t" => diff.create_tmp_table_priv = Some(value),
183
                        "l" => diff.lock_tables_priv = Some(value),
184
                        "r" => diff.references_priv = Some(value),
185
                        "A" => {
186
                            diff.select_priv = Some(value);
187
                            diff.insert_priv = Some(value);
188
                            diff.update_priv = Some(value);
189
                            diff.delete_priv = Some(value);
190
                            diff.create_priv = Some(value);
191
                            diff.drop_priv = Some(value);
192
                            diff.alter_priv = Some(value);
193
                            diff.index_priv = Some(value);
194
                            diff.create_tmp_table_priv = Some(value);
195
                            diff.lock_tables_priv = Some(value);
196
                            diff.references_priv = Some(value);
197
                        }
198
                        _ => unreachable!(),
199
                    }
200
                }
201
            }
202
        }
203

            
204
        Ok(diff)
205
    }
206
}
207

            
208
impl std::fmt::Display for DatabasePrivilegeEditEntry {
209
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210
        if let Some(db) = &self.database {
211
            write!(f, "{}:, ", db)?;
212
        }
213
        write!(f, "{}: ", self.user)?;
214
        match self.type_ {
215
            DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
216
            DatabasePrivilegeEditEntryType::Set => {}
217
            DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
218
        }
219
        for priv_char in &self.privileges {
220
            write!(f, "{}", priv_char)?;
221
        }
222

            
223
        Ok(())
224
    }
225
}
226

            
227
#[cfg(test)]
228
mod tests {
229
    use super::*;
230

            
231
    #[test]
232
1
    fn test_cli_arg_parse_set_db_user_all() {
233
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:A");
234
1
        assert_eq!(
235
1
            result.ok(),
236
1
            Some(DatabasePrivilegeEditEntry {
237
1
                database: Some("db".into()),
238
1
                user: "user".into(),
239
1
                type_: DatabasePrivilegeEditEntryType::Set,
240
1
                privileges: vec!["A".into()],
241
1
            })
242
        );
243
1
    }
244

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

            
259
    #[test]
260
1
    fn test_cli_arg_parse_set_db_user_misc() {
261
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:siud");
262
1
        assert_eq!(
263
1
            result.ok(),
264
1
            Some(DatabasePrivilegeEditEntry {
265
1
                database: Some("db".into()),
266
1
                user: "user".into(),
267
1
                type_: DatabasePrivilegeEditEntryType::Set,
268
1
                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
269
1
            })
270
        );
271
1
    }
272

            
273
    #[test]
274
1
    fn test_cli_arg_parse_set_user_nonexistent_misc() {
275
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("user:siud");
276
1
        assert_eq!(
277
1
            result.ok(),
278
1
            Some(DatabasePrivilegeEditEntry {
279
1
                database: None,
280
1
                user: "user".into(),
281
1
                type_: DatabasePrivilegeEditEntryType::Set,
282
1
                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
283
1
            }),
284
        );
285
1
    }
286

            
287
    #[test]
288
1
    fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
289
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
290
1
        assert!(result.is_err());
291
1
    }
292

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

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

            
305
    #[test]
306
1
    fn test_cli_arg_parse_add_db_user_misc() {
307
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:+siud");
308
1
        assert_eq!(
309
1
            result.ok(),
310
1
            Some(DatabasePrivilegeEditEntry {
311
1
                database: Some("db".into()),
312
1
                user: "user".into(),
313
1
                type_: DatabasePrivilegeEditEntryType::Add,
314
1
                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
315
1
            })
316
        );
317
1
    }
318

            
319
    #[test]
320
1
    fn test_cli_arg_parse_remove_db_user_misc() {
321
1
        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:-siud");
322
1
        assert_eq!(
323
1
            result.ok(),
324
1
            Some(DatabasePrivilegeEditEntry {
325
1
                database: Some("db".into()),
326
1
                user: "user".into(),
327
1
                type_: DatabasePrivilegeEditEntryType::Remove,
328
1
                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
329
1
            }),
330
        );
331
1
    }
332
}