Misc #35
|
@ -269,6 +269,19 @@ dependencies = [
|
||||||
"static_assertions",
|
"static_assertions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "console"
|
||||||
|
version = "0.15.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb"
|
||||||
|
dependencies = [
|
||||||
|
"encode_unicode 0.3.6",
|
||||||
|
"lazy_static",
|
||||||
|
"libc",
|
||||||
|
"unicode-width",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
@ -381,6 +394,19 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dialoguer"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||||
|
dependencies = [
|
||||||
|
"console",
|
||||||
|
"shell-words",
|
||||||
|
"tempfile",
|
||||||
|
"thiserror",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
@ -420,16 +446,6 @@ version = "0.15.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "edit"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f364860e764787163c8c8f58231003839be31276e821e2ad2092ddf496b1aa09"
|
|
||||||
dependencies = [
|
|
||||||
"tempfile",
|
|
||||||
"which",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "either"
|
name = "either"
|
||||||
version = "1.11.0"
|
version = "1.11.0"
|
||||||
|
@ -439,6 +455,12 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encode_unicode"
|
||||||
|
version = "0.3.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "encode_unicode"
|
name = "encode_unicode"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
@ -885,7 +907,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"edit",
|
"dialoguer",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"indoc",
|
"indoc",
|
||||||
"itertools",
|
"itertools",
|
||||||
|
@ -893,7 +915,6 @@ dependencies = [
|
||||||
"nix",
|
"nix",
|
||||||
"prettytable",
|
"prettytable",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"rpassword",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
@ -970,16 +991,6 @@ dependencies = [
|
||||||
"libm",
|
"libm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num_cpus"
|
|
||||||
version = "1.16.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
|
||||||
dependencies = [
|
|
||||||
"hermit-abi",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "object"
|
name = "object"
|
||||||
version = "0.32.2"
|
version = "0.32.2"
|
||||||
|
@ -1091,7 +1102,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781"
|
checksum = "46480520d1b77c9a3482d39939fcf96831537a250ec62d4fd8fbdf8e0302e781"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"csv",
|
"csv",
|
||||||
"encode_unicode",
|
"encode_unicode 1.0.0",
|
||||||
"is-terminal",
|
"is-terminal",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"term",
|
"term",
|
||||||
|
@ -1230,17 +1241,6 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rpassword"
|
|
||||||
version = "7.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"rtoolbox",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
|
@ -1261,16 +1261,6 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rtoolbox"
|
|
||||||
version = "0.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"windows-sys 0.48.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-demangle"
|
name = "rustc-demangle"
|
||||||
version = "0.1.23"
|
version = "0.1.23"
|
||||||
|
@ -1410,6 +1400,12 @@ dependencies = [
|
||||||
"digest",
|
"digest",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "shell-words"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook"
|
name = "signal-hook"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
|
@ -1858,7 +1854,6 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
"num_cpus",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
@ -2057,18 +2052,6 @@ version = "0.25.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "which"
|
|
||||||
version = "4.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
|
|
||||||
dependencies = [
|
|
||||||
"either",
|
|
||||||
"home",
|
|
||||||
"once_cell",
|
|
||||||
"rustix",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "whoami"
|
name = "whoami"
|
||||||
version = "1.5.1"
|
version = "1.5.1"
|
||||||
|
|
|
@ -6,7 +6,7 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
clap = { version = "4.5.4", features = ["derive"] }
|
clap = { version = "4.5.4", features = ["derive"] }
|
||||||
edit = "0.1.5"
|
dialoguer = "0.11.0"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
indoc = "2.0.5"
|
indoc = "2.0.5"
|
||||||
itertools = "0.12.1"
|
itertools = "0.12.1"
|
||||||
|
@ -14,11 +14,10 @@ log = "0.4.21"
|
||||||
nix = { version = "0.28.0", features = ["user"] }
|
nix = { version = "0.28.0", features = ["user"] }
|
||||||
prettytable = "0.10.0"
|
prettytable = "0.10.0"
|
||||||
ratatui = { version = "0.26.2", optional = true }
|
ratatui = { version = "0.26.2", optional = true }
|
||||||
rpassword = "7.3.1"
|
|
||||||
serde = "1.0.198"
|
serde = "1.0.198"
|
||||||
serde_json = "1.0.116"
|
serde_json = "1.0.116"
|
||||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "mysql", "tls-rustls"] }
|
sqlx = { version = "0.7.4", features = ["runtime-tokio", "mysql", "tls-rustls"] }
|
||||||
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.37.0", features = ["rt", "macros"] }
|
||||||
toml = "0.8.12"
|
toml = "0.8.12"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
10
build.rs
10
build.rs
|
@ -17,20 +17,24 @@ fn main() -> anyhow::Result<()> {
|
||||||
.ok_or(anyhow!("Could not resolve target profile directory"))?
|
.ok_or(anyhow!("Could not resolve target profile directory"))?
|
||||||
.to_path_buf();
|
.to_path_buf();
|
||||||
|
|
||||||
dbg!(&target_profile_dir);
|
if !target_profile_dir.exists() {
|
||||||
|
std::fs::create_dir_all(&target_profile_dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
if !target_profile_dir.join("mysql-useradm").exists() {
|
if !target_profile_dir.join("mysql-useradm").exists() {
|
||||||
symlink(
|
symlink(
|
||||||
target_profile_dir.join("mysqladm"),
|
target_profile_dir.join("mysqladm"),
|
||||||
target_profile_dir.join("mysql-useradm"),
|
target_profile_dir.join("mysql-useradm"),
|
||||||
)?;
|
)
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
if !target_profile_dir.join("mysql-dbadm").exists() {
|
if !target_profile_dir.join("mysql-dbadm").exists() {
|
||||||
symlink(
|
symlink(
|
||||||
target_profile_dir.join("mysqladm"),
|
target_profile_dir.join("mysqladm"),
|
||||||
target_profile_dir.join("mysql-dbadm"),
|
target_profile_dir.join("mysql-dbadm"),
|
||||||
)?;
|
)
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::{anyhow, Context};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use dialoguer::Editor;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use prettytable::{Cell, Row, Table};
|
use prettytable::{Cell, Row, Table};
|
||||||
|
@ -7,7 +8,7 @@ use sqlx::{Connection, MySqlConnection};
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
self,
|
self,
|
||||||
common::get_current_unix_user,
|
common::{close_database_connection, get_current_unix_user},
|
||||||
database_operations::{
|
database_operations::{
|
||||||
apply_permission_diffs, db_priv_field_human_readable_name, diff_permissions, yn,
|
apply_permission_diffs, db_priv_field_human_readable_name, diff_permissions, yn,
|
||||||
DatabasePrivileges, DATABASE_PRIVILEGE_FIELDS,
|
DatabasePrivileges, DATABASE_PRIVILEGE_FIELDS,
|
||||||
|
@ -15,9 +16,6 @@ use crate::core::{
|
||||||
user_operations::user_exists,
|
user_operations::user_exists,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Support batch creation/dropping,showing of databases,
|
|
||||||
// using a comma-separated list of database names.
|
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
// #[command(next_help_heading = Some(DATABASE_COMMAND_HEADER))]
|
// #[command(next_help_heading = Some(DATABASE_COMMAND_HEADER))]
|
||||||
pub enum DatabaseCommand {
|
pub enum DatabaseCommand {
|
||||||
|
@ -147,15 +145,21 @@ pub async fn handle_command(
|
||||||
command: DatabaseCommand,
|
command: DatabaseCommand,
|
||||||
mut conn: MySqlConnection,
|
mut conn: MySqlConnection,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let result = match command {
|
let result = conn
|
||||||
DatabaseCommand::CreateDb(args) => create_databases(args, &mut conn).await,
|
.transaction(|txn| {
|
||||||
DatabaseCommand::DropDb(args) => drop_databases(args, &mut conn).await,
|
Box::pin(async move {
|
||||||
DatabaseCommand::ListDb(args) => list_databases(args, &mut conn).await,
|
match command {
|
||||||
DatabaseCommand::ShowDbPerm(args) => show_databases(args, &mut conn).await,
|
DatabaseCommand::CreateDb(args) => create_databases(args, txn).await,
|
||||||
DatabaseCommand::EditDbPerm(args) => edit_permissions(args, &mut conn).await,
|
DatabaseCommand::DropDb(args) => drop_databases(args, txn).await,
|
||||||
};
|
DatabaseCommand::ListDb(args) => list_databases(args, txn).await,
|
||||||
|
DatabaseCommand::ShowDbPerm(args) => show_databases(args, txn).await,
|
||||||
|
DatabaseCommand::EditDbPerm(args) => edit_permissions(args, txn).await,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
conn.close().await?;
|
close_database_connection(conn).await;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
@ -494,27 +498,32 @@ pub async fn edit_permissions(
|
||||||
longest_database_name,
|
longest_database_name,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result = edit::edit_with_builder(
|
// TODO: handle errors better here
|
||||||
format!(
|
let result = Editor::new()
|
||||||
"{}\n{}\n{}",
|
.extension("tsv")
|
||||||
comment,
|
.edit(
|
||||||
header.join(" "),
|
format!(
|
||||||
if permission_data.is_empty() {
|
"{}\n{}\n{}",
|
||||||
format!("# {}", example_line)
|
comment,
|
||||||
} else {
|
header.join(" "),
|
||||||
permission_data
|
if permission_data.is_empty() {
|
||||||
.iter()
|
format!("# {}", example_line)
|
||||||
.map(|perm| {
|
} else {
|
||||||
format_privileges_line(perm, longest_username, longest_database_name)
|
permission_data
|
||||||
})
|
.iter()
|
||||||
.join("\n")
|
.map(|perm| {
|
||||||
}
|
format_privileges_line(
|
||||||
),
|
perm,
|
||||||
edit::Builder::new()
|
longest_username,
|
||||||
.prefix("database-permissions")
|
longest_database_name,
|
||||||
.suffix(".tsv")
|
)
|
||||||
.rand_bytes(10),
|
})
|
||||||
)?;
|
.join("\n")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
parse_permission_data_from_editor(result)
|
parse_permission_data_from_editor(result)
|
||||||
.context("Could not parse permission data from editor")?
|
.context("Could not parse permission data from editor")?
|
||||||
|
|
|
@ -7,11 +7,11 @@ use crate::{
|
||||||
user_command,
|
user_command,
|
||||||
},
|
},
|
||||||
core::{
|
core::{
|
||||||
common::get_current_unix_user,
|
common::{close_database_connection, get_current_unix_user},
|
||||||
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
||||||
user_operations::{
|
user_operations::{
|
||||||
create_database_user, delete_database_user, get_all_database_users_for_unix_user,
|
create_database_user, delete_database_user, get_all_database_users_for_unix_user,
|
||||||
password_is_set_for_database_user, set_password_for_database_user, user_exists,
|
get_database_user_for_user, set_password_for_database_user, user_exists,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -107,14 +107,16 @@ pub async fn main() -> anyhow::Result<()> {
|
||||||
delete_database_user(&name, &mut connection).await?;
|
delete_database_user(&name, &mut connection).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Passwd(args) => passwd(args, connection).await?,
|
Command::Passwd(args) => passwd(args, &mut connection).await?,
|
||||||
Command::Show(args) => show(args, connection).await?,
|
Command::Show(args) => show(args, &mut connection).await?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close_database_connection(connection).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn passwd(args: PasswdArgs, mut connection: MySqlConnection) -> anyhow::Result<()> {
|
async fn passwd(args: PasswdArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::User)?;
|
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`.
|
// NOTE: this gets doubly checked during the call to `set_password_for_database_user`.
|
||||||
|
@ -123,7 +125,7 @@ async fn passwd(args: PasswdArgs, mut connection: MySqlConnection) -> anyhow::Re
|
||||||
// have entered the password twice.
|
// have entered the password twice.
|
||||||
let mut better_filtered_names = Vec::with_capacity(filtered_names.len());
|
let mut better_filtered_names = Vec::with_capacity(filtered_names.len());
|
||||||
for name in filtered_names.into_iter() {
|
for name in filtered_names.into_iter() {
|
||||||
if !user_exists(&name, &mut connection).await? {
|
if !user_exists(&name, connection).await? {
|
||||||
println!(
|
println!(
|
||||||
"{}: User '{}' does not exist. You must create it first.",
|
"{}: User '{}' does not exist. You must create it first.",
|
||||||
std::env::args()
|
std::env::args()
|
||||||
|
@ -138,32 +140,34 @@ async fn passwd(args: PasswdArgs, mut connection: MySqlConnection) -> anyhow::Re
|
||||||
|
|
||||||
for name in better_filtered_names {
|
for name in better_filtered_names {
|
||||||
let password = user_command::read_password_from_stdin_with_double_check(&name)?;
|
let password = user_command::read_password_from_stdin_with_double_check(&name)?;
|
||||||
set_password_for_database_user(&name, &password, &mut connection).await?;
|
set_password_for_database_user(&name, &password, connection).await?;
|
||||||
println!("Password updated for user '{}'.", name);
|
println!("Password updated for user '{}'.", name);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show(args: ShowArgs, mut connection: MySqlConnection) -> anyhow::Result<()> {
|
async fn show(args: ShowArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
let users = if args.name.is_empty() {
|
let users = if args.name.is_empty() {
|
||||||
let unix_user = get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
get_all_database_users_for_unix_user(&unix_user, &mut connection)
|
get_all_database_users_for_unix_user(&unix_user, connection).await?
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(|u| u.user)
|
|
||||||
.collect()
|
|
||||||
} else {
|
} else {
|
||||||
filter_db_or_user_names(args.name, DbOrUser::User)?
|
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
|
||||||
};
|
};
|
||||||
|
|
||||||
for user in users {
|
for user in users {
|
||||||
let password_is_set = password_is_set_for_database_user(&user, &mut connection).await?;
|
if user.has_password {
|
||||||
|
println!("User '{}': password set.", user.user);
|
||||||
match password_is_set {
|
} else {
|
||||||
Some(true) => println!("User '{}': password set.", user),
|
println!("User '{}': no password set.", user.user);
|
||||||
Some(false) => println!("User '{}': no password set.", user),
|
|
||||||
None => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@ use std::vec;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
use dialoguer::{Confirm, Password};
|
||||||
use sqlx::{Connection, MySqlConnection};
|
use sqlx::{Connection, MySqlConnection};
|
||||||
|
|
||||||
use crate::core::user_operations::validate_user_name;
|
use crate::core::{common::close_database_connection, user_operations::validate_user_name};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct UserArgs {
|
pub struct UserArgs {
|
||||||
|
@ -37,6 +38,10 @@ pub enum UserCommand {
|
||||||
pub struct UserCreateArgs {
|
pub struct UserCreateArgs {
|
||||||
#[arg(num_args = 1..)]
|
#[arg(num_args = 1..)]
|
||||||
username: Vec<String>,
|
username: Vec<String>,
|
||||||
|
|
||||||
|
/// Do not ask for a password, leave it unset
|
||||||
|
#[clap(long)]
|
||||||
|
no_password: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -57,22 +62,30 @@ pub struct UserPasswdArgs {
|
||||||
pub struct UserShowArgs {
|
pub struct UserShowArgs {
|
||||||
#[arg(num_args = 0..)]
|
#[arg(num_args = 0..)]
|
||||||
username: Vec<String>,
|
username: Vec<String>,
|
||||||
|
|
||||||
|
#[clap(short, long)]
|
||||||
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_command(command: UserCommand, mut conn: MySqlConnection) -> anyhow::Result<()> {
|
pub async fn handle_command(command: UserCommand, mut conn: MySqlConnection) -> anyhow::Result<()> {
|
||||||
let result = match command {
|
let result = conn
|
||||||
UserCommand::CreateUser(args) => create_users(args, &mut conn).await,
|
.transaction(|txn| {
|
||||||
UserCommand::DropUser(args) => drop_users(args, &mut conn).await,
|
Box::pin(async move {
|
||||||
UserCommand::PasswdUser(args) => change_password_for_user(args, &mut conn).await,
|
match command {
|
||||||
UserCommand::ShowUser(args) => show_users(args, &mut conn).await,
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
conn.close().await?;
|
close_database_connection(conn).await;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: provide a better error message when the user already exists
|
|
||||||
async fn create_users(args: UserCreateArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn create_users(args: UserCreateArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
if args.username.is_empty() {
|
if args.username.is_empty() {
|
||||||
anyhow::bail!("No usernames provided");
|
anyhow::bail!("No usernames provided");
|
||||||
|
@ -81,13 +94,34 @@ async fn create_users(args: UserCreateArgs, conn: &mut MySqlConnection) -> anyho
|
||||||
for username in args.username {
|
for username in args.username {
|
||||||
if let Err(e) = crate::core::user_operations::create_database_user(&username, conn).await {
|
if let Err(e) = crate::core::user_operations::create_database_user(&username, conn).await {
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...\n");
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
println!("User '{}' created.", username);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !args.no_password
|
||||||
|
&& Confirm::new()
|
||||||
|
.with_prompt(format!(
|
||||||
|
"Do you want to set a password for user '{}'?",
|
||||||
|
username
|
||||||
|
))
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
change_password_for_user(
|
||||||
|
UserPasswdArgs {
|
||||||
|
username,
|
||||||
|
password_file: None,
|
||||||
|
},
|
||||||
|
conn,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
println!();
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: provide a better error message when the user does not exist
|
|
||||||
async fn drop_users(args: UserDeleteArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn drop_users(args: UserDeleteArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
if args.username.is_empty() {
|
if args.username.is_empty() {
|
||||||
anyhow::bail!("No usernames provided");
|
anyhow::bail!("No usernames provided");
|
||||||
|
@ -103,20 +137,14 @@ async fn drop_users(args: UserDeleteArgs, conn: &mut MySqlConnection) -> anyhow:
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Result<String> {
|
pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Result<String> {
|
||||||
let pass1 = rpassword::prompt_password(format!("New MySQL password for user '{}': ", username))
|
Password::new()
|
||||||
.context("Failed to read password")?;
|
.with_prompt(format!("New MySQL password for user '{}'", username))
|
||||||
|
.with_confirmation(
|
||||||
let pass2 = rpassword::prompt_password(format!(
|
format!("Retype new MySQL password for user '{}'", username),
|
||||||
"Retype new MySQL password for user '{}': ",
|
"Passwords do not match",
|
||||||
username
|
)
|
||||||
))
|
.interact()
|
||||||
.context("Failed to read password")?;
|
.map_err(Into::into)
|
||||||
|
|
||||||
if pass1 != pass2 {
|
|
||||||
anyhow::bail!("Passwords do not match");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(pass1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn change_password_for_user(
|
async fn change_password_for_user(
|
||||||
|
@ -168,16 +196,20 @@ async fn show_users(args: UserShowArgs, conn: &mut MySqlConnection) -> anyhow::R
|
||||||
result
|
result
|
||||||
};
|
};
|
||||||
|
|
||||||
for user in users {
|
if args.json {
|
||||||
println!(
|
println!("{}", serde_json::to_string_pretty(&users)?);
|
||||||
"User '{}': {}",
|
} else {
|
||||||
&user.user,
|
for user in users {
|
||||||
if !(user.authentication_string.is_empty() && user.password.is_empty()) {
|
println!(
|
||||||
"password set."
|
"User '{}': {}",
|
||||||
} else {
|
&user.user,
|
||||||
"no password set."
|
if user.has_password {
|
||||||
}
|
"password set."
|
||||||
);
|
} else {
|
||||||
|
"no password set."
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -2,6 +2,7 @@ use anyhow::Context;
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nix::unistd::{getuid, Group, User};
|
use nix::unistd::{getuid, Group, User};
|
||||||
|
use sqlx::{Connection, MySqlConnection};
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(not(target_os = "macos"))]
|
||||||
use std::ffi::CString;
|
use std::ffi::CString;
|
||||||
|
@ -140,10 +141,23 @@ pub fn validate_ownership_by_user_prefix<'a>(
|
||||||
Ok(prefix)
|
Ok(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn close_database_connection(conn: MySqlConnection) {
|
||||||
|
if let Err(e) = conn
|
||||||
|
.close()
|
||||||
|
.await
|
||||||
|
.context("Failed to close connection properly")
|
||||||
|
{
|
||||||
|
eprintln!("{}", e);
|
||||||
|
eprintln!("Ignoring...");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn quote_literal(s: &str) -> String {
|
pub fn quote_literal(s: &str) -> String {
|
||||||
format!("'{}'", s.replace('\'', r"\'"))
|
format!("'{}'", s.replace('\'', r"\'"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
pub fn quote_identifier(s: &str) -> String {
|
pub fn quote_identifier(s: &str) -> String {
|
||||||
format!("`{}`", s.replace('`', r"\`"))
|
format!("`{}`", s.replace('`', r"\`"))
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,30 +93,6 @@ pub async fn set_password_for_database_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper struct to deserialize the query made in `password_is_set_for_database_user`.
|
|
||||||
#[derive(sqlx::FromRow)]
|
|
||||||
#[sqlx(transparent)]
|
|
||||||
struct PasswordIsSet(bool);
|
|
||||||
|
|
||||||
/// This function checks if a database user has a password set.
|
|
||||||
/// It returns `Ok(None)` if the user does not exist.
|
|
||||||
pub async fn password_is_set_for_database_user(
|
|
||||||
db_user: &str,
|
|
||||||
conn: &mut MySqlConnection,
|
|
||||||
) -> anyhow::Result<Option<bool>> {
|
|
||||||
let unix_user = crate::core::common::get_current_unix_user()?;
|
|
||||||
validate_user_name(db_user, &unix_user)?;
|
|
||||||
|
|
||||||
let user_has_password = sqlx::query_as::<_, PasswordIsSet>(
|
|
||||||
"SELECT authentication_string != '' FROM mysql.user WHERE User = ?",
|
|
||||||
)
|
|
||||||
.bind(db_user)
|
|
||||||
.fetch_optional(conn)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(user_has_password.map(|PasswordIsSet(is_set)| is_set))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This struct contains information about a database user.
|
/// This struct contains information about a database user.
|
||||||
/// This can be extended if we need more information in the future.
|
/// This can be extended if we need more information in the future.
|
||||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||||
|
@ -124,13 +100,12 @@ pub struct DatabaseUser {
|
||||||
#[sqlx(rename = "User")]
|
#[sqlx(rename = "User")]
|
||||||
pub user: String,
|
pub user: String,
|
||||||
|
|
||||||
|
#[serde(skip)]
|
||||||
#[sqlx(rename = "Host")]
|
#[sqlx(rename = "Host")]
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
|
||||||
#[sqlx(rename = "Password")]
|
#[sqlx(rename = "`Password` != '' OR `authentication_string` != ''")]
|
||||||
pub password: String,
|
pub has_password: bool,
|
||||||
|
|
||||||
pub authentication_string: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function fetches all database users that have a prefix matching the
|
/// This function fetches all database users that have a prefix matching the
|
||||||
|
@ -141,7 +116,10 @@ pub async fn get_all_database_users_for_unix_user(
|
||||||
) -> anyhow::Result<Vec<DatabaseUser>> {
|
) -> anyhow::Result<Vec<DatabaseUser>> {
|
||||||
let users = sqlx::query_as::<_, DatabaseUser>(
|
let users = sqlx::query_as::<_, DatabaseUser>(
|
||||||
r#"
|
r#"
|
||||||
SELECT `User`, `Host`, `Password`, `authentication_string`
|
SELECT
|
||||||
|
`User`,
|
||||||
|
`Host`,
|
||||||
|
`Password` != '' OR `authentication_string` != ''
|
||||||
FROM `mysql`.`user`
|
FROM `mysql`.`user`
|
||||||
WHERE `User` REGEXP ?
|
WHERE `User` REGEXP ?
|
||||||
"#,
|
"#,
|
||||||
|
@ -160,7 +138,10 @@ pub async fn get_database_user_for_user(
|
||||||
) -> anyhow::Result<Option<DatabaseUser>> {
|
) -> anyhow::Result<Option<DatabaseUser>> {
|
||||||
let user = sqlx::query_as::<_, DatabaseUser>(
|
let user = sqlx::query_as::<_, DatabaseUser>(
|
||||||
r#"
|
r#"
|
||||||
SELECT `User`, `Host`, `Password`, `authentication_string`
|
SELECT
|
||||||
|
`User`,
|
||||||
|
`Host`,
|
||||||
|
`Password` != '' OR `authentication_string` != ''
|
||||||
FROM `mysql`.`user`
|
FROM `mysql`.`user`
|
||||||
WHERE `User` = ?
|
WHERE `User` = ?
|
||||||
"#,
|
"#,
|
||||||
|
|
11
src/main.rs
11
src/main.rs
|
@ -44,7 +44,7 @@ enum Command {
|
||||||
User(cli::user_command::UserCommand),
|
User(cli::user_command::UserCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
|
@ -67,8 +67,15 @@ async fn main() -> anyhow::Result<()> {
|
||||||
let config = core::config::get_config(args.config_overrides)?;
|
let config = core::config::get_config(args.config_overrides)?;
|
||||||
let connection = core::config::mysql_connection_from_config(config).await?;
|
let connection = core::config::mysql_connection_from_config(config).await?;
|
||||||
|
|
||||||
match args.command {
|
let result = match args.command {
|
||||||
Command::Db(command) => cli::database_command::handle_command(command, connection).await,
|
Command::Db(command) => cli::database_command::handle_command(command, connection).await,
|
||||||
Command::User(user_args) => cli::user_command::handle_command(user_args, connection).await,
|
Command::User(user_args) => cli::user_command::handle_command(user_args, connection).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => println!("Changes committed to database"),
|
||||||
|
Err(_) => println!("Changes reverted due to error"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue