mysqladm-rs/src/cli/database_command.rs

445 lines
14 KiB
Rust
Raw Normal View History

use anyhow::Context;
use clap::Parser;
use dialoguer::{Confirm, Editor};
use futures_util::{SinkExt, StreamExt};
use nix::unistd::{getuid, User};
use prettytable::{Cell, Row, Table};
use crate::{
cli::common::erroneous_server_response,
core::{
common::yn,
database_privileges::{
db_priv_field_human_readable_name, diff_privileges, display_privilege_diffs,
generate_editor_content_from_privilege_data, parse_privilege_data_from_editor_content,
parse_privilege_table_cli_arg,
},
protocol::{
print_create_databases_output_status, print_drop_databases_output_status,
print_modify_database_privileges_output_status, ClientToServerMessageStream, Request,
Response,
},
},
server::sql::database_privilege_operations::{DatabasePrivilegeRow, DATABASE_PRIVILEGE_FIELDS},
};
#[derive(Parser, Debug, Clone)]
// #[command(next_help_heading = Some(DATABASE_COMMAND_HEADER))]
pub enum DatabaseCommand {
/// Create one or more databases
#[command()]
CreateDb(DatabaseCreateArgs),
/// Delete one or more databases
#[command()]
DropDb(DatabaseDropArgs),
/// List all databases you have access to
#[command()]
ListDb(DatabaseListArgs),
/// List user privileges for one or more databases
///
/// If no database names are provided, it will show privileges for all databases you have access to.
#[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>`
/// 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),
}
#[derive(Parser, Debug, Clone)]
pub struct DatabaseCreateArgs {
/// The name of the database(s) to create.
#[arg(num_args = 1..)]
name: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct DatabaseDropArgs {
/// The name of the database(s) to drop.
#[arg(num_args = 1..)]
name: Vec<String>,
}
#[derive(Parser, Debug, Clone)]
pub struct DatabaseListArgs {
/// Whether to output the information in JSON format.
#[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<String>,
/// Whether to output the information in JSON format.
#[arg(short, long)]
json: bool,
}
#[derive(Parser, Debug, Clone)]
pub struct DatabaseEditPrivsArgs {
/// The name of the database to edit privileges for.
pub name: Option<String>,
#[arg(short, long, value_name = "[DATABASE:]USER:PRIVILEGES", num_args = 0..)]
pub privs: Vec<String>,
/// Whether to output the information in JSON format.
#[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 handle_command(
command: DatabaseCommand,
server_connection: ClientToServerMessageStream,
) -> anyhow::Result<()> {
match command {
DatabaseCommand::CreateDb(args) => create_databases(args, server_connection).await,
DatabaseCommand::DropDb(args) => drop_databases(args, server_connection).await,
DatabaseCommand::ListDb(args) => list_databases(args, server_connection).await,
DatabaseCommand::ShowDbPrivs(args) => {
show_database_privileges(args, server_connection).await
}
DatabaseCommand::EditDbPrivs(args) => {
edit_database_privileges(args, server_connection).await
}
}
}
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.clone());
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?;
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.clone());
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?;
print_drop_databases_output_status(&result);
Ok(())
}
async fn list_databases(
args: DatabaseListArgs,
mut server_connection: ClientToServerMessageStream,
) -> anyhow::Result<()> {
let message = Request::ListDatabases;
server_connection.send(message).await?;
let result = match server_connection.next().await {
Some(Ok(Response::ListAllDatabases(result))) => result,
response => return erroneous_server_response(response),
};
server_connection.send(Request::Exit).await?;
let database_list = match result {
Ok(list) => list,
Err(err) => {
return Err(anyhow::anyhow!(err.to_error_message()).context("Failed to list databases"))
}
};
if args.json {
println!("{}", serde_json::to_string_pretty(&database_list)?);
} else if database_list.is_empty() {
println!("No databases to show.");
} else {
for db in database_list {
println!("{}", db);
}
}
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.clone()))
};
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.clone().map(|name| vec![name]));
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),
};
let privileges_to_change = if !args.privs.is_empty() {
parse_privilege_tables_from_args(&args)?
} else {
edit_privileges_with_editor(&privilege_data, args.name.as_deref())?
};
let diffs = diff_privileges(&privilege_data, &privileges_to_change);
if diffs.is_empty() {
println!("No changes to make.");
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<Vec<DatabasePrivilegeRow>> {
debug_assert!(!args.privs.is_empty());
let result = if let Some(name) = &args.name {
args.privs
.iter()
.map(|p| {
parse_privilege_table_cli_arg(&format!("{}:{}", name, &p))
.context(format!("Failed parsing database privileges: `{}`", &p))
})
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
} else {
args.privs
.iter()
.map(|p| {
parse_privilege_table_cli_arg(p)
.context(format!("Failed parsing database privileges: `{}`", &p))
})
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
};
Ok(result)
}
fn edit_privileges_with_editor(
privilege_data: &[DatabasePrivilegeRow],
database_name: Option<&str>,
) -> 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"),
}
}