WIP
This commit is contained in:
@@ -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... | <DB_NAME> <USER_NAME> <[+-]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,
|
||||
|
||||
@@ -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<MySQLDatabase>,
|
||||
|
||||
/// 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<DatabasePrivilegeEditEntry>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub single_priv: Option<SinglePrivilegeEditEntry>,
|
||||
|
||||
/// 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<MySQLDatabase>,
|
||||
|
||||
/// 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<MySQLUser>,
|
||||
|
||||
// TODO add a proper parser for this
|
||||
/// The privileges to set, grant or revoke
|
||||
#[arg(
|
||||
value_name = "[+-]PRIVILEGES",
|
||||
)]
|
||||
pub single_priv: Option<String>,
|
||||
}
|
||||
|
||||
async fn users_exist(
|
||||
server_connection: &mut ClientToServerMessageStream,
|
||||
privilege_diff: &BTreeSet<DatabasePrivilegesDiff>,
|
||||
@@ -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<MySQLDatabase>,
|
||||
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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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<MySQLDatabase>,
|
||||
pub database: MySQLDatabase,
|
||||
pub user: MySQLUser,
|
||||
pub type_: DatabasePrivilegeEditEntryType,
|
||||
pub privileges: Vec<String>,
|
||||
@@ -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<DatabasePrivilegeEditEntry> {
|
||||
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<DatabasePrivilegeRowDiff> {
|
||||
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<DatabasePrivilegeRowDiff> {
|
||||
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()],
|
||||
|
||||
Reference in New Issue
Block a user