Rewrite entire codebase to split into client and server
This commit is contained in:
20
src/cli/common.rs
Normal file
20
src/cli/common.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use crate::core::protocol::Response;
|
||||
|
||||
pub fn erroneous_server_response(
|
||||
response: Option<Result<Response, std::io::Error>>,
|
||||
) -> anyhow::Result<()> {
|
||||
match response {
|
||||
Some(Ok(Response::Error(e))) => {
|
||||
anyhow::bail!("Server returned error: {}", e);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
anyhow::bail!(e);
|
||||
}
|
||||
Some(response) => {
|
||||
anyhow::bail!("Unexpected response from server: {:?}", response);
|
||||
}
|
||||
None => {
|
||||
anyhow::bail!("No response from server");
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,17 +1,29 @@
|
||||
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 sqlx::{Connection, MySqlConnection};
|
||||
|
||||
use crate::core::{
|
||||
common::{close_database_connection, get_current_unix_user, yn, CommandStatus},
|
||||
database_operations::*,
|
||||
database_privilege_operations::*,
|
||||
user_operations::user_exists,
|
||||
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)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
// #[command(next_help_heading = Some(DATABASE_COMMAND_HEADER))]
|
||||
pub enum DatabaseCommand {
|
||||
/// Create one or more databases
|
||||
@@ -86,28 +98,28 @@ pub enum DatabaseCommand {
|
||||
EditDbPrivs(DatabaseEditPrivsArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseCreateArgs {
|
||||
/// The name of the database(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseDropArgs {
|
||||
/// The name of the database(s) to drop.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseListArgs {
|
||||
/// Whether to output the information in JSON format.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseShowPrivsArgs {
|
||||
/// The name of the database(s) to show.
|
||||
#[arg(num_args = 0..)]
|
||||
@@ -118,7 +130,7 @@ pub struct DatabaseShowPrivsArgs {
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DatabaseEditPrivsArgs {
|
||||
/// The name of the database to edit privileges for.
|
||||
pub name: Option<String>,
|
||||
@@ -141,125 +153,143 @@ pub struct DatabaseEditPrivsArgs {
|
||||
|
||||
pub async fn handle_command(
|
||||
command: DatabaseCommand,
|
||||
mut connection: MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
let result = connection
|
||||
.transaction(|txn| {
|
||||
Box::pin(async move {
|
||||
match command {
|
||||
DatabaseCommand::CreateDb(args) => create_databases(args, txn).await,
|
||||
DatabaseCommand::DropDb(args) => drop_databases(args, txn).await,
|
||||
DatabaseCommand::ListDb(args) => list_databases(args, txn).await,
|
||||
DatabaseCommand::ShowDbPrivs(args) => show_database_privileges(args, txn).await,
|
||||
DatabaseCommand::EditDbPrivs(args) => edit_privileges(args, txn).await,
|
||||
}
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
close_database_connection(connection).await;
|
||||
|
||||
result
|
||||
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,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.name.is_empty() {
|
||||
anyhow::bail!("No database names provided");
|
||||
}
|
||||
|
||||
let mut result = CommandStatus::SuccessfullyModified;
|
||||
let message = Request::CreateDatabases(args.name.clone());
|
||||
server_connection.send(message).await?;
|
||||
|
||||
for name in args.name {
|
||||
// TODO: This can be optimized by fetching all the database privileges in one query.
|
||||
if let Err(e) = create_database(&name, connection).await {
|
||||
eprintln!("Failed to create database '{}': {}", name, e);
|
||||
eprintln!("Skipping...");
|
||||
result = CommandStatus::PartiallySuccessfullyModified;
|
||||
} else {
|
||||
println!("Database '{}' created.", name);
|
||||
}
|
||||
}
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CreateDatabases(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
print_create_databases_output_status(&result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drop_databases(
|
||||
args: DatabaseDropArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.name.is_empty() {
|
||||
anyhow::bail!("No database names provided");
|
||||
}
|
||||
|
||||
let mut result = CommandStatus::SuccessfullyModified;
|
||||
let message = Request::DropDatabases(args.name.clone());
|
||||
server_connection.send(message).await?;
|
||||
|
||||
for name in args.name {
|
||||
// TODO: This can be optimized by fetching all the database privileges in one query.
|
||||
if let Err(e) = drop_database(&name, connection).await {
|
||||
eprintln!("Failed to drop database '{}': {}", name, e);
|
||||
eprintln!("Skipping...");
|
||||
result = CommandStatus::PartiallySuccessfullyModified;
|
||||
} else {
|
||||
println!("Database '{}' dropped.", name);
|
||||
}
|
||||
}
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::DropDatabases(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
print_drop_databases_output_status(&result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_databases(
|
||||
args: DatabaseListArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
let databases = get_database_list(connection).await?;
|
||||
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(&databases)?);
|
||||
return Ok(CommandStatus::NoModificationsIntended);
|
||||
}
|
||||
|
||||
if databases.is_empty() {
|
||||
println!("{}", serde_json::to_string_pretty(&database_list)?);
|
||||
} else if database_list.is_empty() {
|
||||
println!("No databases to show.");
|
||||
} else {
|
||||
for db in databases {
|
||||
for db in database_list {
|
||||
println!("{}", db);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CommandStatus::NoModificationsIntended)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_database_privileges(
|
||||
args: DatabaseShowPrivsArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
let database_users_to_show = if args.name.is_empty() {
|
||||
get_all_database_privileges(connection).await?
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let message = if args.name.is_empty() {
|
||||
Request::ListPrivileges(None)
|
||||
} else {
|
||||
// TODO: This can be optimized by fetching all the database privileges in one query.
|
||||
let mut result = Vec::with_capacity(args.name.len());
|
||||
for name in args.name {
|
||||
match get_database_privileges(&name, connection).await {
|
||||
Ok(db) => result.extend(db),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to show database '{}': {}", name, e);
|
||||
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"));
|
||||
}
|
||||
}
|
||||
result
|
||||
},
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
if args.json {
|
||||
println!("{}", serde_json::to_string_pretty(&database_users_to_show)?);
|
||||
return Ok(CommandStatus::NoModificationsIntended);
|
||||
}
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if database_users_to_show.is_empty() {
|
||||
println!("No database users to show.");
|
||||
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(
|
||||
@@ -270,7 +300,7 @@ async fn show_database_privileges(
|
||||
.collect(),
|
||||
));
|
||||
|
||||
for row in database_users_to_show {
|
||||
for row in privilege_data {
|
||||
table.add_row(row![
|
||||
row.db,
|
||||
row.user,
|
||||
@@ -290,17 +320,40 @@ async fn show_database_privileges(
|
||||
table.printstd();
|
||||
}
|
||||
|
||||
Ok(CommandStatus::NoModificationsIntended)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn edit_privileges(
|
||||
pub async fn edit_database_privileges(
|
||||
args: DatabaseEditPrivsArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
let privilege_data = if let Some(name) = &args.name {
|
||||
get_database_privileges(name, connection).await?
|
||||
} else {
|
||||
get_all_database_privileges(connection).await?
|
||||
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),
|
||||
};
|
||||
|
||||
// TODO: The data from args should not be absolute.
|
||||
@@ -316,22 +369,16 @@ pub async fn edit_privileges(
|
||||
edit_privileges_with_editor(&privilege_data)?
|
||||
};
|
||||
|
||||
for row in privileges_to_change.iter() {
|
||||
if !user_exists(&row.user, connection).await? {
|
||||
// TODO: allow user to return and correct their mistake
|
||||
anyhow::bail!("User {} does not exist", row.user);
|
||||
}
|
||||
}
|
||||
|
||||
let diffs = diff_privileges(&privilege_data, &privileges_to_change);
|
||||
|
||||
if diffs.is_empty() {
|
||||
println!("No changes to make.");
|
||||
return Ok(CommandStatus::NoModificationsNeeded);
|
||||
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?")
|
||||
@@ -339,15 +386,27 @@ pub async fn edit_privileges(
|
||||
.show_default(true)
|
||||
.interact()?
|
||||
{
|
||||
return Ok(CommandStatus::Cancelled);
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
apply_privilege_diffs(diffs, connection).await?;
|
||||
let message = Request::ModifyPrivileges(diffs);
|
||||
server_connection.send(message).await?;
|
||||
|
||||
Ok(CommandStatus::SuccessfullyModified)
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::ModifyPrivileges(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
// TODO: allow user to return and correct their mistake
|
||||
print_modify_database_privileges_output_status(&result);
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_privilege_tables_from_args(
|
||||
fn parse_privilege_tables_from_args(
|
||||
args: &DatabaseEditPrivsArgs,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
debug_assert!(!args.privs.is_empty());
|
||||
@@ -371,20 +430,22 @@ pub fn parse_privilege_tables_from_args(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn edit_privileges_with_editor(
|
||||
fn edit_privileges_with_editor(
|
||||
privilege_data: &[DatabasePrivilegeRow],
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
let unix_user = get_current_unix_user()?;
|
||||
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);
|
||||
|
||||
// TODO: handle errors better here
|
||||
let result = Editor::new()
|
||||
.extension("tsv")
|
||||
.edit(&editor_content)?
|
||||
.unwrap();
|
||||
let result = Editor::new().extension("tsv").edit(&editor_content)?;
|
||||
|
||||
parse_privilege_data_from_editor_content(result)
|
||||
.context("Could not parse privilege data from editor")
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
pub mod common;
|
||||
mod error_messages;
|
||||
pub mod mysql_dbadm;
|
||||
pub mod mysql_useradm;
|
||||
|
@@ -1,57 +1,4 @@
|
||||
use crate::core::common::{
|
||||
get_current_unix_user, validate_name_or_error, validate_ownership_or_error, DbOrUser,
|
||||
};
|
||||
|
||||
/// In contrast to the new implementation which reports errors on any invalid name
|
||||
/// for any reason, mysql-admutils would only log the error and skip that particular
|
||||
/// name. This function replicates that behavior.
|
||||
pub fn filter_db_or_user_names(
|
||||
names: Vec<String>,
|
||||
db_or_user: DbOrUser,
|
||||
) -> anyhow::Result<Vec<String>> {
|
||||
let unix_user = get_current_unix_user()?;
|
||||
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(),
|
||||
});
|
||||
|
||||
let filtered_names = names
|
||||
.into_iter()
|
||||
// NOTE: The original implementation would only copy the first 32 characters
|
||||
// of the argument into it's internal buffer. We replicate that behavior
|
||||
// here.
|
||||
.map(|name| name.chars().take(32).collect::<String>())
|
||||
.filter(|name| {
|
||||
if let Err(_err) = validate_ownership_or_error(name, &unix_user, db_or_user) {
|
||||
println!(
|
||||
"You are not in charge of mysql-{}: '{}'. Skipping.",
|
||||
db_or_user.lowercased(),
|
||||
name
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.filter(|name| {
|
||||
// NOTE: while this also checks for the length of the name,
|
||||
// the name is already truncated to 32 characters. So
|
||||
// if there is an error, it's guaranteed to be due to
|
||||
// invalid characters.
|
||||
if let Err(_err) = validate_name_or_error(name, db_or_user) {
|
||||
println!(
|
||||
concat!(
|
||||
"{}: {} name '{}' contains invalid characters.\n",
|
||||
"Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted. Skipping.",
|
||||
),
|
||||
argv0,
|
||||
db_or_user.capitalized(),
|
||||
name
|
||||
);
|
||||
return false;
|
||||
}
|
||||
true
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(filtered_names)
|
||||
#[inline]
|
||||
pub fn trim_to_32_chars(name: &str) -> String {
|
||||
name.chars().take(32).collect()
|
||||
}
|
||||
|
176
src/cli/mysql_admutils_compatibility/error_messages.rs
Normal file
176
src/cli/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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,14 +1,29 @@
|
||||
use clap::Parser;
|
||||
use sqlx::MySqlConnection;
|
||||
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::{
|
||||
cli::{database_command, mysql_admutils_compatibility::common::filter_db_or_user_names},
|
||||
core::{
|
||||
common::{yn, DbOrUser},
|
||||
config::{create_mysql_connection_from_config, get_config, GlobalConfigArgs},
|
||||
database_operations::{create_database, drop_database, get_database_list},
|
||||
database_privilege_operations,
|
||||
cli::{
|
||||
common::erroneous_server_response,
|
||||
database_command,
|
||||
mysql_admutils_compatibility::{
|
||||
common::trim_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,
|
||||
protocol::{
|
||||
create_client_to_server_message_stream, ClientToServerMessageStream,
|
||||
GetDatabasesPrivilegeDataError, Request, Response,
|
||||
},
|
||||
},
|
||||
server::sql::database_privilege_operations::DatabasePrivilegeRow,
|
||||
};
|
||||
|
||||
const HELP_DB_PERM: &str = r#"
|
||||
@@ -39,8 +54,25 @@ pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
#[command(flatten)]
|
||||
config_overrides: GlobalConfigArgs,
|
||||
/// 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)]
|
||||
@@ -76,7 +108,7 @@ pub enum Command {
|
||||
/// to make changes to the permission table.
|
||||
/// Run 'mysql-dbadm --help-editperm' for more
|
||||
/// information.
|
||||
EditPerm(EditPermArgs),
|
||||
Editperm(EditPermArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -106,7 +138,7 @@ pub struct EditPermArgs {
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
if args.help_editperm {
|
||||
@@ -114,6 +146,9 @@ pub async fn main() -> anyhow::Result<()> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server_connection =
|
||||
bootstrap_server_connection_and_drop_privileges(args.server_socket_path, args.config)?;
|
||||
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
@@ -125,64 +160,164 @@ pub async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let config = get_config(args.config_overrides)?;
|
||||
let mut connection = create_mysql_connection_from_config(config.mysql).await?;
|
||||
tokio_run_command(command, server_connection)?;
|
||||
|
||||
match command {
|
||||
Command::Create(args) => {
|
||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
||||
for name in filtered_names {
|
||||
create_database(&name, &mut connection).await?;
|
||||
println!("Database {} created.", name);
|
||||
}
|
||||
}
|
||||
Command::Drop(args) => {
|
||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
||||
for name in filtered_names {
|
||||
drop_database(&name, &mut connection).await?;
|
||||
println!("Database {} dropped.", name);
|
||||
}
|
||||
}
|
||||
Command::Show(args) => {
|
||||
let names = if args.name.is_empty() {
|
||||
get_database_list(&mut connection).await?
|
||||
} else {
|
||||
filter_db_or_user_names(args.name, DbOrUser::Database)?
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
for name in names {
|
||||
show_db(&name, &mut connection).await?;
|
||||
}
|
||||
}
|
||||
Command::EditPerm(args) => {
|
||||
// TODO: This does not accurately replicate the behavior of the old implementation.
|
||||
// Hopefully, not many people rely on this in an automated fashion, as it
|
||||
// is made to be interactive in nature. However, we should still try to
|
||||
// replicate the old behavior as closely as possible.
|
||||
let edit_privileges_args = database_command::DatabaseEditPrivsArgs {
|
||||
name: Some(args.database),
|
||||
privs: vec![],
|
||||
json: false,
|
||||
editor: None,
|
||||
yes: false,
|
||||
};
|
||||
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 = database_command::DatabaseEditPrivsArgs {
|
||||
name: Some(args.database),
|
||||
privs: vec![],
|
||||
json: false,
|
||||
// TODO: use this to mimic the old editor-finding logic
|
||||
editor: None,
|
||||
yes: false,
|
||||
};
|
||||
|
||||
database_command::edit_privileges(edit_privileges_args, &mut connection).await?;
|
||||
database_command::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(|name| trim_to_32_chars(name))
|
||||
.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 show_db(name: &str, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||
async fn drop_databases(
|
||||
args: DatabaseDropArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let database_names = args
|
||||
.name
|
||||
.iter()
|
||||
.map(|name| trim_to_32_chars(name))
|
||||
.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<String> = args
|
||||
.name
|
||||
.iter()
|
||||
.map(|name| trim_to_32_chars(name))
|
||||
.collect();
|
||||
|
||||
let message = if database_names.is_empty() {
|
||||
let message = Request::ListDatabases;
|
||||
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),
|
||||
};
|
||||
|
||||
Request::ListPrivileges(Some(databases))
|
||||
} 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.
|
||||
// This function should *not* check for db existence, only
|
||||
// validate the names.
|
||||
let privileges = database_privilege_operations::get_database_privileges(name, connection)
|
||||
.await
|
||||
.unwrap_or(vec![]);
|
||||
let results: Vec<Result<(String, Vec<DatabasePrivilegeRow>), String>> = match response {
|
||||
Some(Ok(Response::ListPrivileges(result))) => result
|
||||
.into_iter()
|
||||
.map(|(name, rows)| match rows.map(|rows| (name.clone(), 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",
|
||||
@@ -191,10 +326,10 @@ async fn show_db(name: &str, connection: &mut MySqlConnection) -> anyhow::Result
|
||||
),
|
||||
name,
|
||||
);
|
||||
if privileges.is_empty() {
|
||||
if rows.is_empty() {
|
||||
println!("# (no permissions currently granted to any users)");
|
||||
} else {
|
||||
for privilege in privileges {
|
||||
for privilege in rows {
|
||||
println!(
|
||||
" {:<16} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
|
||||
privilege.user,
|
||||
|
@@ -1,13 +1,28 @@
|
||||
use clap::Parser;
|
||||
use sqlx::MySqlConnection;
|
||||
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::{
|
||||
cli::{mysql_admutils_compatibility::common::filter_db_or_user_names, user_command},
|
||||
core::{
|
||||
common::{close_database_connection, get_current_unix_user, DbOrUser},
|
||||
config::{create_mysql_connection_from_config, get_config, GlobalConfigArgs},
|
||||
user_operations::*,
|
||||
cli::{
|
||||
common::erroneous_server_response,
|
||||
mysql_admutils_compatibility::{
|
||||
common::trim_to_32_chars,
|
||||
error_messages::{
|
||||
handle_create_user_error, handle_drop_user_error, handle_list_users_error,
|
||||
},
|
||||
},
|
||||
user_command::read_password_from_stdin_with_double_check,
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
protocol::{
|
||||
create_client_to_server_message_stream, ClientToServerMessageStream, Request, Response,
|
||||
},
|
||||
},
|
||||
server::sql::user_operations::DatabaseUser,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -15,8 +30,25 @@ pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
#[command(flatten)]
|
||||
config_overrides: GlobalConfigArgs,
|
||||
/// 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>,
|
||||
}
|
||||
|
||||
/// Create, delete or change password for the USER(s),
|
||||
@@ -69,7 +101,7 @@ pub struct ShowArgs {
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let command = match args.command {
|
||||
@@ -85,78 +117,185 @@ pub async fn main() -> anyhow::Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
let config = get_config(args.config_overrides)?;
|
||||
let mut connection = create_mysql_connection_from_config(config.mysql).await?;
|
||||
let server_connection =
|
||||
bootstrap_server_connection_and_drop_privileges(args.server_socket_path, args.config)?;
|
||||
|
||||
match command {
|
||||
Command::Create(args) => {
|
||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::User)?;
|
||||
for name in filtered_names {
|
||||
create_database_user(&name, &mut connection).await?;
|
||||
}
|
||||
}
|
||||
Command::Delete(args) => {
|
||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::User)?;
|
||||
for name in filtered_names {
|
||||
delete_database_user(&name, &mut connection).await?;
|
||||
}
|
||||
}
|
||||
Command::Passwd(args) => passwd(args, &mut connection).await?,
|
||||
Command::Show(args) => show(args, &mut connection).await?,
|
||||
}
|
||||
|
||||
close_database_connection(connection).await;
|
||||
tokio_run_command(command, server_connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn passwd(args: PasswdArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::User)?;
|
||||
|
||||
// NOTE: this gets doubly checked during the call to `set_password_for_database_user`.
|
||||
// This is moving the check before asking the user for the password,
|
||||
// to avoid having them figure out that the user does not exist after they
|
||||
// have entered the password twice.
|
||||
let mut better_filtered_names = Vec::with_capacity(filtered_names.len());
|
||||
for name in filtered_names.into_iter() {
|
||||
if !user_exists(&name, connection).await? {
|
||||
println!(
|
||||
"{}: User '{}' does not exist. You must create it first.",
|
||||
std::env::args()
|
||||
.next()
|
||||
.unwrap_or("mysql-useradm".to_string()),
|
||||
name,
|
||||
);
|
||||
} else {
|
||||
better_filtered_names.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
for name in better_filtered_names {
|
||||
let password = user_command::read_password_from_stdin_with_double_check(&name)?;
|
||||
set_password_for_database_user(&name, &password, connection).await?;
|
||||
println!("Password updated for user '{}'.", name);
|
||||
}
|
||||
|
||||
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 show(args: ShowArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||
let users = if args.name.is_empty() {
|
||||
let unix_user = get_current_unix_user()?;
|
||||
get_all_database_users_for_unix_user(&unix_user, connection).await?
|
||||
} else {
|
||||
let filtered_usernames = filter_db_or_user_names(args.name, DbOrUser::User)?;
|
||||
let mut result = Vec::with_capacity(filtered_usernames.len());
|
||||
for username in filtered_usernames.iter() {
|
||||
// TODO: fetch all users in one query
|
||||
if let Some(user) = get_database_user_for_user(username, connection).await? {
|
||||
result.push(user)
|
||||
}
|
||||
}
|
||||
result
|
||||
async fn create_user(
|
||||
args: CreateArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let usernames = args
|
||||
.name
|
||||
.iter()
|
||||
.map(|name| trim_to_32_chars(name))
|
||||
.collect();
|
||||
|
||||
let message = Request::CreateUsers(usernames);
|
||||
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 usernames = args
|
||||
.name
|
||||
.iter()
|
||||
.map(|name| trim_to_32_chars(name))
|
||||
.collect();
|
||||
|
||||
let message = Request::DropUsers(usernames);
|
||||
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 usernames = args
|
||||
.name
|
||||
.iter()
|
||||
.map(|name| trim_to_32_chars(name))
|
||||
.collect();
|
||||
|
||||
let message = Request::ListUsers(Some(usernames));
|
||||
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.clone(), 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 usernames: Vec<_> = args
|
||||
.name
|
||||
.iter()
|
||||
.map(|name| trim_to_32_chars(name))
|
||||
.collect();
|
||||
|
||||
let message = if usernames.is_empty() {
|
||||
Request::ListUsers(None)
|
||||
} else {
|
||||
Request::ListUsers(Some(usernames))
|
||||
};
|
||||
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);
|
||||
|
@@ -1,27 +1,24 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::vec;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::{Confirm, Password};
|
||||
use prettytable::Table;
|
||||
use serde_json::json;
|
||||
use sqlx::{Connection, MySqlConnection};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
|
||||
use crate::core::{
|
||||
common::{close_database_connection, get_current_unix_user, CommandStatus},
|
||||
database_operations::*,
|
||||
user_operations::*,
|
||||
use crate::core::protocol::{
|
||||
print_create_users_output_status, print_drop_users_output_status,
|
||||
print_lock_users_output_status, print_set_password_output_status,
|
||||
print_unlock_users_output_status, ClientToServerMessageStream, Request, Response,
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
use super::common::erroneous_server_response;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserArgs {
|
||||
#[clap(subcommand)]
|
||||
subcmd: UserCommand,
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub enum UserCommand {
|
||||
/// Create one or more users
|
||||
#[command()]
|
||||
@@ -50,7 +47,7 @@ pub enum UserCommand {
|
||||
UnlockUser(UserUnlockArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserCreateArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<String>,
|
||||
@@ -60,13 +57,13 @@ pub struct UserCreateArgs {
|
||||
no_password: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserDeleteArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserPasswdArgs {
|
||||
username: String,
|
||||
|
||||
@@ -74,7 +71,7 @@ pub struct UserPasswdArgs {
|
||||
password_file: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserShowArgs {
|
||||
#[arg(num_args = 0..)]
|
||||
username: Vec<String>,
|
||||
@@ -83,13 +80,13 @@ pub struct UserShowArgs {
|
||||
json: bool,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserLockArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UserUnlockArgs {
|
||||
#[arg(num_args = 1..)]
|
||||
username: Vec<String>,
|
||||
@@ -97,48 +94,45 @@ pub struct UserUnlockArgs {
|
||||
|
||||
pub async fn handle_command(
|
||||
command: UserCommand,
|
||||
mut connection: MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
let result = connection
|
||||
.transaction(|txn| {
|
||||
Box::pin(async move {
|
||||
match command {
|
||||
UserCommand::CreateUser(args) => create_users(args, txn).await,
|
||||
UserCommand::DropUser(args) => drop_users(args, txn).await,
|
||||
UserCommand::PasswdUser(args) => change_password_for_user(args, txn).await,
|
||||
UserCommand::ShowUser(args) => show_users(args, txn).await,
|
||||
UserCommand::LockUser(args) => lock_users(args, txn).await,
|
||||
UserCommand::UnlockUser(args) => unlock_users(args, txn).await,
|
||||
}
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
close_database_connection(connection).await;
|
||||
|
||||
result
|
||||
server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
match command {
|
||||
UserCommand::CreateUser(args) => create_users(args, server_connection).await,
|
||||
UserCommand::DropUser(args) => drop_users(args, server_connection).await,
|
||||
UserCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
||||
UserCommand::ShowUser(args) => show_users(args, server_connection).await,
|
||||
UserCommand::LockUser(args) => lock_users(args, server_connection).await,
|
||||
UserCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_users(
|
||||
args: UserCreateArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.username.is_empty() {
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let mut result = CommandStatus::SuccessfullyModified;
|
||||
let message = Request::CreateUsers(args.username.clone());
|
||||
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"));
|
||||
}
|
||||
|
||||
for username in args.username {
|
||||
if let Err(e) = create_database_user(&username, connection).await {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("Skipping...\n");
|
||||
result = CommandStatus::PartiallySuccessfullyModified;
|
||||
continue;
|
||||
} else {
|
||||
println!("User '{}' created.", username);
|
||||
}
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CreateUsers(result))) => result,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
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!(
|
||||
@@ -147,41 +141,55 @@ async fn create_users(
|
||||
))
|
||||
.interact()?
|
||||
{
|
||||
change_password_for_user(
|
||||
UserPasswdArgs {
|
||||
username,
|
||||
password_file: None,
|
||||
},
|
||||
connection,
|
||||
)
|
||||
.await?;
|
||||
let password = read_password_from_stdin_with_double_check(username)?;
|
||||
let message = Request::PasswdUser(username.clone(), 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!();
|
||||
}
|
||||
println!();
|
||||
}
|
||||
Ok(result)
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn drop_users(
|
||||
args: UserDeleteArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.username.is_empty() {
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let mut result = CommandStatus::SuccessfullyModified;
|
||||
let message = Request::DropUsers(args.username.clone());
|
||||
|
||||
for username in args.username {
|
||||
if let Err(e) = delete_database_user(&username, connection).await {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("Skipping...");
|
||||
result = CommandStatus::PartiallySuccessfullyModified;
|
||||
} else {
|
||||
println!("User '{}' dropped.", username);
|
||||
}
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(err);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
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?;
|
||||
|
||||
print_drop_users_output_status(&result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Result<String> {
|
||||
@@ -195,15 +203,10 @@ pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Res
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn change_password_for_user(
|
||||
async fn passwd_user(
|
||||
args: UserPasswdArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
// NOTE: although this also is checked in `set_password_for_database_user`, we check it here
|
||||
// to provide a more natural order of error messages.
|
||||
let unix_user = get_current_unix_user()?;
|
||||
validate_user_name(&args.username, &unix_user)?;
|
||||
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let password = if let Some(password_file) = args.password_file {
|
||||
std::fs::read_to_string(password_file)
|
||||
.context("Failed to read password file")?
|
||||
@@ -213,129 +216,146 @@ async fn change_password_for_user(
|
||||
read_password_from_stdin_with_double_check(&args.username)?
|
||||
};
|
||||
|
||||
set_password_for_database_user(&args.username, &password, connection).await?;
|
||||
let message = Request::PasswdUser(args.username.clone(), password);
|
||||
|
||||
Ok(CommandStatus::SuccessfullyModified)
|
||||
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,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
let unix_user = get_current_unix_user()?;
|
||||
|
||||
let users = if args.username.is_empty() {
|
||||
get_all_database_users_for_unix_user(&unix_user, connection).await?
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let message = if args.username.is_empty() {
|
||||
Request::ListUsers(None)
|
||||
} else {
|
||||
let mut result = vec![];
|
||||
for username in args.username {
|
||||
if let Err(e) = validate_user_name(&username, &unix_user) {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = get_database_user_for_user(&username, connection).await?;
|
||||
if let Some(user) = user {
|
||||
result.push(user);
|
||||
} else {
|
||||
eprintln!("User not found: {}", username);
|
||||
}
|
||||
}
|
||||
result
|
||||
Request::ListUsers(Some(args.username.clone()))
|
||||
};
|
||||
|
||||
let mut user_databases: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
for user in users.iter() {
|
||||
user_databases.insert(
|
||||
user.user.clone(),
|
||||
get_databases_where_user_has_privileges(&user.user, connection).await?,
|
||||
);
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(err);
|
||||
}
|
||||
|
||||
if args.json {
|
||||
let users_json = users
|
||||
let users = match server_connection.next().await {
|
||||
Some(Ok(Response::ListUsers(users))) => users
|
||||
.into_iter()
|
||||
.map(|user| {
|
||||
json!({
|
||||
"user": user.user,
|
||||
"has_password": user.has_password,
|
||||
"is_locked": user.is_locked,
|
||||
"databases": user_databases.get(&user.user).unwrap_or(&vec![]),
|
||||
})
|
||||
.filter_map(|(username, result)| match result {
|
||||
Ok(user) => Some(user),
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(&username));
|
||||
eprintln!("Skipping...");
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<serde_json::Value>();
|
||||
.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?;
|
||||
|
||||
// TODO: print databases where user has privileges
|
||||
if args.json {
|
||||
println!(
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&users_json)
|
||||
.context("Failed to serialize users to JSON")?
|
||||
serde_json::to_string_pretty(&users).context("Failed to serialize users to JSON")?
|
||||
);
|
||||
} else if users.is_empty() {
|
||||
println!("No users found.");
|
||||
println!("No users to show.");
|
||||
} else {
|
||||
let mut table = Table::new();
|
||||
let mut table = prettytable::Table::new();
|
||||
table.add_row(row![
|
||||
"User",
|
||||
"Password is set",
|
||||
"Locked",
|
||||
"Databases where user has privileges"
|
||||
// "Databases where user has privileges"
|
||||
]);
|
||||
for user in users {
|
||||
table.add_row(row![
|
||||
user.user,
|
||||
user.has_password,
|
||||
user.is_locked,
|
||||
user_databases.get(&user.user).unwrap_or(&vec![]).join("\n")
|
||||
// user.databases.join("\n")
|
||||
]);
|
||||
}
|
||||
table.printstd();
|
||||
}
|
||||
|
||||
Ok(CommandStatus::NoModificationsIntended)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn lock_users(
|
||||
args: UserLockArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.username.is_empty() {
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let mut result = CommandStatus::SuccessfullyModified;
|
||||
let message = Request::LockUsers(args.username.clone());
|
||||
|
||||
for username in args.username {
|
||||
if let Err(e) = lock_database_user(&username, connection).await {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("Skipping...");
|
||||
result = CommandStatus::PartiallySuccessfullyModified;
|
||||
} else {
|
||||
println!("User '{}' locked.", username);
|
||||
}
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(err);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
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?;
|
||||
|
||||
print_lock_users_output_status(&result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn unlock_users(
|
||||
args: UserUnlockArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> anyhow::Result<CommandStatus> {
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
if args.username.is_empty() {
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let mut result = CommandStatus::SuccessfullyModified;
|
||||
let message = Request::UnlockUsers(args.username.clone());
|
||||
|
||||
for username in args.username {
|
||||
if let Err(e) = unlock_database_user(&username, connection).await {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("Skipping...");
|
||||
result = CommandStatus::PartiallySuccessfullyModified;
|
||||
} else {
|
||||
println!("User '{}' unlocked.", username);
|
||||
}
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(err);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
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?;
|
||||
|
||||
print_unlock_users_output_status(&result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Reference in New Issue
Block a user