diff --git a/src/client/commands.rs b/src/client/commands.rs index 7f44f35..29b3cd8 100644 --- a/src/client/commands.rs +++ b/src/client/commands.rs @@ -50,6 +50,7 @@ pub enum ClientCommand { /// If no database names are provided, all databases you have access to will be shown. ShowPrivs(ShowPrivsArgs), + // TODO: rewrite doc comment to match new CLI /// Change user privileges for one or more databases. See `edit-privs --help` for details. /// /// This command has two modes of operation: @@ -108,7 +109,10 @@ pub enum ClientCommand { /// /// `muscl edit-privs my_db -p my_user:+d /// - #[command(verbatim_doc_comment)] + #[command( + verbatim_doc_comment, + override_usage = "muscl edit-privs [OPTIONS] [ -p DATABASE:USER:[+-]PRIVILEGES... | <[+-]PRIVILEGES> ]" + )] EditPrivs(EditPrivsArgs), /// Create one or more users @@ -142,7 +146,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..a2448b6 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,7 +11,7 @@ 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, @@ -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 `DB_NAME`, `USER_NAME` and `PRIVILEGES` arguments. #[arg( short, long, - value_name = "[DATABASE:]USER:[+-]PRIVILEGES", + value_name = "DATABASE:USER:[+-]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,30 @@ pub struct EditPrivsArgs { pub yes: bool, } +#[derive(Args, Debug, Clone)] +pub struct SinglePrivilegeEditEntry { + /// 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, + + // TODO add a proper parser for this + /// The privileges to set, grant or revoke + #[arg( + value_name = "[+-]PRIVILEGES", + )] + pub single_priv: Option, +} + async fn users_exist( server_connection: &mut ClientToServerMessageStream, privilege_diff: &BTreeSet, @@ -120,12 +148,43 @@ 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])); + // TODO: handle args properly + 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 db_name = single_priv_entry.db_name.clone().or(use_database.clone()); + let user_name = 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 priv_entry = 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: db_name.ok_or_else(|| { + anyhow::anyhow!( + "DB_NAME must be specified when editing privileges in single privilege mode" + ) + })?, + user: user_name, + privileges: priv_entry.privileges, + }] + } else { + args.privs.clone() + }; + let existing_privilege_rows = match server_connection.next().await { Some(Ok(Response::ListPrivileges(databases))) => databases .into_iter() @@ -156,7 +215,7 @@ pub async fn edit_database_privileges( 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) }; @@ -228,7 +287,7 @@ fn parse_privilege_tables_from_args( .iter() .map(|priv_edit_entry| { priv_edit_entry - .as_database_privileges_diff(args.name.as_ref()) + .as_database_privileges_diff(None) .context(format!( "Failed parsing database privileges: `{}`", priv_edit_entry 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..f1af7b1 100644 --- a/src/core/database_privileges/cli.rs +++ b/src/core/database_privileges/cli.rs @@ -17,10 +17,10 @@ pub enum DatabasePrivilegeEditEntryType { /// /// 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, @@ -31,25 +31,21 @@ 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 { 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); @@ -77,34 +73,19 @@ impl DatabasePrivilegeEditEntry { } Ok(DatabasePrivilegeEditEntry { - database: database.map(MySQLDatabase::from), + database: MySQLDatabase::from(database), user: MySQLUser::from(user), type_: edit_type, privileges, }) } - 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_ { DatabasePrivilegeEditEntryType::Set => { diff = DatabasePrivilegeRowDiff { - db: database, + db: self.database.clone(), user: self.user.clone(), select_priv: Some(DatabasePrivilegeChange::YesToNo), insert_priv: Some(DatabasePrivilegeChange::YesToNo), @@ -150,7 +131,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, @@ -207,9 +188,7 @@ 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, "+")?, @@ -234,7 +213,7 @@ 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()], @@ -248,7 +227,7 @@ mod tests { assert_eq!( result.ok(), Some(DatabasePrivilegeEditEntry { - database: Some("db".into()), + database: "db".into(), user: "user".into(), type_: DatabasePrivilegeEditEntryType::Set, privileges: vec![], @@ -262,7 +241,7 @@ 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()], @@ -270,20 +249,6 @@ mod tests { ); } - #[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,7 +273,7 @@ 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()], @@ -322,7 +287,7 @@ 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()],