client: split commands into separate files
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, MySQLDatabase, Request, Response,
|
||||
print_create_databases_output_status, print_create_databases_output_status_json,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct CreateDbArgs {
|
||||
/// The name of the database(s) to create
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn create_databases(
|
||||
args: CreateDbArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.name.is_empty() {
|
||||
anyhow::bail!("No database names provided");
|
||||
}
|
||||
|
||||
let message = Request::CreateDatabases(args.name.to_owned());
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CreateDatabases(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_create_databases_output_status_json(&result);
|
||||
} else {
|
||||
print_create_databases_output_status(&result);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
use clap::Parser;
|
||||
use dialoguer::Confirm;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, read_password_from_stdin_with_double_check},
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, MySQLUser, Request, Response,
|
||||
print_create_users_output_status, print_create_users_output_status_json,
|
||||
print_set_password_output_status,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct CreateUserArgs {
|
||||
#[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,
|
||||
}
|
||||
|
||||
pub async fn create_users(
|
||||
args: CreateUserArgs,
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, MySQLDatabase, Request, Response,
|
||||
print_drop_databases_output_status, print_drop_databases_output_status_json,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DropDbArgs {
|
||||
/// The name of the database(s) to drop
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn drop_databases(
|
||||
args: DropDbArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.name.is_empty() {
|
||||
anyhow::bail!("No database names provided");
|
||||
}
|
||||
|
||||
let message = Request::DropDatabases(args.name.to_owned());
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::DropDatabases(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_drop_databases_output_status_json(&result);
|
||||
} else {
|
||||
print_drop_databases_output_status(&result);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, MySQLUser, Request, Response, print_drop_users_output_status,
|
||||
print_drop_users_output_status_json,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DropUserArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn drop_users(
|
||||
args: DropUserArgs,
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::{Confirm, Editor};
|
||||
use futures_util::SinkExt;
|
||||
use nix::unistd::{User, getuid};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
database_privileges::{
|
||||
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::{
|
||||
ClientToServerMessageStream, MySQLDatabase, Request, Response,
|
||||
print_modify_database_privileges_output_status,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct EditDbPrivsArgs {
|
||||
/// The name of the database to edit privileges for
|
||||
pub name: Option<MySQLDatabase>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "[DATABASE:]USER:[+-]PRIVILEGES",
|
||||
num_args = 0..,
|
||||
value_parser = DatabasePrivilegeEditEntry::parse_from_str,
|
||||
)]
|
||||
pub privs: Vec<DatabasePrivilegeEditEntry>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
pub json: bool,
|
||||
|
||||
/// Specify the text editor to use for editing privileges
|
||||
#[arg(short, long)]
|
||||
pub editor: Option<String>,
|
||||
|
||||
/// Disable interactive confirmation before saving changes
|
||||
#[arg(short, long)]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
pub async fn edit_database_privileges(
|
||||
args: EditDbPrivsArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let message = Request::ListPrivileges(args.name.to_owned().map(|name| vec![name]));
|
||||
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let existing_privilege_rows = match server_connection.next().await {
|
||||
Some(Ok(Response::ListPrivileges(databases))) => databases
|
||||
.into_iter()
|
||||
.filter_map(|(database_name, result)| match result {
|
||||
Ok(privileges) => Some(privileges),
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(&database_name));
|
||||
eprintln!("Skipping...");
|
||||
println!();
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>(),
|
||||
Some(Ok(Response::ListAllPrivileges(privilege_rows))) => match privilege_rows {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Err(anyhow::anyhow!(err.to_error_message())
|
||||
.context("Failed to list database privileges"));
|
||||
}
|
||||
},
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
let diffs: BTreeSet<DatabasePrivilegesDiff> = if !args.privs.is_empty() {
|
||||
let privileges_to_change = parse_privilege_tables_from_args(&args)?;
|
||||
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())?;
|
||||
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
||||
};
|
||||
let diffs = reduce_privilege_diffs(&existing_privilege_rows, diffs)?;
|
||||
|
||||
if diffs.is_empty() {
|
||||
println!("No changes to make.");
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("The following changes will be made:\n");
|
||||
println!("{}", display_privilege_diffs(&diffs));
|
||||
|
||||
if !args.yes
|
||||
&& !Confirm::new()
|
||||
.with_prompt("Do you want to apply these changes?")
|
||||
.default(false)
|
||||
.show_default(true)
|
||||
.interact()?
|
||||
{
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let message = Request::ModifyPrivileges(diffs);
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::ModifyPrivileges(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
print_modify_database_privileges_output_status(&result);
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_privilege_tables_from_args(
|
||||
args: &EditDbPrivsArgs,
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>> {
|
||||
debug_assert!(!args.privs.is_empty());
|
||||
args.privs
|
||||
.iter()
|
||||
.map(|priv_edit_entry| {
|
||||
priv_edit_entry
|
||||
.as_database_privileges_diff(args.name.as_ref())
|
||||
.context(format!(
|
||||
"Failed parsing database privileges: `{}`",
|
||||
priv_edit_entry
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>>>()
|
||||
}
|
||||
|
||||
fn edit_privileges_with_editor(
|
||||
privilege_data: &[DatabasePrivilegeRow],
|
||||
database_name: Option<&MySQLDatabase>,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
let unix_user = User::from_uid(getuid())
|
||||
.context("Failed to look up your UNIX username")
|
||||
.and_then(|u| u.ok_or(anyhow::anyhow!("Failed to look up your UNIX username")))?;
|
||||
|
||||
let editor_content =
|
||||
generate_editor_content_from_privilege_data(privilege_data, &unix_user.name, database_name);
|
||||
|
||||
// TODO: handle errors better here
|
||||
let result = Editor::new().extension("tsv").edit(&editor_content)?;
|
||||
|
||||
match result {
|
||||
None => Ok(privilege_data.to_vec()),
|
||||
Some(result) => parse_privilege_data_from_editor_content(result)
|
||||
.context("Could not parse privilege data from editor"),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, MySQLUser, Request, Response, print_lock_users_output_status,
|
||||
print_lock_users_output_status_json,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct LockUserArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn lock_users(
|
||||
args: LockUserArgs,
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::Password;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, ListUsersError, MySQLUser, Request, Response,
|
||||
print_set_password_output_status,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct PasswdUserArgs {
|
||||
username: MySQLUser,
|
||||
|
||||
#[clap(short, long)]
|
||||
password_file: Option<String>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
pub async fn passwd_user(
|
||||
args: PasswdUserArgs,
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{ClientToServerMessageStream, MySQLDatabase, Request, Response},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowDbArgs {
|
||||
/// The name of the database(s) to show
|
||||
#[arg(num_args = 0..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn show_databases(
|
||||
args: ShowDbArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let message = if args.name.is_empty() {
|
||||
Request::ListDatabases(None)
|
||||
} else {
|
||||
Request::ListDatabases(Some(args.name.to_owned()))
|
||||
};
|
||||
|
||||
server_connection.send(message).await?;
|
||||
|
||||
// TODO: collect errors for json output.
|
||||
|
||||
let database_list = match server_connection.next().await {
|
||||
Some(Ok(Response::ListDatabases(databases))) => databases
|
||||
.into_iter()
|
||||
.filter_map(|(database_name, result)| match result {
|
||||
Ok(database_row) => Some(database_row),
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(&database_name));
|
||||
eprintln!("Skipping...");
|
||||
println!();
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Some(Ok(Response::ListAllDatabases(database_list))) => match database_list {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Err(
|
||||
anyhow::anyhow!(err.to_error_message()).context("Failed to list databases")
|
||||
);
|
||||
}
|
||||
},
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
println!("{}", serde_json::to_string_pretty(&database_list)?);
|
||||
} else if database_list.is_empty() {
|
||||
println!("No databases to show.");
|
||||
} else {
|
||||
let mut table = Table::new();
|
||||
table.add_row(Row::new(vec![Cell::new("Database")]));
|
||||
for db in database_list {
|
||||
table.add_row(row![db.database]);
|
||||
}
|
||||
table.printstd();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
common::yn,
|
||||
database_privileges::{DATABASE_PRIVILEGE_FIELDS, db_priv_field_human_readable_name},
|
||||
protocol::{ClientToServerMessageStream, MySQLDatabase, Request, Response},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowDbPrivsArgs {
|
||||
/// The name of the database(s) to show
|
||||
#[arg(num_args = 0..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn show_database_privileges(
|
||||
args: ShowDbPrivsArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let message = if args.name.is_empty() {
|
||||
Request::ListPrivileges(None)
|
||||
} else {
|
||||
Request::ListPrivileges(Some(args.name.to_owned()))
|
||||
};
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let privilege_data = match server_connection.next().await {
|
||||
Some(Ok(Response::ListPrivileges(databases))) => databases
|
||||
.into_iter()
|
||||
.filter_map(|(database_name, result)| match result {
|
||||
Ok(privileges) => Some(privileges),
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(&database_name));
|
||||
eprintln!("Skipping...");
|
||||
println!();
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect::<Vec<_>>(),
|
||||
Some(Ok(Response::ListAllPrivileges(privilege_rows))) => match privilege_rows {
|
||||
Ok(list) => list,
|
||||
Err(err) => {
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Err(anyhow::anyhow!(err.to_error_message())
|
||||
.context("Failed to list database privileges"));
|
||||
}
|
||||
},
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
println!("{}", serde_json::to_string_pretty(&privilege_data)?);
|
||||
} else if privilege_data.is_empty() {
|
||||
println!("No database privileges to show.");
|
||||
} else {
|
||||
let mut table = Table::new();
|
||||
table.add_row(Row::new(
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.map(db_priv_field_human_readable_name)
|
||||
.map(|name| Cell::new(&name))
|
||||
.collect(),
|
||||
));
|
||||
|
||||
for row in privilege_data {
|
||||
table.add_row(row![
|
||||
row.db,
|
||||
row.user,
|
||||
c->yn(row.select_priv),
|
||||
c->yn(row.insert_priv),
|
||||
c->yn(row.update_priv),
|
||||
c->yn(row.delete_priv),
|
||||
c->yn(row.create_priv),
|
||||
c->yn(row.drop_priv),
|
||||
c->yn(row.alter_priv),
|
||||
c->yn(row.index_priv),
|
||||
c->yn(row.create_tmp_table_priv),
|
||||
c->yn(row.lock_tables_priv),
|
||||
c->yn(row.references_priv),
|
||||
]);
|
||||
}
|
||||
table.printstd();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{ClientToServerMessageStream, MySQLUser, Request, Response},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowUserArgs {
|
||||
#[arg(num_args = 0..)]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn show_users(
|
||||
args: ShowUserArgs,
|
||||
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(())
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use clap::Parser;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::protocol::{
|
||||
ClientToServerMessageStream, MySQLUser, Request, Response,
|
||||
print_unlock_users_output_status, print_unlock_users_output_status_json,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UnlockUserArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
pub async fn unlock_users(
|
||||
args: UnlockUserArgs,
|
||||
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(())
|
||||
}
|
||||
Reference in New Issue
Block a user