mysqladm/core/database_privileges/
cli.rs

1//! This module contains serialization and deserialization logic for
2//! database privileges related CLI commands.
3
4use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff};
5use 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)]
10pub 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)]
22pub struct DatabasePrivilegeEditEntry {
23    pub database: Option<MySQLDatabase>,
24    pub user: MySQLUser,
25    pub type_: DatabasePrivilegeEditEntryType,
26    pub privileges: Vec<String>,
27}
28
29impl 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    pub fn parse_from_str(arg: &str) -> anyhow::Result<DatabasePrivilegeEditEntry> {
43        let parts: Vec<&str> = arg.split(':').collect();
44        if parts.len() < 2 || parts.len() > 3 {
45            anyhow::bail!("Invalid privilege edit entry format: {}", arg);
46        }
47
48        let (database, user, user_privs) = if parts.len() == 3 {
49            (Some(parts[0].to_string()), parts[1].to_string(), parts[2])
50        } else {
51            (None, parts[0].to_string(), parts[1])
52        };
53
54        if user.is_empty() {
55            anyhow::bail!("Username cannot be empty in privilege edit entry: {}", arg);
56        }
57
58        let (edit_type, privs_str) = if let Some(privs_str) = user_privs.strip_prefix('+') {
59            (DatabasePrivilegeEditEntryType::Add, privs_str)
60        } else if let Some(privs_str) = user_privs.strip_prefix('-') {
61            (DatabasePrivilegeEditEntryType::Remove, privs_str)
62        } else {
63            (DatabasePrivilegeEditEntryType::Set, user_privs)
64        };
65
66        let privileges: Vec<String> = privs_str.chars().map(|c| c.to_string()).collect();
67        if privileges.iter().any(|c| !"siudcDaAItlrA".contains(c)) {
68            let invalid_chars: String = privileges
69                .iter()
70                .filter(|c| !"siudcDaAItlrA".contains(c.as_str()))
71                .cloned()
72                .collect();
73            anyhow::bail!(
74                "Invalid character(s) in privilege edit entry: {}",
75                invalid_chars
76            );
77        }
78
79        Ok(DatabasePrivilegeEditEntry {
80            database: database.map(MySQLDatabase::from),
81            user: MySQLUser::from(user),
82            type_: edit_type,
83            privileges,
84        })
85    }
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
208impl 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)]
228mod tests {
229    use super::*;
230
231    #[test]
232    fn test_cli_arg_parse_set_db_user_all() {
233        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:A");
234        assert_eq!(
235            result.ok(),
236            Some(DatabasePrivilegeEditEntry {
237                database: Some("db".into()),
238                user: "user".into(),
239                type_: DatabasePrivilegeEditEntryType::Set,
240                privileges: vec!["A".into()],
241            })
242        );
243    }
244
245    #[test]
246    fn test_cli_arg_parse_set_db_user_none() {
247        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:");
248        assert_eq!(
249            result.ok(),
250            Some(DatabasePrivilegeEditEntry {
251                database: Some("db".into()),
252                user: "user".into(),
253                type_: DatabasePrivilegeEditEntryType::Set,
254                privileges: vec![],
255            })
256        );
257    }
258
259    #[test]
260    fn test_cli_arg_parse_set_db_user_misc() {
261        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:siud");
262        assert_eq!(
263            result.ok(),
264            Some(DatabasePrivilegeEditEntry {
265                database: Some("db".into()),
266                user: "user".into(),
267                type_: DatabasePrivilegeEditEntryType::Set,
268                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
269            })
270        );
271    }
272
273    #[test]
274    fn test_cli_arg_parse_set_user_nonexistent_misc() {
275        let result = DatabasePrivilegeEditEntry::parse_from_str("user:siud");
276        assert_eq!(
277            result.ok(),
278            Some(DatabasePrivilegeEditEntry {
279                database: None,
280                user: "user".into(),
281                type_: DatabasePrivilegeEditEntryType::Set,
282                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
283            }),
284        );
285    }
286
287    #[test]
288    fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
289        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
290        assert!(result.is_err());
291    }
292
293    #[test]
294    fn test_cli_arg_parse_set_user_empty_string() {
295        let result = DatabasePrivilegeEditEntry::parse_from_str("::");
296        assert!(result.is_err());
297    }
298
299    #[test]
300    fn test_cli_arg_parse_set_db_user_empty_string() {
301        let result = DatabasePrivilegeEditEntry::parse_from_str("db::");
302        assert!(result.is_err());
303    }
304
305    #[test]
306    fn test_cli_arg_parse_add_db_user_misc() {
307        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:+siud");
308        assert_eq!(
309            result.ok(),
310            Some(DatabasePrivilegeEditEntry {
311                database: Some("db".into()),
312                user: "user".into(),
313                type_: DatabasePrivilegeEditEntryType::Add,
314                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
315            })
316        );
317    }
318
319    #[test]
320    fn test_cli_arg_parse_remove_db_user_misc() {
321        let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:-siud");
322        assert_eq!(
323            result.ok(),
324            Some(DatabasePrivilegeEditEntry {
325                database: Some("db".into()),
326                user: "user".into(),
327                type_: DatabasePrivilegeEditEntryType::Remove,
328                privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
329            }),
330        );
331    }
332}