Rewrite entire codebase to split into client and server

This commit is contained in:
2024-08-10 02:16:38 +02:00
parent 20e60ca5c7
commit af86893acf
32 changed files with 3708 additions and 1599 deletions

20
src/cli/common.rs Normal file
View 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");
}
}
}

View File

@@ -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"),
}
}

View File

@@ -1,3 +1,4 @@
pub mod common;
mod error_messages;
pub mod mysql_dbadm;
pub mod mysql_useradm;

View File

@@ -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()
}

View 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)
}
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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(())
}