client: rename and merge user/db command modules
This commit is contained in:
@@ -1,20 +0,0 @@
|
|||||||
use crate::core::protocol::Response;
|
|
||||||
|
|
||||||
pub fn erroneous_server_response(
|
|
||||||
response: Option<Result<Response, std::io::Error>>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
match response {
|
|
||||||
Some(Ok(Response::Error(e))) => {
|
|
||||||
anyhow::bail!("Server returned error: {}", e);
|
|
||||||
}
|
|
||||||
Some(Err(e)) => {
|
|
||||||
anyhow::bail!(e);
|
|
||||||
}
|
|
||||||
Some(response) => {
|
|
||||||
anyhow::bail!("Unexpected response from server: {:?}", response);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
anyhow::bail!("No response from server");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,425 +0,0 @@
|
|||||||
use anyhow::Context;
|
|
||||||
use clap::Parser;
|
|
||||||
use dialoguer::{Confirm, Password};
|
|
||||||
use futures_util::{SinkExt, StreamExt};
|
|
||||||
|
|
||||||
use crate::core::protocol::{
|
|
||||||
ClientToServerMessageStream, ListUsersError, MySQLUser, Request, Response,
|
|
||||||
print_create_users_output_status, print_create_users_output_status_json,
|
|
||||||
print_drop_users_output_status, print_drop_users_output_status_json,
|
|
||||||
print_lock_users_output_status, print_lock_users_output_status_json,
|
|
||||||
print_set_password_output_status, print_unlock_users_output_status,
|
|
||||||
print_unlock_users_output_status_json,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::common::erroneous_server_response;
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserArgs {
|
|
||||||
#[clap(subcommand)]
|
|
||||||
subcmd: UserCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub enum UserCommand {
|
|
||||||
/// Create one or more users
|
|
||||||
#[command()]
|
|
||||||
CreateUser(UserCreateArgs),
|
|
||||||
|
|
||||||
/// Delete one or more users
|
|
||||||
#[command()]
|
|
||||||
DropUser(UserDeleteArgs),
|
|
||||||
|
|
||||||
/// Change the MySQL password for a user
|
|
||||||
#[command()]
|
|
||||||
PasswdUser(UserPasswdArgs),
|
|
||||||
|
|
||||||
/// Print information about one or more users
|
|
||||||
///
|
|
||||||
/// If no username is provided, all users you have access will be shown.
|
|
||||||
#[command()]
|
|
||||||
ShowUser(UserShowArgs),
|
|
||||||
|
|
||||||
/// Lock account for one or more users
|
|
||||||
#[command()]
|
|
||||||
LockUser(UserLockArgs),
|
|
||||||
|
|
||||||
/// Unlock account for one or more users
|
|
||||||
#[command()]
|
|
||||||
UnlockUser(UserUnlockArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserCreateArgs {
|
|
||||||
#[arg(num_args = 1..)]
|
|
||||||
username: Vec<MySQLUser>,
|
|
||||||
|
|
||||||
/// Do not ask for a password, leave it unset
|
|
||||||
#[clap(long)]
|
|
||||||
no_password: bool,
|
|
||||||
|
|
||||||
/// Print the information as JSON
|
|
||||||
///
|
|
||||||
/// Note that this implies `--no-password`, since the command will become non-interactive.
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserDeleteArgs {
|
|
||||||
#[arg(num_args = 1..)]
|
|
||||||
username: Vec<MySQLUser>,
|
|
||||||
|
|
||||||
/// Print the information as JSON
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserPasswdArgs {
|
|
||||||
username: MySQLUser,
|
|
||||||
|
|
||||||
#[clap(short, long)]
|
|
||||||
password_file: Option<String>,
|
|
||||||
|
|
||||||
/// Print the information as JSON
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserShowArgs {
|
|
||||||
#[arg(num_args = 0..)]
|
|
||||||
username: Vec<MySQLUser>,
|
|
||||||
|
|
||||||
/// Print the information as JSON
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserLockArgs {
|
|
||||||
#[arg(num_args = 1..)]
|
|
||||||
username: Vec<MySQLUser>,
|
|
||||||
|
|
||||||
/// Print the information as JSON
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
pub struct UserUnlockArgs {
|
|
||||||
#[arg(num_args = 1..)]
|
|
||||||
username: Vec<MySQLUser>,
|
|
||||||
|
|
||||||
/// Print the information as JSON
|
|
||||||
#[arg(short, long)]
|
|
||||||
json: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_command(
|
|
||||||
command: UserCommand,
|
|
||||||
server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
match command {
|
|
||||||
UserCommand::CreateUser(args) => create_users(args, server_connection).await,
|
|
||||||
UserCommand::DropUser(args) => drop_users(args, server_connection).await,
|
|
||||||
UserCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
|
||||||
UserCommand::ShowUser(args) => show_users(args, server_connection).await,
|
|
||||||
UserCommand::LockUser(args) => lock_users(args, server_connection).await,
|
|
||||||
UserCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_users(
|
|
||||||
args: UserCreateArgs,
|
|
||||||
mut server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if args.username.is_empty() {
|
|
||||||
anyhow::bail!("No usernames provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = Request::CreateUsers(args.username.to_owned());
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(anyhow::Error::from(err).context("Failed to communicate with server"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::CreateUsers(result))) => result,
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
if args.json {
|
|
||||||
print_create_users_output_status_json(&result);
|
|
||||||
} else {
|
|
||||||
print_create_users_output_status(&result);
|
|
||||||
|
|
||||||
let successfully_created_users = result
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(username, result)| result.as_ref().ok().map(|_| username))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
for username in successfully_created_users {
|
|
||||||
if !args.no_password
|
|
||||||
&& Confirm::new()
|
|
||||||
.with_prompt(format!(
|
|
||||||
"Do you want to set a password for user '{}'?",
|
|
||||||
username
|
|
||||||
))
|
|
||||||
.default(false)
|
|
||||||
.interact()?
|
|
||||||
{
|
|
||||||
let password = read_password_from_stdin_with_double_check(username)?;
|
|
||||||
let message = Request::PasswdUser(username.to_owned(), password);
|
|
||||||
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
match server_connection.next().await {
|
|
||||||
Some(Ok(Response::PasswdUser(result))) => {
|
|
||||||
print_set_password_output_status(&result, username)
|
|
||||||
}
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn drop_users(
|
|
||||||
args: UserDeleteArgs,
|
|
||||||
mut server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if args.username.is_empty() {
|
|
||||||
anyhow::bail!("No usernames provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = Request::DropUsers(args.username.to_owned());
|
|
||||||
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::DropUsers(result))) => result,
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
|
|
||||||
if args.json {
|
|
||||||
print_drop_users_output_status_json(&result);
|
|
||||||
} else {
|
|
||||||
print_drop_users_output_status(&result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_password_from_stdin_with_double_check(username: &MySQLUser) -> anyhow::Result<String> {
|
|
||||||
Password::new()
|
|
||||||
.with_prompt(format!("New MySQL password for user '{}'", username))
|
|
||||||
.with_confirmation(
|
|
||||||
format!("Retype new MySQL password for user '{}'", username),
|
|
||||||
"Passwords do not match",
|
|
||||||
)
|
|
||||||
.interact()
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn passwd_user(
|
|
||||||
args: UserPasswdArgs,
|
|
||||||
mut server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
// TODO: create a "user" exists check" command
|
|
||||||
let message = Request::ListUsers(Some(vec![args.username.to_owned()]));
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
let response = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::ListUsers(users))) => users,
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
match response
|
|
||||||
.get(&args.username)
|
|
||||||
.unwrap_or(&Err(ListUsersError::UserDoesNotExist))
|
|
||||||
{
|
|
||||||
Ok(_) => {}
|
|
||||||
Err(err) => {
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!("{}", err.to_error_message(&args.username));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = if let Some(password_file) = args.password_file {
|
|
||||||
std::fs::read_to_string(password_file)
|
|
||||||
.context("Failed to read password file")?
|
|
||||||
.trim()
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
read_password_from_stdin_with_double_check(&args.username)?
|
|
||||||
};
|
|
||||||
|
|
||||||
let message = Request::PasswdUser(args.username.to_owned(), password);
|
|
||||||
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::PasswdUser(result))) => result,
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
|
|
||||||
print_set_password_output_status(&result, &args.username);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn show_users(
|
|
||||||
args: UserShowArgs,
|
|
||||||
mut server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let message = if args.username.is_empty() {
|
|
||||||
Request::ListUsers(None)
|
|
||||||
} else {
|
|
||||||
Request::ListUsers(Some(args.username.to_owned()))
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let users = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::ListUsers(users))) => users
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|(username, result)| match result {
|
|
||||||
Ok(user) => Some(user),
|
|
||||||
Err(err) => {
|
|
||||||
eprintln!("{}", err.to_error_message(&username));
|
|
||||||
eprintln!("Skipping...");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
Some(Ok(Response::ListAllUsers(users))) => match users {
|
|
||||||
Ok(users) => users,
|
|
||||||
Err(err) => {
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
return Err(
|
|
||||||
anyhow::anyhow!(err.to_error_message()).context("Failed to list all users")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
|
|
||||||
if args.json {
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
serde_json::to_string_pretty(&users).context("Failed to serialize users to JSON")?
|
|
||||||
);
|
|
||||||
} else if users.is_empty() {
|
|
||||||
println!("No users to show.");
|
|
||||||
} else {
|
|
||||||
let mut table = prettytable::Table::new();
|
|
||||||
table.add_row(row![
|
|
||||||
"User",
|
|
||||||
"Password is set",
|
|
||||||
"Locked",
|
|
||||||
"Databases where user has privileges"
|
|
||||||
]);
|
|
||||||
for user in users {
|
|
||||||
table.add_row(row![
|
|
||||||
user.user,
|
|
||||||
user.has_password,
|
|
||||||
user.is_locked,
|
|
||||||
user.databases.join("\n")
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
table.printstd();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn lock_users(
|
|
||||||
args: UserLockArgs,
|
|
||||||
mut server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if args.username.is_empty() {
|
|
||||||
anyhow::bail!("No usernames provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = Request::LockUsers(args.username.to_owned());
|
|
||||||
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::LockUsers(result))) => result,
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
|
|
||||||
if args.json {
|
|
||||||
print_lock_users_output_status_json(&result);
|
|
||||||
} else {
|
|
||||||
print_lock_users_output_status(&result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn unlock_users(
|
|
||||||
args: UserUnlockArgs,
|
|
||||||
mut server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
if args.username.is_empty() {
|
|
||||||
anyhow::bail!("No usernames provided");
|
|
||||||
}
|
|
||||||
|
|
||||||
let message = Request::UnlockUsers(args.username.to_owned());
|
|
||||||
|
|
||||||
if let Err(err) = server_connection.send(message).await {
|
|
||||||
server_connection.close().await.ok();
|
|
||||||
anyhow::bail!(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
let result = match server_connection.next().await {
|
|
||||||
Some(Ok(Response::UnlockUsers(result))) => result,
|
|
||||||
response => return erroneous_server_response(response),
|
|
||||||
};
|
|
||||||
|
|
||||||
server_connection.send(Request::Exit).await?;
|
|
||||||
|
|
||||||
if args.json {
|
|
||||||
print_unlock_users_output_status_json(&result);
|
|
||||||
} else {
|
|
||||||
print_unlock_users_output_status(&result);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
mod common;
|
pub mod command;
|
||||||
pub mod database_command;
|
|
||||||
pub mod user_command;
|
|
||||||
|
|
||||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
#[cfg(feature = "mysql-admutils-compatibility")]
|
||||||
pub mod mysql_admutils_compatibility;
|
pub mod mysql_admutils_compatibility;
|
||||||
@@ -2,13 +2,12 @@ use std::collections::BTreeSet;
|
|||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dialoguer::{Confirm, Editor};
|
use dialoguer::{Confirm, Editor, Password};
|
||||||
use futures_util::{SinkExt, StreamExt};
|
use futures_util::{SinkExt, StreamExt};
|
||||||
use nix::unistd::{User, getuid};
|
use nix::unistd::{User, getuid};
|
||||||
use prettytable::{Cell, Row, Table};
|
use prettytable::{Cell, Row, Table};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::common::erroneous_server_response,
|
|
||||||
core::{
|
core::{
|
||||||
common::yn,
|
common::yn,
|
||||||
database_privileges::{
|
database_privileges::{
|
||||||
@@ -19,17 +18,21 @@ use crate::{
|
|||||||
reduce_privilege_diffs,
|
reduce_privilege_diffs,
|
||||||
},
|
},
|
||||||
protocol::{
|
protocol::{
|
||||||
ClientToServerMessageStream, MySQLDatabase, Request, Response,
|
ClientToServerMessageStream, ListUsersError, MySQLDatabase, MySQLUser, Request,
|
||||||
print_create_databases_output_status, print_create_databases_output_status_json,
|
Response, print_create_databases_output_status,
|
||||||
print_drop_databases_output_status, print_drop_databases_output_status_json,
|
print_create_databases_output_status_json, print_create_users_output_status,
|
||||||
print_modify_database_privileges_output_status,
|
print_create_users_output_status_json, print_drop_databases_output_status,
|
||||||
|
print_drop_databases_output_status_json, print_drop_users_output_status,
|
||||||
|
print_drop_users_output_status_json, print_lock_users_output_status,
|
||||||
|
print_lock_users_output_status_json, print_modify_database_privileges_output_status,
|
||||||
|
print_set_password_output_status, print_unlock_users_output_status,
|
||||||
|
print_unlock_users_output_status_json,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
// #[command(next_help_heading = Some(DATABASE_COMMAND_HEADER))]
|
pub enum ClientCommand {
|
||||||
pub enum DatabaseCommand {
|
|
||||||
/// Create one or more databases
|
/// Create one or more databases
|
||||||
#[command()]
|
#[command()]
|
||||||
CreateDb(DatabaseCreateArgs),
|
CreateDb(DatabaseCreateArgs),
|
||||||
@@ -102,6 +105,32 @@ pub enum DatabaseCommand {
|
|||||||
///
|
///
|
||||||
#[command(verbatim_doc_comment)]
|
#[command(verbatim_doc_comment)]
|
||||||
EditDbPrivs(DatabaseEditPrivsArgs),
|
EditDbPrivs(DatabaseEditPrivsArgs),
|
||||||
|
|
||||||
|
/// Create one or more users
|
||||||
|
#[command()]
|
||||||
|
CreateUser(UserCreateArgs),
|
||||||
|
|
||||||
|
/// Delete one or more users
|
||||||
|
#[command()]
|
||||||
|
DropUser(UserDeleteArgs),
|
||||||
|
|
||||||
|
/// Change the MySQL password for a user
|
||||||
|
#[command()]
|
||||||
|
PasswdUser(UserPasswdArgs),
|
||||||
|
|
||||||
|
/// Print information about one or more users
|
||||||
|
///
|
||||||
|
/// If no username is provided, all users you have access will be shown.
|
||||||
|
#[command()]
|
||||||
|
ShowUser(UserShowArgs),
|
||||||
|
|
||||||
|
/// Lock account for one or more users
|
||||||
|
#[command()]
|
||||||
|
LockUser(UserLockArgs),
|
||||||
|
|
||||||
|
/// Unlock account for one or more users
|
||||||
|
#[command()]
|
||||||
|
UnlockUser(UserUnlockArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@@ -169,19 +198,108 @@ pub struct DatabaseEditPrivsArgs {
|
|||||||
pub yes: bool,
|
pub yes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct UserCreateArgs {
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
username: Vec<MySQLUser>,
|
||||||
|
|
||||||
|
/// Do not ask for a password, leave it unset
|
||||||
|
#[clap(long)]
|
||||||
|
no_password: bool,
|
||||||
|
|
||||||
|
/// Print the information as JSON
|
||||||
|
///
|
||||||
|
/// Note that this implies `--no-password`, since the command will become non-interactive.
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct UserDeleteArgs {
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
username: Vec<MySQLUser>,
|
||||||
|
|
||||||
|
/// Print the information as JSON
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct UserPasswdArgs {
|
||||||
|
username: MySQLUser,
|
||||||
|
|
||||||
|
#[clap(short, long)]
|
||||||
|
password_file: Option<String>,
|
||||||
|
|
||||||
|
/// Print the information as JSON
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct UserShowArgs {
|
||||||
|
#[arg(num_args = 0..)]
|
||||||
|
username: Vec<MySQLUser>,
|
||||||
|
|
||||||
|
/// Print the information as JSON
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct UserLockArgs {
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
username: Vec<MySQLUser>,
|
||||||
|
|
||||||
|
/// Print the information as JSON
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug, Clone)]
|
||||||
|
pub struct UserUnlockArgs {
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
username: Vec<MySQLUser>,
|
||||||
|
|
||||||
|
/// Print the information as JSON
|
||||||
|
#[arg(short, long)]
|
||||||
|
json: bool,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_command(
|
pub async fn handle_command(
|
||||||
command: DatabaseCommand,
|
command: ClientCommand,
|
||||||
server_connection: ClientToServerMessageStream,
|
server_connection: ClientToServerMessageStream,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
match command {
|
match command {
|
||||||
DatabaseCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
ClientCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
||||||
DatabaseCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
||||||
DatabaseCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
||||||
DatabaseCommand::ShowDbPrivs(args) => {
|
ClientCommand::ShowDbPrivs(args) => show_database_privileges(args, server_connection).await,
|
||||||
show_database_privileges(args, server_connection).await
|
ClientCommand::EditDbPrivs(args) => edit_database_privileges(args, 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,
|
||||||
|
ClientCommand::ShowUser(args) => show_users(args, server_connection).await,
|
||||||
|
ClientCommand::LockUser(args) => lock_users(args, server_connection).await,
|
||||||
|
ClientCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn erroneous_server_response(
|
||||||
|
response: Option<Result<Response, std::io::Error>>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match response {
|
||||||
|
Some(Ok(Response::Error(e))) => {
|
||||||
|
anyhow::bail!("Server returned error: {}", e);
|
||||||
}
|
}
|
||||||
DatabaseCommand::EditDbPrivs(args) => {
|
Some(Err(e)) => {
|
||||||
edit_database_privileges(args, server_connection).await
|
anyhow::bail!(e);
|
||||||
|
}
|
||||||
|
Some(response) => {
|
||||||
|
anyhow::bail!("Unexpected response from server: {:?}", response);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
anyhow::bail!("No response from server");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -487,3 +605,295 @@ fn edit_privileges_with_editor(
|
|||||||
.context("Could not parse privilege data from editor"),
|
.context("Could not parse privilege data from editor"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn create_users(
|
||||||
|
args: UserCreateArgs,
|
||||||
|
mut server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if args.username.is_empty() {
|
||||||
|
anyhow::bail!("No usernames provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = Request::CreateUsers(args.username.to_owned());
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(anyhow::Error::from(err).context("Failed to communicate with server"));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::CreateUsers(result))) => result,
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
print_create_users_output_status_json(&result);
|
||||||
|
} else {
|
||||||
|
print_create_users_output_status(&result);
|
||||||
|
|
||||||
|
let successfully_created_users = result
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(username, result)| result.as_ref().ok().map(|_| username))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
for username in successfully_created_users {
|
||||||
|
if !args.no_password
|
||||||
|
&& Confirm::new()
|
||||||
|
.with_prompt(format!(
|
||||||
|
"Do you want to set a password for user '{}'?",
|
||||||
|
username
|
||||||
|
))
|
||||||
|
.default(false)
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
let password = read_password_from_stdin_with_double_check(username)?;
|
||||||
|
let message = Request::PasswdUser(username.to_owned(), password);
|
||||||
|
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
match server_connection.next().await {
|
||||||
|
Some(Ok(Response::PasswdUser(result))) => {
|
||||||
|
print_set_password_output_status(&result, username)
|
||||||
|
}
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn drop_users(
|
||||||
|
args: UserDeleteArgs,
|
||||||
|
mut server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if args.username.is_empty() {
|
||||||
|
anyhow::bail!("No usernames provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = Request::DropUsers(args.username.to_owned());
|
||||||
|
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::DropUsers(result))) => result,
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
print_drop_users_output_status_json(&result);
|
||||||
|
} else {
|
||||||
|
print_drop_users_output_status(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_password_from_stdin_with_double_check(username: &MySQLUser) -> anyhow::Result<String> {
|
||||||
|
Password::new()
|
||||||
|
.with_prompt(format!("New MySQL password for user '{}'", username))
|
||||||
|
.with_confirmation(
|
||||||
|
format!("Retype new MySQL password for user '{}'", username),
|
||||||
|
"Passwords do not match",
|
||||||
|
)
|
||||||
|
.interact()
|
||||||
|
.map_err(Into::into)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn passwd_user(
|
||||||
|
args: UserPasswdArgs,
|
||||||
|
mut server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// TODO: create a "user" exists check" command
|
||||||
|
let message = Request::ListUsers(Some(vec![args.username.to_owned()]));
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
let response = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::ListUsers(users))) => users,
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
match response
|
||||||
|
.get(&args.username)
|
||||||
|
.unwrap_or(&Err(ListUsersError::UserDoesNotExist))
|
||||||
|
{
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(err) => {
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!("{}", err.to_error_message(&args.username));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = if let Some(password_file) = args.password_file {
|
||||||
|
std::fs::read_to_string(password_file)
|
||||||
|
.context("Failed to read password file")?
|
||||||
|
.trim()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
read_password_from_stdin_with_double_check(&args.username)?
|
||||||
|
};
|
||||||
|
|
||||||
|
let message = Request::PasswdUser(args.username.to_owned(), password);
|
||||||
|
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::PasswdUser(result))) => result,
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
|
||||||
|
print_set_password_output_status(&result, &args.username);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn show_users(
|
||||||
|
args: UserShowArgs,
|
||||||
|
mut server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let message = if args.username.is_empty() {
|
||||||
|
Request::ListUsers(None)
|
||||||
|
} else {
|
||||||
|
Request::ListUsers(Some(args.username.to_owned()))
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let users = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::ListUsers(users))) => users
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|(username, result)| match result {
|
||||||
|
Ok(user) => Some(user),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{}", err.to_error_message(&username));
|
||||||
|
eprintln!("Skipping...");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
Some(Ok(Response::ListAllUsers(users))) => match users {
|
||||||
|
Ok(users) => users,
|
||||||
|
Err(err) => {
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
return Err(
|
||||||
|
anyhow::anyhow!(err.to_error_message()).context("Failed to list all users")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&users).context("Failed to serialize users to JSON")?
|
||||||
|
);
|
||||||
|
} else if users.is_empty() {
|
||||||
|
println!("No users to show.");
|
||||||
|
} else {
|
||||||
|
let mut table = prettytable::Table::new();
|
||||||
|
table.add_row(row![
|
||||||
|
"User",
|
||||||
|
"Password is set",
|
||||||
|
"Locked",
|
||||||
|
"Databases where user has privileges"
|
||||||
|
]);
|
||||||
|
for user in users {
|
||||||
|
table.add_row(row![
|
||||||
|
user.user,
|
||||||
|
user.has_password,
|
||||||
|
user.is_locked,
|
||||||
|
user.databases.join("\n")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
table.printstd();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn lock_users(
|
||||||
|
args: UserLockArgs,
|
||||||
|
mut server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if args.username.is_empty() {
|
||||||
|
anyhow::bail!("No usernames provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = Request::LockUsers(args.username.to_owned());
|
||||||
|
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::LockUsers(result))) => result,
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
print_lock_users_output_status_json(&result);
|
||||||
|
} else {
|
||||||
|
print_lock_users_output_status(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlock_users(
|
||||||
|
args: UserUnlockArgs,
|
||||||
|
mut server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if args.username.is_empty() {
|
||||||
|
anyhow::bail!("No usernames provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let message = Request::UnlockUsers(args.username.to_owned());
|
||||||
|
|
||||||
|
if let Err(err) = server_connection.send(message).await {
|
||||||
|
server_connection.close().await.ok();
|
||||||
|
anyhow::bail!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = match server_connection.next().await {
|
||||||
|
Some(Ok(Response::UnlockUsers(result))) => result,
|
||||||
|
response => return erroneous_server_response(response),
|
||||||
|
};
|
||||||
|
|
||||||
|
server_connection.send(Request::Exit).await?;
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
print_unlock_users_output_status_json(&result);
|
||||||
|
} else {
|
||||||
|
print_unlock_users_output_status(&result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -5,9 +5,8 @@ use std::path::PathBuf;
|
|||||||
use tokio::net::UnixStream as TokioUnixStream;
|
use tokio::net::UnixStream as TokioUnixStream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
client::{
|
||||||
common::erroneous_server_response,
|
command::{DatabaseEditPrivsArgs, edit_database_privileges, erroneous_server_response},
|
||||||
database_command,
|
|
||||||
mysql_admutils_compatibility::{
|
mysql_admutils_compatibility::{
|
||||||
common::trim_db_name_to_32_chars,
|
common::trim_db_name_to_32_chars,
|
||||||
error_messages::{
|
error_messages::{
|
||||||
@@ -187,7 +186,7 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
|||||||
Command::Drop(args) => drop_databases(args, message_stream).await,
|
Command::Drop(args) => drop_databases(args, message_stream).await,
|
||||||
Command::Show(args) => show_databases(args, message_stream).await,
|
Command::Show(args) => show_databases(args, message_stream).await,
|
||||||
Command::Editperm(args) => {
|
Command::Editperm(args) => {
|
||||||
let edit_privileges_args = database_command::DatabaseEditPrivsArgs {
|
let edit_privileges_args = DatabaseEditPrivsArgs {
|
||||||
name: Some(args.database),
|
name: Some(args.database),
|
||||||
privs: vec![],
|
privs: vec![],
|
||||||
json: false,
|
json: false,
|
||||||
@@ -195,8 +194,7 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
|||||||
yes: false,
|
yes: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
database_command::edit_database_privileges(edit_privileges_args, message_stream)
|
edit_database_privileges(edit_privileges_args, message_stream).await
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -6,15 +6,13 @@ use std::os::unix::net::UnixStream as StdUnixStream;
|
|||||||
use tokio::net::UnixStream as TokioUnixStream;
|
use tokio::net::UnixStream as TokioUnixStream;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cli::{
|
client::{
|
||||||
common::erroneous_server_response,
|
command::{erroneous_server_response, read_password_from_stdin_with_double_check}, mysql_admutils_compatibility::{
|
||||||
mysql_admutils_compatibility::{
|
|
||||||
common::trim_user_name_to_32_chars,
|
common::trim_user_name_to_32_chars,
|
||||||
error_messages::{
|
error_messages::{
|
||||||
handle_create_user_error, handle_drop_user_error, handle_list_users_error,
|
handle_create_user_error, handle_drop_user_error, handle_list_users_error,
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
user_command::read_password_from_stdin_with_double_check,
|
|
||||||
},
|
},
|
||||||
core::{
|
core::{
|
||||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||||
16
src/main.rs
16
src/main.rs
@@ -23,11 +23,11 @@ use crate::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
#[cfg(feature = "mysql-admutils-compatibility")]
|
||||||
use crate::cli::mysql_admutils_compatibility::{mysql_dbadm, mysql_useradm};
|
use crate::client::mysql_admutils_compatibility::{mysql_dbadm, mysql_useradm};
|
||||||
|
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
mod cli;
|
mod client;
|
||||||
mod core;
|
mod core;
|
||||||
|
|
||||||
#[cfg(feature = "tui")]
|
#[cfg(feature = "tui")]
|
||||||
@@ -77,10 +77,7 @@ struct Args {
|
|||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
enum Command {
|
enum Command {
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
Db(cli::database_command::DatabaseCommand),
|
Client(client::command::ClientCommand),
|
||||||
|
|
||||||
#[command(flatten)]
|
|
||||||
User(cli::user_command::UserCommand),
|
|
||||||
|
|
||||||
#[command(hide = true)]
|
#[command(hide = true)]
|
||||||
Server(server::command::ServerArgs),
|
Server(server::command::ServerArgs),
|
||||||
@@ -241,11 +238,8 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
|||||||
}
|
}
|
||||||
|
|
||||||
match command {
|
match command {
|
||||||
Command::User(user_args) => {
|
Command::Client(client_args) => {
|
||||||
cli::user_command::handle_command(user_args, message_stream).await
|
client::command::handle_command(client_args, message_stream).await
|
||||||
}
|
|
||||||
Command::Db(db_args) => {
|
|
||||||
cli::database_command::handle_command(db_args, message_stream).await
|
|
||||||
}
|
}
|
||||||
Command::Server(_) => unreachable!(),
|
Command::Server(_) => unreachable!(),
|
||||||
Command::GenerateCompletions(_) => unreachable!(),
|
Command::GenerateCompletions(_) => unreachable!(),
|
||||||
|
|||||||
Reference in New Issue
Block a user