diff --git a/src/client/commands.rs b/src/client/commands.rs index 7f44f35..a97f767 100644 --- a/src/client/commands.rs +++ b/src/client/commands.rs @@ -52,23 +52,28 @@ pub enum ClientCommand { /// Change user privileges for one or more databases. See `edit-privs --help` for details. /// - /// This command has two modes of operation: + /// This command has three modes of operation: /// - /// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the privileges using a text editor. + /// 1. Interactive mode: + /// + /// If no arguments are provided, the user will be prompted to edit the privileges using a text editor. /// /// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables. /// /// Follow the instructions inside the editor for more information. /// - /// 2. Non-interactive mode: If the `-p` flag is specified, the user can write privileges using arguments. + /// 2. Non-interactive human-friendly mode: /// - /// The privilege arguments should be formatted as `::` - /// where the privileges are a string of characters, each representing a single privilege. + /// You can provide the command with three positional arguments: + /// + /// - ``: The name of the database for which you want to edit privileges. + /// - ``: The name of the user whose privileges you want to edit. + /// - `<[+-]PRIVILEGES>`: A string representing the privileges to set for the user. + /// + /// The `<[+-]PRIVILEGES>` argument is a string of characters, each representing a single privilege. /// The character `A` is an exception - it represents all privileges. - /// - /// The `` character is optional and can be either `+` to grant additional privileges - /// or `-` to revoke privileges. If omitted, the privileges will be set exactly as specified, - /// removing any privileges not listed, and adding any that are. + /// The optional leading character can be either `+` to grant additional privileges or `-` to revoke privileges. + /// If omitted, the privileges will be set exactly as specified, removing any privileges not listed, and adding any that are. /// /// The character-to-privilege mapping is defined as follows: /// @@ -85,30 +90,36 @@ pub enum ClientCommand { /// - `r` - REFERENCES /// - `A` - ALL PRIVILEGES /// - /// If you provide a database name, you can omit it from the privilege string, - /// e.g. `edit-privs my_db -p my_user:siu` is equivalent to `edit-privs -p my_db:my_user:siu`. - /// While it doesn't make much of a difference for a single edit, it can be useful for editing multiple users - /// on the same database at once. + /// 3. Non-interactive batch mode: + /// + /// By using the `-p` flag, you can provide multiple privilege edits in a single command. + /// + /// The flag value should be formatted as `DB_NAME:USER_NAME:[+-]PRIVILEGES` + /// where the privileges are a string of characters, each representing a single privilege. + /// (See the character-to-privilege mapping above.) /// /// Example usage of non-interactive mode: /// - /// Enable privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`: + /// Set privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`: /// - /// `muscl edit-privs -p my_db:my_user:siu` + /// `muscl edit-privs my_db my_user siu` /// - /// Enable all privileges for user `my_other_user` on database `my_other_db`: + /// Set all privileges for user `my_other_user` on database `my_other_db`: /// - /// `muscl edit-privs -p my_other_db:my_other_user:A` - /// - /// Set miscellaneous privileges for multiple users on database `my_db`: - /// - /// `muscl edit-privs my_db -p my_user:siu my_other_user:ct`` + /// `muscl edit-privs my_other_db my_other_user A` /// /// Add the `DELETE` privilege for user `my_user` on database `my_db`: /// - /// `muscl edit-privs my_db -p my_user:+d + /// `muscl edit-privs -p my_db my_user +d /// - #[command(verbatim_doc_comment)] + /// Set miscellaneous privileges for multiple users on database `my_db`: + /// + /// `muscl edit-privs -p my_db:my_user:siu -p my_db:my_other_user:+ct` + /// + #[command( + verbatim_doc_comment, + override_usage = "muscl edit-privs [OPTIONS] [ -p ... | <[+-]PRIVILEGES> ]" + )] EditPrivs(EditPrivsArgs), /// Create one or more users @@ -142,7 +153,9 @@ pub async fn handle_command( ClientCommand::DropDb(args) => drop_databases(args, server_connection).await, ClientCommand::ShowDb(args) => show_databases(args, server_connection).await, ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await, - ClientCommand::EditPrivs(args) => edit_database_privileges(args, server_connection).await, + ClientCommand::EditPrivs(args) => { + edit_database_privileges(args, None, server_connection).await + } ClientCommand::CreateUser(args) => create_users(args, server_connection).await, ClientCommand::DropUser(args) => drop_users(args, server_connection).await, ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await, diff --git a/src/client/commands/edit_privs.rs b/src/client/commands/edit_privs.rs index 71ce15a..ac88d08 100644 --- a/src/client/commands/edit_privs.rs +++ b/src/client/commands/edit_privs.rs @@ -1,7 +1,7 @@ use std::collections::{BTreeMap, BTreeSet}; use anyhow::Context; -use clap::Parser; +use clap::{Args, Parser}; use clap_complete::ArgValueCompleter; use dialoguer::{Confirm, Editor}; use futures_util::SinkExt; @@ -11,11 +11,11 @@ use tokio_stream::StreamExt; use crate::{ client::commands::erroneous_server_response, core::{ - completion::mysql_database_completer, + completion::{mysql_database_completer, mysql_user_completer}, database_privileges::{ - DatabasePrivilegeEditEntry, DatabasePrivilegeRow, DatabasePrivilegeRowDiff, - DatabasePrivilegesDiff, create_or_modify_privilege_rows, diff_privileges, - display_privilege_diffs, generate_editor_content_from_privilege_data, + DatabasePrivilegeEdit, DatabasePrivilegeEditEntry, DatabasePrivilegeRow, + DatabasePrivilegeRowDiff, DatabasePrivilegesDiff, create_or_modify_privilege_rows, + diff_privileges, display_privilege_diffs, generate_editor_content_from_privilege_data, parse_privilege_data_from_editor_content, reduce_privilege_diffs, }, protocol::{ @@ -28,20 +28,24 @@ use crate::{ #[derive(Parser, Debug, Clone)] pub struct EditPrivsArgs { - /// The MySQL database to edit privileges for - #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))] - #[arg(value_name = "DB_NAME")] - pub name: Option, - + /// The privileges to set, grant or revoke, in the format `DATABASE:USER:[+-]PRIVILEGES` + /// + /// This option allows for changing privileges for multiple databases and users in batch. + /// + /// This can not be used together with the positional `DB_NAME`, `USER_NAME` and `PRIVILEGES` arguments. #[arg( short, long, - value_name = "[DATABASE:]USER:[+-]PRIVILEGES", + value_name = "DB_NAME:USER_NAME:[+-]PRIVILEGES", num_args = 0.., value_parser = DatabasePrivilegeEditEntry::parse_from_str, + conflicts_with("single_priv"), )] pub privs: Vec, + #[command(flatten)] + pub single_priv: Option, + /// Print the information as JSON #[arg(short, long)] pub json: bool, @@ -60,6 +64,31 @@ pub struct EditPrivsArgs { pub yes: bool, } +#[derive(Args, Debug, Clone)] +pub struct SinglePrivilegeEditArgs { + /// The MySQL database to edit privileges for + #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))] + #[arg( + value_name = "DB_NAME", + requires = "user_name", + requires = "single_priv" + )] + pub db_name: Option, + + /// The MySQL database to edit privileges for + #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))] + #[arg(value_name = "USER_NAME")] + pub user_name: Option, + + /// The privileges to set, grant or revoke + #[arg( + allow_hyphen_values = true, + value_name = "[+-]PRIVILEGES", + value_parser = DatabasePrivilegeEdit::parse_from_str, + )] + pub single_priv: Option, +} + async fn users_exist( server_connection: &mut ClientToServerMessageStream, privilege_diff: &BTreeSet, @@ -120,12 +149,42 @@ async fn databases_exist( pub async fn edit_database_privileges( args: EditPrivsArgs, + // NOTE: this is only used for backwards compat with mysql-admutils + use_database: Option, mut server_connection: ClientToServerMessageStream, ) -> anyhow::Result<()> { - let message = Request::ListPrivileges(args.name.to_owned().map(|name| vec![name])); + let message = Request::ListPrivileges(use_database.clone().map(|db| vec![db])); server_connection.send(message).await?; + debug_assert!(args.privs.is_empty() ^ args.single_priv.is_none()); + + let privs = if let Some(single_priv_entry) = &args.single_priv { + let database = single_priv_entry.db_name.clone().ok_or_else(|| { + anyhow::anyhow!( + "DB_NAME must be specified when editing privileges in single privilege mode" + ) + })?; + let user = single_priv_entry.user_name.clone().ok_or_else(|| { + anyhow::anyhow!( + "USER_NAME must be specified when DB_NAME is specified in single privilege mode" + ) + })?; + let privilege_edit = single_priv_entry.single_priv.clone().ok_or_else(|| { + anyhow::anyhow!( + "PRIVILEGES must be specified when DB_NAME is specified in single privilege mode" + ) + })?; + + vec![DatabasePrivilegeEditEntry { + database, + user, + privilege_edit, + }] + } else { + args.privs.clone() + }; + let existing_privilege_rows = match server_connection.next().await { Some(Ok(Response::ListPrivileges(databases))) => databases .into_iter() @@ -151,12 +210,12 @@ pub async fn edit_database_privileges( response => return erroneous_server_response(response), }; - let diffs: BTreeSet = if !args.privs.is_empty() { - let privileges_to_change = parse_privilege_tables_from_args(&args)?; + let diffs: BTreeSet = if !privs.is_empty() { + let privileges_to_change = parse_privilege_tables(&privs)?; create_or_modify_privilege_rows(&existing_privilege_rows, &privileges_to_change)? } else { let privileges_to_change = - edit_privileges_with_editor(&existing_privilege_rows, args.name.as_ref())?; + edit_privileges_with_editor(&existing_privilege_rows, use_database.as_ref())?; diff_privileges(&existing_privilege_rows, &privileges_to_change) }; @@ -220,15 +279,15 @@ pub async fn edit_database_privileges( Ok(()) } -fn parse_privilege_tables_from_args( - args: &EditPrivsArgs, +fn parse_privilege_tables( + privs: &[DatabasePrivilegeEditEntry], ) -> anyhow::Result> { - debug_assert!(!args.privs.is_empty()); - args.privs + debug_assert!(!privs.is_empty()); + privs .iter() .map(|priv_edit_entry| { priv_edit_entry - .as_database_privileges_diff(args.name.as_ref()) + .as_database_privileges_diff() .context(format!( "Failed parsing database privileges: `{}`", priv_edit_entry @@ -239,6 +298,7 @@ fn parse_privilege_tables_from_args( fn edit_privileges_with_editor( privilege_data: &[DatabasePrivilegeRow], + // NOTE: this is only used for backwards compat with mysql-admtools database_name: Option<&MySQLDatabase>, ) -> anyhow::Result> { let unix_user = User::from_uid(getuid()) diff --git a/src/client/mysql_admutils_compatibility/mysql_dbadm.rs b/src/client/mysql_admutils_compatibility/mysql_dbadm.rs index 63c81fa..891c32e 100644 --- a/src/client/mysql_admutils_compatibility/mysql_dbadm.rs +++ b/src/client/mysql_admutils_compatibility/mysql_dbadm.rs @@ -208,14 +208,19 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh Command::Show(args) => show_databases(args, message_stream).await, Command::Editperm(args) => { let edit_privileges_args = EditPrivsArgs { - name: Some(args.database), + single_priv: None, privs: vec![], json: false, editor: None, yes: false, }; - edit_database_privileges(edit_privileges_args, message_stream).await + edit_database_privileges( + edit_privileges_args, + Some(args.database), + message_stream, + ) + .await } } }) diff --git a/src/core/database_privileges/cli.rs b/src/core/database_privileges/cli.rs index d045899..7052a17 100644 --- a/src/core/database_privileges/cli.rs +++ b/src/core/database_privileges/cli.rs @@ -1,9 +1,15 @@ //! This module contains serialization and deserialization logic for //! database privileges related CLI commands. +use itertools::Itertools; + use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff}; use crate::core::types::{MySQLDatabase, MySQLUser}; +const VALID_PRIVILEGE_EDIT_CHARS: &[char] = &[ + 's', 'i', 'u', 'd', 'c', 'D', 'a', 'A', 'I', 't', 'l', 'r', 'A', +]; + /// This enum represents a part of a CLI argument for editing database privileges, /// indicating whether privileges are to be added, set, or removed. #[derive(Debug, Clone, PartialEq, Eq)] @@ -13,17 +19,76 @@ pub enum DatabasePrivilegeEditEntryType { Remove, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DatabasePrivilegeEdit { + pub type_: DatabasePrivilegeEditEntryType, + pub privileges: Vec, +} + +impl DatabasePrivilegeEdit { + pub fn parse_from_str(input: &str) -> anyhow::Result { + let (edit_type, privs_str) = if let Some(privs_str) = input.strip_prefix('+') { + (DatabasePrivilegeEditEntryType::Add, privs_str) + } else if let Some(privs_str) = input.strip_prefix('-') { + (DatabasePrivilegeEditEntryType::Remove, privs_str) + } else { + (DatabasePrivilegeEditEntryType::Set, input) + }; + + let privileges: Vec = privs_str.chars().collect(); + + if privileges + .iter() + .any(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c)) + { + let invalid_chars: String = privileges + .iter() + .filter(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c)) + .map(|c| format!("'{c}'")) + .join(", "); + let valid_characters: String = VALID_PRIVILEGE_EDIT_CHARS + .iter() + .map(|c| format!("'{c}'")) + .join(", "); + anyhow::bail!( + "Invalid character(s) in privilege edit entry: {}\n\nValid characters are: {}", + invalid_chars, + valid_characters, + ); + } + + Ok(DatabasePrivilegeEdit { + type_: edit_type, + privileges, + }) + } +} + +impl std::fmt::Display for DatabasePrivilegeEdit { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.type_ { + DatabasePrivilegeEditEntryType::Add => write!(f, "+")?, + DatabasePrivilegeEditEntryType::Set => {} + DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?, + } + for priv_char in &self.privileges { + write!(f, "{}", priv_char)?; + } + + Ok(()) + } +} + /// This struct represents a single CLI argument for editing database privileges. /// /// This is typically parsed from a string looking like: /// -/// `[database_name:]username:[+|-]privileges` +/// `database_name:username:[+|-]privileges` #[derive(Debug, Clone, PartialEq, Eq)] pub struct DatabasePrivilegeEditEntry { - pub database: Option, + pub database: MySQLDatabase, pub user: MySQLUser, - pub type_: DatabasePrivilegeEditEntryType, - pub privileges: Vec, + pub privilege_edit: DatabasePrivilegeEdit, } impl DatabasePrivilegeEditEntry { @@ -31,80 +96,41 @@ impl DatabasePrivilegeEditEntry { /// /// The expected format is: /// - /// `[database_name:]username:[+|-]privileges` + /// `database_name:username:[+|-]privileges` /// /// where: - /// - database_name is optional, if omitted the entry applies to all databases + /// - database_name is the name of the database to edit privileges for /// - username is the name of the user to edit privileges for /// - privileges is a string of characters representing the privileges to add, set or remove /// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly /// - privileges characters are: siudcDaAItlrA - pub fn parse_from_str(arg: &str) -> anyhow::Result { + pub fn parse_from_str(arg: &str) -> anyhow::Result { let parts: Vec<&str> = arg.split(':').collect(); - if parts.len() < 2 || parts.len() > 3 { + if parts.len() != 3 { anyhow::bail!("Invalid privilege edit entry format: {}", arg); } - let (database, user, user_privs) = if parts.len() == 3 { - (Some(parts[0].to_string()), parts[1].to_string(), parts[2]) - } else { - (None, parts[0].to_string(), parts[1]) - }; + let (database, user, user_privs) = (parts[0].to_string(), parts[1].to_string(), parts[2]); if user.is_empty() { anyhow::bail!("Username cannot be empty in privilege edit entry: {}", arg); } - let (edit_type, privs_str) = if let Some(privs_str) = user_privs.strip_prefix('+') { - (DatabasePrivilegeEditEntryType::Add, privs_str) - } else if let Some(privs_str) = user_privs.strip_prefix('-') { - (DatabasePrivilegeEditEntryType::Remove, privs_str) - } else { - (DatabasePrivilegeEditEntryType::Set, user_privs) - }; - - let privileges: Vec = privs_str.chars().map(|c| c.to_string()).collect(); - if privileges.iter().any(|c| !"siudcDaAItlrA".contains(c)) { - let invalid_chars: String = privileges - .iter() - .filter(|c| !"siudcDaAItlrA".contains(c.as_str())) - .cloned() - .collect(); - anyhow::bail!( - "Invalid character(s) in privilege edit entry: {}", - invalid_chars - ); - } + let privilege_edit = DatabasePrivilegeEdit::parse_from_str(user_privs)?; Ok(DatabasePrivilegeEditEntry { - database: database.map(MySQLDatabase::from), + database: MySQLDatabase::from(database), user: MySQLUser::from(user), - type_: edit_type, - privileges, + privilege_edit, }) } - pub fn as_database_privileges_diff( - &self, - external_database_name: Option<&MySQLDatabase>, - ) -> anyhow::Result { - let database = match self.database.as_ref() { - Some(db) => db.clone(), - None => { - if let Some(external_db) = external_database_name { - external_db.clone() - } else { - anyhow::bail!( - "Database name must be specified either in the privilege edit entry or as an external argument." - ); - } - } - }; + pub fn as_database_privileges_diff(&self) -> anyhow::Result { let mut diff; - match self.type_ { + match self.privilege_edit.type_ { DatabasePrivilegeEditEntryType::Set => { diff = DatabasePrivilegeRowDiff { - db: database, + db: self.database.clone(), user: self.user.clone(), select_priv: Some(DatabasePrivilegeChange::YesToNo), insert_priv: Some(DatabasePrivilegeChange::YesToNo), @@ -118,20 +144,20 @@ impl DatabasePrivilegeEditEntry { lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo), references_priv: Some(DatabasePrivilegeChange::YesToNo), }; - for priv_char in &self.privileges { - match priv_char.as_str() { - "s" => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes), - "i" => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes), - "u" => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes), - "d" => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes), - "c" => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes), - "D" => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes), - "a" => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes), - "I" => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes), - "t" => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes), - "l" => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes), - "r" => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes), - "A" => { + for priv_char in &self.privilege_edit.privileges { + match priv_char { + 's' => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes), + 'i' => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes), + 'u' => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes), + 'd' => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes), + 'c' => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes), + 'D' => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes), + 'a' => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes), + 'I' => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes), + 't' => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes), + 'l' => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes), + 'r' => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes), + 'A' => { diff.select_priv = Some(DatabasePrivilegeChange::NoToYes); diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes); diff.update_priv = Some(DatabasePrivilegeChange::NoToYes); @@ -150,7 +176,7 @@ impl DatabasePrivilegeEditEntry { } DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => { diff = DatabasePrivilegeRowDiff { - db: database, + db: self.database.clone(), user: self.user.clone(), select_priv: None, insert_priv: None, @@ -164,25 +190,25 @@ impl DatabasePrivilegeEditEntry { lock_tables_priv: None, references_priv: None, }; - let value = match self.type_ { + let value = match self.privilege_edit.type_ { DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes, DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo, _ => unreachable!(), }; - for priv_char in &self.privileges { - match priv_char.as_str() { - "s" => diff.select_priv = Some(value), - "i" => diff.insert_priv = Some(value), - "u" => diff.update_priv = Some(value), - "d" => diff.delete_priv = Some(value), - "c" => diff.create_priv = Some(value), - "D" => diff.drop_priv = Some(value), - "a" => diff.alter_priv = Some(value), - "I" => diff.index_priv = Some(value), - "t" => diff.create_tmp_table_priv = Some(value), - "l" => diff.lock_tables_priv = Some(value), - "r" => diff.references_priv = Some(value), - "A" => { + for priv_char in &self.privilege_edit.privileges { + match priv_char { + 's' => diff.select_priv = Some(value), + 'i' => diff.insert_priv = Some(value), + 'u' => diff.update_priv = Some(value), + 'd' => diff.delete_priv = Some(value), + 'c' => diff.create_priv = Some(value), + 'D' => diff.drop_priv = Some(value), + 'a' => diff.alter_priv = Some(value), + 'I' => diff.index_priv = Some(value), + 't' => diff.create_tmp_table_priv = Some(value), + 'l' => diff.lock_tables_priv = Some(value), + 'r' => diff.references_priv = Some(value), + 'A' => { diff.select_priv = Some(value); diff.insert_priv = Some(value); diff.update_priv = Some(value); @@ -207,19 +233,9 @@ impl DatabasePrivilegeEditEntry { impl std::fmt::Display for DatabasePrivilegeEditEntry { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if let Some(db) = &self.database { - write!(f, "{}:, ", db)?; - } + write!(f, "{}:, ", self.database)?; write!(f, "{}: ", self.user)?; - match self.type_ { - DatabasePrivilegeEditEntryType::Add => write!(f, "+")?, - DatabasePrivilegeEditEntryType::Set => {} - DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?, - } - for priv_char in &self.privileges { - write!(f, "{}", priv_char)?; - } - + write!(f, "{}", self.privilege_edit)?; Ok(()) } } @@ -234,10 +250,12 @@ mod tests { assert_eq!( result.ok(), Some(DatabasePrivilegeEditEntry { - database: Some("db".into()), + database: "db".into(), user: "user".into(), - type_: DatabasePrivilegeEditEntryType::Set, - privileges: vec!["A".into()], + privilege_edit: DatabasePrivilegeEdit { + type_: DatabasePrivilegeEditEntryType::Set, + privileges: vec!['A'], + }, }) ); } @@ -248,10 +266,12 @@ mod tests { assert_eq!( result.ok(), Some(DatabasePrivilegeEditEntry { - database: Some("db".into()), + database: "db".into(), user: "user".into(), - type_: DatabasePrivilegeEditEntryType::Set, - privileges: vec![], + privilege_edit: DatabasePrivilegeEdit { + type_: DatabasePrivilegeEditEntryType::Set, + privileges: vec![], + }, }) ); } @@ -262,28 +282,16 @@ mod tests { assert_eq!( result.ok(), Some(DatabasePrivilegeEditEntry { - database: Some("db".into()), + database: "db".into(), user: "user".into(), - type_: DatabasePrivilegeEditEntryType::Set, - privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()], + privilege_edit: DatabasePrivilegeEdit { + type_: DatabasePrivilegeEditEntryType::Set, + privileges: vec!['s', 'i', 'u', 'd'], + }, }) ); } - #[test] - fn test_cli_arg_parse_set_user_nonexistent_misc() { - let result = DatabasePrivilegeEditEntry::parse_from_str("user:siud"); - assert_eq!( - result.ok(), - Some(DatabasePrivilegeEditEntry { - database: None, - user: "user".into(), - type_: DatabasePrivilegeEditEntryType::Set, - privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()], - }), - ); - } - #[test] fn test_cli_arg_parse_set_db_user_nonexistent_privilege() { let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F"); @@ -308,10 +316,12 @@ mod tests { assert_eq!( result.ok(), Some(DatabasePrivilegeEditEntry { - database: Some("db".into()), + database: "db".into(), user: "user".into(), - type_: DatabasePrivilegeEditEntryType::Add, - privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()], + privilege_edit: DatabasePrivilegeEdit { + type_: DatabasePrivilegeEditEntryType::Add, + privileges: vec!['s', 'i', 'u', 'd'], + }, }) ); } @@ -322,10 +332,12 @@ mod tests { assert_eq!( result.ok(), Some(DatabasePrivilegeEditEntry { - database: Some("db".into()), + database: "db".into(), user: "user".into(), - type_: DatabasePrivilegeEditEntryType::Remove, - privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()], + privilege_edit: DatabasePrivilegeEdit { + type_: DatabasePrivilegeEditEntryType::Remove, + privileges: vec!['s', 'i', 'u', 'd'], + }, }), ); } diff --git a/src/main.rs b/src/main.rs index 60aaaf4..6efcbc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -86,9 +86,11 @@ struct Args { #[command(subcommand)] command: Command, + // NOTE: be careful not to add short options that collide with the `edit-privs` privilege + // characters. It should in theory be possible for `edit-privs` to ignore any options + // specified here, but in practice clap is being difficult to work with. /// Path to the socket of the server, if it already exists. #[arg( - short, long, value_name = "PATH", value_hint = clap::ValueHint::FilePath, @@ -99,7 +101,6 @@ struct Args { /// Config file to use for the server. #[arg( - short, long, value_name = "PATH", value_hint = clap::ValueHint::FilePath,