client: rename and merge user/db command modules
This commit is contained in:
899
src/client/command.rs
Normal file
899
src/client/command.rs
Normal file
@@ -0,0 +1,899 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::{Confirm, Editor, Password};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use nix::unistd::{User, getuid};
|
||||
use prettytable::{Cell, Row, Table};
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
common::yn,
|
||||
database_privileges::{
|
||||
DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeEditEntry, DatabasePrivilegeRow,
|
||||
DatabasePrivilegeRowDiff, DatabasePrivilegesDiff, create_or_modify_privilege_rows,
|
||||
db_priv_field_human_readable_name, diff_privileges, display_privilege_diffs,
|
||||
generate_editor_content_from_privilege_data, parse_privilege_data_from_editor_content,
|
||||
reduce_privilege_diffs,
|
||||
},
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListUsersError, MySQLDatabase, MySQLUser, Request,
|
||||
Response, print_create_databases_output_status,
|
||||
print_create_databases_output_status_json, print_create_users_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)]
|
||||
pub enum ClientCommand {
|
||||
/// Create one or more databases
|
||||
#[command()]
|
||||
CreateDb(DatabaseCreateArgs),
|
||||
|
||||
/// Delete one or more databases
|
||||
#[command()]
|
||||
DropDb(DatabaseDropArgs),
|
||||
|
||||
/// Print information about one or more databases
|
||||
///
|
||||
/// If no database name is provided, all databases you have access will be shown.
|
||||
#[command()]
|
||||
ShowDb(DatabaseShowArgs),
|
||||
|
||||
/// Print user privileges for one or more databases
|
||||
///
|
||||
/// If no database names are provided, all databases you have access to will be shown.
|
||||
#[command()]
|
||||
ShowDbPrivs(DatabaseShowPrivsArgs),
|
||||
|
||||
/// Change user privileges for one or more databases. See `edit-db-privs --help` for details.
|
||||
///
|
||||
/// This command has two modes of operation:
|
||||
///
|
||||
/// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the privileges using a text editor.
|
||||
///
|
||||
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
||||
///
|
||||
/// Follow the instructions inside the editor for more information.
|
||||
///
|
||||
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write privileges using arguments.
|
||||
///
|
||||
/// The privilege arguments should be formatted as `<db>:<user>+<privileges>-<privileges>`
|
||||
/// where the privileges are a string of characters, each representing a single privilege.
|
||||
/// The character `A` is an exception - it represents all privileges.
|
||||
///
|
||||
/// The character-to-privilege mapping is defined as follows:
|
||||
///
|
||||
/// - `s` - SELECT
|
||||
/// - `i` - INSERT
|
||||
/// - `u` - UPDATE
|
||||
/// - `d` - DELETE
|
||||
/// - `c` - CREATE
|
||||
/// - `D` - DROP
|
||||
/// - `a` - ALTER
|
||||
/// - `I` - INDEX
|
||||
/// - `t` - CREATE TEMPORARY TABLES
|
||||
/// - `l` - LOCK TABLES
|
||||
/// - `r` - REFERENCES
|
||||
/// - `A` - ALL PRIVILEGES
|
||||
///
|
||||
/// If you provide a database name, you can omit it from the privilege string,
|
||||
/// e.g. `edit-db-privs my_db -p my_user+siu` is equivalent to `edit-db-privs -p my_db:my_user:siu`.
|
||||
/// While it doesn't make much of a difference for a single edit, it can be useful for editing multiple users
|
||||
/// on the same database at once.
|
||||
///
|
||||
/// Example usage of non-interactive mode:
|
||||
///
|
||||
/// Enable privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
|
||||
///
|
||||
/// `mysqladm edit-db-privs -p my_db:my_user:siu`
|
||||
///
|
||||
/// Enable all privileges for user `my_other_user` on database `my_other_db`:
|
||||
///
|
||||
/// `mysqladm edit-db-privs -p my_other_db:my_other_user:A`
|
||||
///
|
||||
/// Set miscellaneous privileges for multiple users on database `my_db`:
|
||||
///
|
||||
/// `mysqladm edit-db-privs my_db -p my_user:siu my_other_user:ct``
|
||||
///
|
||||
#[command(verbatim_doc_comment)]
|
||||
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)]
|
||||
pub struct DatabaseCreateArgs {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseDropArgs {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseShowArgs {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseShowPrivsArgs {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseEditPrivsArgs {
|
||||
/// 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,
|
||||
}
|
||||
|
||||
#[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: ClientCommand,
|
||||
server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
match command {
|
||||
ClientCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
||||
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
||||
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
||||
ClientCommand::ShowDbPrivs(args) => 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);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
anyhow::bail!(e);
|
||||
}
|
||||
Some(response) => {
|
||||
anyhow::bail!("Unexpected response from server: {:?}", response);
|
||||
}
|
||||
None => {
|
||||
anyhow::bail!("No response from server");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_databases(
|
||||
args: DatabaseCreateArgs,
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn drop_databases(
|
||||
args: DatabaseDropArgs,
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn show_databases(
|
||||
args: DatabaseShowArgs,
|
||||
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(())
|
||||
}
|
||||
|
||||
async fn show_database_privileges(
|
||||
args: DatabaseShowPrivsArgs,
|
||||
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(())
|
||||
}
|
||||
|
||||
pub async fn edit_database_privileges(
|
||||
args: DatabaseEditPrivsArgs,
|
||||
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: &DatabaseEditPrivsArgs,
|
||||
) -> 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"),
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
4
src/client/mysql_admutils_compatibility.rs
Normal file
4
src/client/mysql_admutils_compatibility.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
pub mod common;
|
||||
mod error_messages;
|
||||
pub mod mysql_dbadm;
|
||||
pub mod mysql_useradm;
|
||||
11
src/client/mysql_admutils_compatibility/common.rs
Normal file
11
src/client/mysql_admutils_compatibility/common.rs
Normal file
@@ -0,0 +1,11 @@
|
||||
use crate::core::protocol::{MySQLDatabase, MySQLUser};
|
||||
|
||||
#[inline]
|
||||
pub fn trim_db_name_to_32_chars(db_name: &MySQLDatabase) -> MySQLDatabase {
|
||||
db_name.chars().take(32).collect::<String>().into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn trim_user_name_to_32_chars(user_name: &MySQLUser) -> MySQLUser {
|
||||
user_name.chars().take(32).collect::<String>().into()
|
||||
}
|
||||
176
src/client/mysql_admutils_compatibility/error_messages.rs
Normal file
176
src/client/mysql_admutils_compatibility/error_messages.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use crate::core::protocol::{
|
||||
CreateDatabaseError, CreateUserError, DbOrUser, DropDatabaseError, DropUserError,
|
||||
GetDatabasesPrivilegeDataError, ListUsersError,
|
||||
};
|
||||
|
||||
pub fn name_validation_error_to_error_message(name: &str, db_or_user: DbOrUser) -> String {
|
||||
let argv0 = std::env::args().next().unwrap_or_else(|| match db_or_user {
|
||||
DbOrUser::Database => "mysql-dbadm".to_string(),
|
||||
DbOrUser::User => "mysql-useradm".to_string(),
|
||||
});
|
||||
|
||||
format!(
|
||||
concat!(
|
||||
"{}: {} name '{}' contains invalid characters.\n",
|
||||
"Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted. Skipping.",
|
||||
),
|
||||
argv0,
|
||||
db_or_user.capitalized(),
|
||||
name,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn owner_validation_error_message(name: &str, db_or_user: DbOrUser) -> String {
|
||||
format!(
|
||||
"You are not in charge of mysql-{}: '{}'. Skipping.",
|
||||
db_or_user.lowercased(),
|
||||
name
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_create_user_error(error: CreateUserError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-useradm".to_string());
|
||||
match error {
|
||||
CreateUserError::SanitizationError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(name, DbOrUser::User)
|
||||
);
|
||||
}
|
||||
CreateUserError::OwnershipError(_) => {
|
||||
eprintln!("{}", owner_validation_error_message(name, DbOrUser::User));
|
||||
}
|
||||
CreateUserError::MySqlError(_) | CreateUserError::UserAlreadyExists => {
|
||||
eprintln!("{}: Failed to create user '{}'.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_drop_user_error(error: DropUserError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-useradm".to_string());
|
||||
match error {
|
||||
DropUserError::SanitizationError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(name, DbOrUser::User)
|
||||
);
|
||||
}
|
||||
DropUserError::OwnershipError(_) => {
|
||||
eprintln!("{}", owner_validation_error_message(name, DbOrUser::User));
|
||||
}
|
||||
DropUserError::MySqlError(_) | DropUserError::UserDoesNotExist => {
|
||||
eprintln!("{}: Failed to delete user '{}'.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_list_users_error(error: ListUsersError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-useradm".to_string());
|
||||
match error {
|
||||
ListUsersError::SanitizationError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(name, DbOrUser::User)
|
||||
);
|
||||
}
|
||||
ListUsersError::OwnershipError(_) => {
|
||||
eprintln!("{}", owner_validation_error_message(name, DbOrUser::User));
|
||||
}
|
||||
ListUsersError::UserDoesNotExist => {
|
||||
eprintln!(
|
||||
"{}: User '{}' does not exist. You must create it first.",
|
||||
argv0, name,
|
||||
);
|
||||
}
|
||||
ListUsersError::MySqlError(_) => {
|
||||
eprintln!("{}: Failed to look up password for user '{}'", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn handle_create_database_error(error: CreateDatabaseError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-dbadm".to_string());
|
||||
match error {
|
||||
CreateDatabaseError::SanitizationError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(name, DbOrUser::Database)
|
||||
);
|
||||
}
|
||||
CreateDatabaseError::OwnershipError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
owner_validation_error_message(name, DbOrUser::Database)
|
||||
);
|
||||
}
|
||||
CreateDatabaseError::MySqlError(_) => {
|
||||
eprintln!("{}: Cannot create database '{}'.", argv0, name);
|
||||
}
|
||||
CreateDatabaseError::DatabaseAlreadyExists => {
|
||||
eprintln!("{}: Database '{}' already exists.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_drop_database_error(error: DropDatabaseError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-dbadm".to_string());
|
||||
match error {
|
||||
DropDatabaseError::SanitizationError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(name, DbOrUser::Database)
|
||||
);
|
||||
}
|
||||
DropDatabaseError::OwnershipError(_) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
owner_validation_error_message(name, DbOrUser::Database)
|
||||
);
|
||||
}
|
||||
DropDatabaseError::MySqlError(_) => {
|
||||
eprintln!("{}: Cannot drop database '{}'.", argv0, name);
|
||||
}
|
||||
DropDatabaseError::DatabaseDoesNotExist => {
|
||||
eprintln!("{}: Database '{}' doesn't exist.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_show_database_error_message(
|
||||
error: GetDatabasesPrivilegeDataError,
|
||||
name: &str,
|
||||
) -> String {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-dbadm".to_string());
|
||||
|
||||
match error {
|
||||
GetDatabasesPrivilegeDataError::SanitizationError(_) => {
|
||||
name_validation_error_to_error_message(name, DbOrUser::Database)
|
||||
}
|
||||
GetDatabasesPrivilegeDataError::OwnershipError(_) => {
|
||||
owner_validation_error_message(name, DbOrUser::Database)
|
||||
}
|
||||
GetDatabasesPrivilegeDataError::MySqlError(err) => {
|
||||
format!(
|
||||
"{}: Failed to look up privileges for database '{}': {}",
|
||||
argv0, name, err
|
||||
)
|
||||
}
|
||||
GetDatabasesPrivilegeDataError::DatabaseDoesNotExist => {
|
||||
format!("{}: Database '{}' doesn't exist.", argv0, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
347
src/client/mysql_admutils_compatibility/mysql_dbadm.rs
Normal file
347
src/client/mysql_admutils_compatibility/mysql_dbadm.rs
Normal file
@@ -0,0 +1,347 @@
|
||||
use clap::Parser;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::path::PathBuf;
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
command::{DatabaseEditPrivsArgs, edit_database_privileges, erroneous_server_response},
|
||||
mysql_admutils_compatibility::{
|
||||
common::trim_db_name_to_32_chars,
|
||||
error_messages::{
|
||||
format_show_database_error_message, handle_create_database_error,
|
||||
handle_drop_database_error,
|
||||
},
|
||||
},
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
database_privileges::DatabasePrivilegeRow,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, GetDatabasesPrivilegeDataError, MySQLDatabase, Request,
|
||||
Response, create_client_to_server_message_stream,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const HELP_DB_PERM: &str = r#"
|
||||
Edit permissions for the DATABASE(s). Running this command will
|
||||
spawn the editor stored in the $EDITOR environment variable.
|
||||
(pico will be used if the variable is unset)
|
||||
|
||||
The file should contain one line per user, starting with the
|
||||
username and followed by ten Y/N-values seperated by whitespace.
|
||||
Lines starting with # are ignored.
|
||||
|
||||
The Y/N-values corresponds to the following mysql privileges:
|
||||
Select - Enables use of SELECT
|
||||
Insert - Enables use of INSERT
|
||||
Update - Enables use of UPDATE
|
||||
Delete - Enables use of DELETE
|
||||
Create - Enables use of CREATE TABLE
|
||||
Drop - Enables use of DROP TABLE
|
||||
Alter - Enables use of ALTER TABLE
|
||||
Index - Enables use of CREATE INDEX and DROP INDEX
|
||||
Temp - Enables use of CREATE TEMPORARY TABLE
|
||||
Lock - Enables use of LOCK TABLE
|
||||
References - Enables use of REFERENCES
|
||||
"#;
|
||||
|
||||
/// Create, drop or edit permissions for the DATABASE(s),
|
||||
/// as determined by the COMMAND.
|
||||
///
|
||||
/// This is a compatibility layer for the mysql-dbadm command.
|
||||
/// Please consider using the newer mysqladm command instead.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
bin_name = "mysql-dbadm",
|
||||
version,
|
||||
about,
|
||||
disable_help_subcommand = true,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
/// Path to the socket of the server, if it already exists.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
server_socket_path: Option<PathBuf>,
|
||||
|
||||
/// Config file to use for the server.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
/// Print help for the 'editperm' subcommand.
|
||||
#[arg(long, global = true)]
|
||||
pub help_editperm: bool,
|
||||
}
|
||||
|
||||
// NOTE: mysql-dbadm explicitly calls privileges "permissions".
|
||||
// This is something we're trying to move away from.
|
||||
// See https://git.pvv.ntnu.no/Projects/mysqladm-rs/issues/29
|
||||
#[derive(Parser)]
|
||||
pub enum Command {
|
||||
/// create the DATABASE(s).
|
||||
Create(CreateArgs),
|
||||
|
||||
/// delete the DATABASE(s).
|
||||
Drop(DatabaseDropArgs),
|
||||
|
||||
/// give information about the DATABASE(s), or, if
|
||||
/// none are given, all the ones you own.
|
||||
Show(DatabaseShowArgs),
|
||||
|
||||
// TODO: make this output more verbatim_doc_comment-like,
|
||||
// without messing up the indentation.
|
||||
/// change permissions for the DATABASE(s). Your
|
||||
/// favorite editor will be started, allowing you
|
||||
/// to make changes to the permission table.
|
||||
/// Run 'mysql-dbadm --help-editperm' for more
|
||||
/// information.
|
||||
Editperm(EditPermArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct CreateArgs {
|
||||
/// The name of the DATABASE(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct DatabaseDropArgs {
|
||||
/// The name of the DATABASE(s) to drop.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct DatabaseShowArgs {
|
||||
/// The name of the DATABASE(s) to show.
|
||||
#[arg(num_args = 0..)]
|
||||
name: Vec<MySQLDatabase>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct EditPermArgs {
|
||||
/// The name of the DATABASE to edit permissions for.
|
||||
pub database: MySQLDatabase,
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
if args.help_editperm {
|
||||
println!("{}", HELP_DB_PERM);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server_connection = bootstrap_server_connection_and_drop_privileges(
|
||||
args.server_socket_path,
|
||||
args.config,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args().next().unwrap_or("mysql-dbadm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
tokio_run_command(command, server_connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyhow::Result<()> {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let message_stream = create_client_to_server_message_stream(tokio_socket);
|
||||
match command {
|
||||
Command::Create(args) => create_databases(args, message_stream).await,
|
||||
Command::Drop(args) => drop_databases(args, message_stream).await,
|
||||
Command::Show(args) => show_databases(args, message_stream).await,
|
||||
Command::Editperm(args) => {
|
||||
let edit_privileges_args = DatabaseEditPrivsArgs {
|
||||
name: Some(args.database),
|
||||
privs: vec![],
|
||||
json: false,
|
||||
editor: None,
|
||||
yes: false,
|
||||
};
|
||||
|
||||
edit_database_privileges(edit_privileges_args, message_stream).await
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_databases(
|
||||
args: CreateArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
|
||||
|
||||
let message = Request::CreateDatabases(database_names);
|
||||
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?;
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("Database {} created.", name),
|
||||
Err(err) => handle_create_database_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drop_databases(
|
||||
args: DatabaseDropArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
|
||||
|
||||
let message = Request::DropDatabases(database_names);
|
||||
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?;
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("Database {} dropped.", name),
|
||||
Err(err) => handle_drop_database_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_databases(
|
||||
args: DatabaseShowArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let database_names: Vec<MySQLDatabase> =
|
||||
args.name.iter().map(trim_db_name_to_32_chars).collect();
|
||||
|
||||
let message = if database_names.is_empty() {
|
||||
let message = Request::ListDatabases(None);
|
||||
server_connection.send(message).await?;
|
||||
let response = server_connection.next().await;
|
||||
let databases = match response {
|
||||
Some(Ok(Response::ListAllDatabases(databases))) => databases.unwrap_or(vec![]),
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
let database_names = databases.into_iter().map(|db| db.database).collect();
|
||||
|
||||
Request::ListPrivileges(Some(database_names))
|
||||
} else {
|
||||
Request::ListPrivileges(Some(database_names))
|
||||
};
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let response = server_connection.next().await;
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
// NOTE: mysql-dbadm show has a quirk where valid database names
|
||||
// for non-existent databases will report with no users.
|
||||
let results: Vec<Result<(MySQLDatabase, Vec<DatabasePrivilegeRow>), String>> = match response {
|
||||
Some(Ok(Response::ListPrivileges(result))) => result
|
||||
.into_iter()
|
||||
.map(
|
||||
|(name, rows)| match rows.map(|rows| (name.to_owned(), rows)) {
|
||||
Ok(rows) => Ok(rows),
|
||||
Err(GetDatabasesPrivilegeDataError::DatabaseDoesNotExist) => Ok((name, vec![])),
|
||||
Err(err) => Err(format_show_database_error_message(err, &name)),
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
results.into_iter().try_for_each(|result| match result {
|
||||
Ok((name, rows)) => print_db_privs(&name, rows),
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn yn(value: bool) -> &'static str {
|
||||
if value { "Y" } else { "N" }
|
||||
}
|
||||
|
||||
fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) -> anyhow::Result<()> {
|
||||
println!(
|
||||
concat!(
|
||||
"Database '{}':\n",
|
||||
"# User Select Insert Update Delete Create Drop Alter Index Temp Lock References\n",
|
||||
"# ---------------- ------ ------ ------ ------ ------ ---- ----- ----- ---- ---- ----------"
|
||||
),
|
||||
name,
|
||||
);
|
||||
if rows.is_empty() {
|
||||
println!("# (no permissions currently granted to any users)");
|
||||
} else {
|
||||
for privilege in rows {
|
||||
println!(
|
||||
" {:<16} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
|
||||
privilege.user,
|
||||
yn(privilege.select_priv),
|
||||
yn(privilege.insert_priv),
|
||||
yn(privilege.update_priv),
|
||||
yn(privilege.delete_priv),
|
||||
yn(privilege.create_priv),
|
||||
yn(privilege.drop_priv),
|
||||
yn(privilege.alter_priv),
|
||||
yn(privilege.index_priv),
|
||||
yn(privilege.create_tmp_table_priv),
|
||||
yn(privilege.lock_tables_priv),
|
||||
yn(privilege.references_priv)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
301
src/client/mysql_admutils_compatibility/mysql_useradm.rs
Normal file
301
src/client/mysql_admutils_compatibility/mysql_useradm.rs
Normal file
@@ -0,0 +1,301 @@
|
||||
use clap::Parser;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
command::{erroneous_server_response, read_password_from_stdin_with_double_check}, mysql_admutils_compatibility::{
|
||||
common::trim_user_name_to_32_chars,
|
||||
error_messages::{
|
||||
handle_create_user_error, handle_drop_user_error, handle_list_users_error,
|
||||
},
|
||||
}
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, MySQLUser, Request, Response,
|
||||
create_client_to_server_message_stream,
|
||||
},
|
||||
},
|
||||
server::sql::user_operations::DatabaseUser,
|
||||
};
|
||||
|
||||
/// Create, delete or change password for the USER(s),
|
||||
/// as determined by the COMMAND.
|
||||
///
|
||||
/// This is a compatibility layer for the mysql-useradm command.
|
||||
/// Please consider using the newer mysqladm command instead.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
bin_name = "mysql-useradm",
|
||||
version,
|
||||
about,
|
||||
disable_help_subcommand = true,
|
||||
verbatim_doc_comment
|
||||
)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
/// Path to the socket of the server, if it already exists.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
server_socket_path: Option<PathBuf>,
|
||||
|
||||
/// Config file to use for the server.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
config: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub enum Command {
|
||||
/// create the USER(s).
|
||||
Create(CreateArgs),
|
||||
|
||||
/// delete the USER(s).
|
||||
Delete(DeleteArgs),
|
||||
|
||||
/// change the MySQL password for the USER(s).
|
||||
Passwd(PasswdArgs),
|
||||
|
||||
/// give information about the USERS(s), or, if
|
||||
/// none are given, all the users you have.
|
||||
Show(ShowArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct CreateArgs {
|
||||
/// The name of the USER(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLUser>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct DeleteArgs {
|
||||
/// The name of the USER(s) to delete.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLUser>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct PasswdArgs {
|
||||
/// The name of the USER(s) to change the password for.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<MySQLUser>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct ShowArgs {
|
||||
/// The name of the USER(s) to show.
|
||||
#[arg(num_args = 0..)]
|
||||
name: Vec<MySQLUser>,
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args()
|
||||
.next()
|
||||
.unwrap_or("mysql-useradm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let server_connection = bootstrap_server_connection_and_drop_privileges(
|
||||
args.server_socket_path,
|
||||
args.config,
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
tokio_run_command(command, server_connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyhow::Result<()> {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let message_stream = create_client_to_server_message_stream(tokio_socket);
|
||||
match command {
|
||||
Command::Create(args) => create_user(args, message_stream).await,
|
||||
Command::Delete(args) => drop_users(args, message_stream).await,
|
||||
Command::Passwd(args) => passwd_users(args, message_stream).await,
|
||||
Command::Show(args) => show_users(args, message_stream).await,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn create_user(
|
||||
args: CreateArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let db_users = args.name.iter().map(trim_user_name_to_32_chars).collect();
|
||||
|
||||
let message = Request::CreateUsers(db_users);
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CreateUsers(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("User '{}' created.", name),
|
||||
Err(err) => handle_create_user_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drop_users(
|
||||
args: DeleteArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let db_users = args.name.iter().map(trim_user_name_to_32_chars).collect();
|
||||
|
||||
let message = Request::DropUsers(db_users);
|
||||
server_connection.send(message).await?;
|
||||
|
||||
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?;
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("User '{}' deleted.", name),
|
||||
Err(err) => handle_drop_user_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn passwd_users(
|
||||
args: PasswdArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let db_users = args.name.iter().map(trim_user_name_to_32_chars).collect();
|
||||
|
||||
let message = Request::ListUsers(Some(db_users));
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let response = match server_connection.next().await {
|
||||
Some(Ok(Response::ListUsers(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or("mysql-useradm".to_string());
|
||||
|
||||
let users = response
|
||||
.into_iter()
|
||||
.filter_map(|(name, result)| match result {
|
||||
Ok(user) => Some(user),
|
||||
Err(err) => {
|
||||
handle_list_users_error(err, &name);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for user in users {
|
||||
let password = read_password_from_stdin_with_double_check(&user.user)?;
|
||||
let message = Request::PasswdUser(user.user.to_owned(), password);
|
||||
server_connection.send(message).await?;
|
||||
match server_connection.next().await {
|
||||
Some(Ok(Response::PasswdUser(result))) => match result {
|
||||
Ok(()) => println!("Password updated for user '{}'.", &user.user),
|
||||
Err(_) => eprintln!(
|
||||
"{}: Failed to update password for user '{}'.",
|
||||
argv0, user.user,
|
||||
),
|
||||
},
|
||||
response => return erroneous_server_response(response),
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_users(
|
||||
args: ShowArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let db_users: Vec<_> = args.name.iter().map(trim_user_name_to_32_chars).collect();
|
||||
|
||||
let message = if db_users.is_empty() {
|
||||
Request::ListUsers(None)
|
||||
} else {
|
||||
Request::ListUsers(Some(db_users))
|
||||
};
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let users: Vec<DatabaseUser> = match server_connection.next().await {
|
||||
Some(Ok(Response::ListAllUsers(result))) => match result {
|
||||
Ok(users) => users,
|
||||
Err(err) => {
|
||||
println!("Failed to list users: {:?}", err);
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
Some(Ok(Response::ListUsers(result))) => result
|
||||
.into_iter()
|
||||
.filter_map(|(name, result)| match result {
|
||||
Ok(user) => Some(user),
|
||||
Err(err) => {
|
||||
handle_list_users_error(err, &name);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
for user in users {
|
||||
if user.has_password {
|
||||
println!("User '{}': password set.", user.user);
|
||||
} else {
|
||||
println!("User '{}': no password set.", user.user);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user