Compare commits
2 Commits
password-c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
9f45c2e5da
|
|||
|
107333208c
|
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -285,10 +285,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
@@ -1336,7 +1334,6 @@ dependencies = [
|
||||
"async-bincode",
|
||||
"bincode 2.0.1",
|
||||
"build-info-build",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"clap_complete",
|
||||
|
||||
@@ -22,7 +22,6 @@ autolib = false
|
||||
anyhow = "1.0.100"
|
||||
async-bincode = "0.8.0"
|
||||
bincode = "2.0.1"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.53", features = ["cargo", "derive"] }
|
||||
clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ] }
|
||||
clap_complete = { version = "4.5.62", features = ["unstable-dynamic"] }
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::Confirm;
|
||||
@@ -6,16 +8,15 @@ use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{
|
||||
erroneous_server_response, interactive_password_dialogue_with_double_check,
|
||||
interactive_password_expiry_dialogue, print_authorization_owner_hint,
|
||||
erroneous_server_response, print_authorization_owner_hint,
|
||||
read_password_from_stdin_with_double_check,
|
||||
},
|
||||
core::{
|
||||
completion::prefix_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, CreateUserError, Request, Response,
|
||||
SetUserPasswordRequest, print_create_users_output_status,
|
||||
print_create_users_output_status_json, print_set_password_output_status,
|
||||
request_validation::ValidationError,
|
||||
print_create_users_output_status, print_create_users_output_status_json,
|
||||
print_set_password_output_status, request_validation::ValidationError,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -79,6 +80,15 @@ pub async fn create_users(
|
||||
.filter_map(|(username, result)| result.as_ref().ok().map(|()| username))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !std::io::stdin().is_terminal()
|
||||
&& !args.no_password
|
||||
&& !successfully_created_users.is_empty()
|
||||
{
|
||||
anyhow::bail!(
|
||||
"Cannot prompt for passwords in non-interactive mode. Use --no-password to skip setting passwords."
|
||||
);
|
||||
}
|
||||
|
||||
for username in successfully_created_users {
|
||||
if !args.no_password
|
||||
&& Confirm::new()
|
||||
@@ -88,14 +98,8 @@ pub async fn create_users(
|
||||
.default(false)
|
||||
.interact()?
|
||||
{
|
||||
let password = interactive_password_dialogue_with_double_check(username)?;
|
||||
let expiry = interactive_password_expiry_dialogue(username)?;
|
||||
|
||||
let message = Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: username.clone(),
|
||||
new_password: Some(password),
|
||||
expiry: expiry,
|
||||
});
|
||||
let password = read_password_from_stdin_with_double_check(username)?;
|
||||
let message = Request::PasswdUser((username.to_owned(), password));
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::Confirm;
|
||||
@@ -41,6 +43,12 @@ pub async fn drop_databases(
|
||||
anyhow::bail!("No database names provided");
|
||||
}
|
||||
|
||||
if !std::io::stdin().is_terminal() && !args.yes {
|
||||
anyhow::bail!(
|
||||
"Cannot prompt for confirmation in non-interactive mode. Use --yes to automatically confirm."
|
||||
);
|
||||
}
|
||||
|
||||
if !args.yes {
|
||||
let confirmation = Confirm::new()
|
||||
.with_prompt(format!(
|
||||
@@ -53,7 +61,6 @@ pub async fn drop_databases(
|
||||
))
|
||||
.interact()?;
|
||||
|
||||
//
|
||||
if !confirmation {
|
||||
// TODO: should we return with an error code here?
|
||||
println!("Aborting drop operation.");
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::io::IsTerminal;
|
||||
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::Confirm;
|
||||
@@ -41,6 +43,12 @@ pub async fn drop_users(
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
if !std::io::stdin().is_terminal() && !args.yes {
|
||||
anyhow::bail!(
|
||||
"Cannot prompt for confirmation in non-interactive mode. Use --yes to automatically confirm."
|
||||
);
|
||||
}
|
||||
|
||||
if !args.yes {
|
||||
let confirmation = Confirm::new()
|
||||
.with_prompt(format!(
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
io::IsTerminal,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{Args, Parser};
|
||||
@@ -213,6 +216,11 @@ pub async fn edit_database_privileges(
|
||||
};
|
||||
|
||||
let diffs: BTreeSet<DatabasePrivilegesDiff> = if privs.is_empty() {
|
||||
if !std::io::stdin().is_terminal() {
|
||||
anyhow::bail!(
|
||||
"Cannot launch editor in non-interactive mode. Please provide privileges via command line arguments."
|
||||
);
|
||||
}
|
||||
let privileges_to_change =
|
||||
edit_privileges_with_editor(&existing_privilege_rows, use_database.as_ref())?;
|
||||
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
||||
@@ -275,7 +283,8 @@ pub async fn edit_database_privileges(
|
||||
println!("The following changes will be made:\n");
|
||||
println!("{}", display_privilege_diffs(&diffs));
|
||||
|
||||
if !args.yes
|
||||
if std::io::stdin().is_terminal()
|
||||
&& !args.yes
|
||||
&& !Confirm::new()
|
||||
.with_prompt("Do you want to apply these changes?")
|
||||
.default(false)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::PathBuf;
|
||||
use std::{io::IsTerminal, path::PathBuf};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
@@ -13,8 +13,7 @@ use crate::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListUsersError, Request, Response, SetPasswordError,
|
||||
SetUserPasswordRequest, print_set_password_output_status,
|
||||
request_validation::ValidationError,
|
||||
print_set_password_output_status, request_validation::ValidationError,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -38,21 +37,9 @@ pub struct PasswdUserArgs {
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
|
||||
/// Set the password to expire on the given date (YYYY-MM-DD)
|
||||
#[arg(short, long, value_name = "DATE", conflicts_with = "no-expire")]
|
||||
expire_on: Option<chrono::NaiveDate>,
|
||||
|
||||
/// Set the password to never expire
|
||||
#[arg(short, long, conflicts_with = "expire_on")]
|
||||
no_expire: bool,
|
||||
|
||||
/// Clear the password for the user instead of setting a new one
|
||||
#[arg(short, long, conflicts_with_all = &["password_file", "stdin", "expire_on", "no-expire"])]
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
pub fn interactive_password_dialogue_with_double_check(username: &MySQLUser) -> anyhow::Result<String> {
|
||||
pub fn read_password_from_stdin_with_double_check(username: &MySQLUser) -> anyhow::Result<String> {
|
||||
Password::new()
|
||||
.with_prompt(format!("New MySQL password for user '{username}'"))
|
||||
.with_confirmation(
|
||||
@@ -63,29 +50,6 @@ pub fn interactive_password_dialogue_with_double_check(username: &MySQLUser) ->
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn interactive_password_expiry_dialogue(username: &MySQLUser) -> anyhow::Result<Option<chrono::NaiveDate>> {
|
||||
let input = dialoguer::Input::<String>::new()
|
||||
.with_prompt(format!(
|
||||
"Enter the password expiry date for user '{username}' (YYYY-MM-DD)"
|
||||
))
|
||||
.allow_empty(true)
|
||||
.validate_with(|input: &String| {
|
||||
chrono::NaiveDate::parse_from_str(input, "%Y-%m-%d")
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Invalid date format. Please use YYYY-MM-DD".to_string())
|
||||
})
|
||||
.interact_text()?;
|
||||
|
||||
if input.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let date = chrono::NaiveDate::parse_from_str(&input, "%Y-%m-%d")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse date: {}", e))?;
|
||||
|
||||
Ok(Some(date))
|
||||
}
|
||||
|
||||
pub async fn passwd_user(
|
||||
args: PasswdUserArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
@@ -112,38 +76,27 @@ pub async fn passwd_user(
|
||||
}
|
||||
}
|
||||
|
||||
let password: Option<String> = if let Some(password_file) = args.password_file {
|
||||
Some(
|
||||
std::fs::read_to_string(password_file)
|
||||
.context("Failed to read password file")?
|
||||
.trim()
|
||||
.to_string(),
|
||||
)
|
||||
let password = if let Some(password_file) = args.password_file {
|
||||
std::fs::read_to_string(password_file)
|
||||
.context("Failed to read password file")?
|
||||
.trim()
|
||||
.to_string()
|
||||
} else if args.stdin {
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut buffer)
|
||||
.context("Failed to read password from stdin")?;
|
||||
Some(buffer.trim().to_string())
|
||||
} else if args.clear {
|
||||
None
|
||||
buffer.trim().to_string()
|
||||
} else {
|
||||
Some(interactive_password_dialogue_with_double_check(&args.username)?)
|
||||
if !std::io::stdin().is_terminal() {
|
||||
anyhow::bail!(
|
||||
"Cannot prompt for password in non-interactive mode. Use --stdin or --password-file to provide the password."
|
||||
);
|
||||
}
|
||||
read_password_from_stdin_with_double_check(&args.username)?
|
||||
};
|
||||
|
||||
let expiry_date = if args.no_expire {
|
||||
None
|
||||
} else if let Some(date) = args.expire_on {
|
||||
Some(date)
|
||||
} else {
|
||||
interactive_password_expiry_dialogue(&args.username)?
|
||||
};
|
||||
|
||||
let message = Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: args.username.clone(),
|
||||
new_password: password,
|
||||
expiry: expiry_date,
|
||||
});
|
||||
let message = Request::PasswdUser((args.username.clone(), password));
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::net::UnixStream as TokioUnixStream;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
commands::{erroneous_server_response, interactive_password_dialogue_with_double_check},
|
||||
commands::{erroneous_server_response, read_password_from_stdin_with_double_check},
|
||||
mysql_admutils_compatibility::{
|
||||
common::trim_user_name_to_32_chars,
|
||||
error_messages::{
|
||||
@@ -20,7 +20,7 @@ use crate::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
completion::{mysql_user_completer, prefix_completer},
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, SetUserPasswordRequest, create_client_to_server_message_stream
|
||||
ClientToServerMessageStream, Request, Response, create_client_to_server_message_stream,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -252,12 +252,8 @@ async fn passwd_users(
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for user in users {
|
||||
let password = interactive_password_dialogue_with_double_check(&user.user)?;
|
||||
let message = Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: user.user.clone(),
|
||||
new_password: Some(password),
|
||||
expiry: None,
|
||||
});
|
||||
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::SetUserPassword(result))) => match result {
|
||||
|
||||
@@ -6,12 +6,7 @@ use crate::core::{
|
||||
types::{DbOrUser, MySQLUser},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetUserPasswordRequest {
|
||||
pub user: MySQLUser,
|
||||
pub new_password: Option<String>,
|
||||
pub expiry: Option<chrono::NaiveDate>,
|
||||
}
|
||||
pub type SetUserPasswordRequest = (MySQLUser, String);
|
||||
|
||||
pub type SetUserPasswordResponse = Result<(), SetPasswordError>;
|
||||
|
||||
@@ -23,9 +18,6 @@ pub enum SetPasswordError {
|
||||
#[error("User does not exist")]
|
||||
UserDoesNotExist,
|
||||
|
||||
#[error("Cannot clear password with an expiry date set")]
|
||||
ClearPasswordWithExpiry,
|
||||
|
||||
#[error("MySQL error: {0}")]
|
||||
MySqlError(String),
|
||||
}
|
||||
@@ -52,9 +44,6 @@ impl SetPasswordError {
|
||||
SetPasswordError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
}
|
||||
SetPasswordError::ClearPasswordWithExpiry => {
|
||||
format!("Cannot clear password for user '{username}' when an expiry date is set.")
|
||||
}
|
||||
SetPasswordError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
}
|
||||
@@ -67,7 +56,6 @@ impl SetPasswordError {
|
||||
match self {
|
||||
SetPasswordError::ValidationError(err) => err.error_type(),
|
||||
SetPasswordError::UserDoesNotExist => "user-does-not-exist".to_string(),
|
||||
SetPasswordError::ClearPasswordWithExpiry => "clear-password-with-expiry".to_string(),
|
||||
SetPasswordError::MySqlError(_) => "mysql-error".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +82,11 @@ const EXAMPLES: &str = const_format::concatcp!(
|
||||
|
||||
# Show all databases
|
||||
muscl show-db
|
||||
muscl sd
|
||||
|
||||
# Show which users have privileges on which databases
|
||||
muscl show-privs
|
||||
muscl sp
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -169,22 +171,27 @@ const EDIT_PRIVS_EXAMPLES: &str = color_print::cstr!(
|
||||
#[command(subcommand_required = true)]
|
||||
pub enum ClientCommand {
|
||||
/// Check whether you are authorized to manage the specified databases or users.
|
||||
#[command(alias = "ca")]
|
||||
CheckAuth(CheckAuthArgs),
|
||||
|
||||
/// Create one or more databases
|
||||
#[command(alias = "cd")]
|
||||
CreateDb(CreateDbArgs),
|
||||
|
||||
/// Delete one or more databases
|
||||
#[command(alias = "dd")]
|
||||
DropDb(DropDbArgs),
|
||||
|
||||
/// Print information about one or more databases
|
||||
///
|
||||
/// If no database name is provided, all databases you have access will be shown.
|
||||
#[command(alias = "sd")]
|
||||
ShowDb(ShowDbArgs),
|
||||
|
||||
/// Print user privileges for one or more databases
|
||||
///
|
||||
/// If no database names are provided, all databases you have access to will be shown.
|
||||
#[command(alias = "sp")]
|
||||
ShowPrivs(ShowPrivsArgs),
|
||||
|
||||
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
||||
@@ -239,27 +246,34 @@ pub enum ClientCommand {
|
||||
verbatim_doc_comment,
|
||||
override_usage = "muscl edit-privs [OPTIONS] [ -p <DB_NAME:USER_NAME:[+-]PRIVILEGES>... | <DB_NAME> <USER_NAME> <[+-]PRIVILEGES> ]",
|
||||
after_long_help = EDIT_PRIVS_EXAMPLES,
|
||||
alias = "ep",
|
||||
)]
|
||||
EditPrivs(EditPrivsArgs),
|
||||
|
||||
/// Create one or more users
|
||||
#[command(alias = "cu")]
|
||||
CreateUser(CreateUserArgs),
|
||||
|
||||
/// Delete one or more users
|
||||
#[command(alias = "du")]
|
||||
DropUser(DropUserArgs),
|
||||
|
||||
/// Change the MySQL password for a user
|
||||
#[command(alias = "pu")]
|
||||
PasswdUser(PasswdUserArgs),
|
||||
|
||||
/// Print information about one or more users
|
||||
///
|
||||
/// If no username is provided, all users you have access will be shown.
|
||||
#[command(alias = "su")]
|
||||
ShowUser(ShowUserArgs),
|
||||
|
||||
/// Lock account for one or more users
|
||||
#[command(alias = "lu")]
|
||||
LockUser(LockUserArgs),
|
||||
|
||||
/// Unlock account for one or more users
|
||||
#[command(alias = "uu")]
|
||||
UnlockUser(UnlockUserArgs),
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,7 @@ use crate::{
|
||||
common::UnixUser,
|
||||
protocol::{
|
||||
Request, Response, ServerToClientMessageStream, SetPasswordError,
|
||||
SetUserPasswordRequest, create_server_to_client_message_stream,
|
||||
request_validation::GroupDenylist,
|
||||
create_server_to_client_message_stream, request_validation::GroupDenylist,
|
||||
},
|
||||
},
|
||||
server::{
|
||||
@@ -176,15 +175,9 @@ async fn session_handler_with_db_connection(
|
||||
|
||||
// TODO: don't clone the request
|
||||
let request_to_display = match &request {
|
||||
Request::PasswdUser(SetUserPasswordRequest {
|
||||
user,
|
||||
new_password,
|
||||
expiry,
|
||||
}) => Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: user.clone(),
|
||||
new_password: new_password.as_ref().map(|_| "<REDACTED>".to_string()),
|
||||
expiry: *expiry,
|
||||
}),
|
||||
Request::PasswdUser((db_user, _)) => {
|
||||
Request::PasswdUser((db_user.to_owned(), "<REDACTED>".to_string()))
|
||||
}
|
||||
request => request.to_owned(),
|
||||
};
|
||||
|
||||
@@ -346,15 +339,10 @@ async fn session_handler_with_db_connection(
|
||||
.await;
|
||||
Response::DropUsers(result)
|
||||
}
|
||||
Request::PasswdUser(SetUserPasswordRequest {
|
||||
user,
|
||||
new_password,
|
||||
expiry,
|
||||
}) => {
|
||||
Request::PasswdUser((db_user, password)) => {
|
||||
let result = set_password_for_database_user(
|
||||
&user,
|
||||
new_password.as_deref(),
|
||||
expiry,
|
||||
&db_user,
|
||||
&password,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
|
||||
@@ -188,8 +188,7 @@ pub async fn drop_database_users(
|
||||
|
||||
pub async fn set_password_for_database_user(
|
||||
db_user: &MySQLUser,
|
||||
password: Option<&str>,
|
||||
expiry: Option<chrono::NaiveDate>,
|
||||
password: &str,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
@@ -198,44 +197,24 @@ pub async fn set_password_for_database_user(
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
.map_err(SetPasswordError::ValidationError)?;
|
||||
|
||||
if password.is_none() && expiry.is_some() {
|
||||
return Err(SetPasswordError::ClearPasswordWithExpiry);
|
||||
}
|
||||
|
||||
match unsafe_user_exists(db_user, &mut *connection).await {
|
||||
Ok(false) => return Err(SetPasswordError::UserDoesNotExist),
|
||||
Err(err) => return Err(SetPasswordError::MySqlError(err.to_string())),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let result = if let Some(password) = password {
|
||||
let mut query = format!(
|
||||
let result = sqlx::query(
|
||||
format!(
|
||||
"ALTER USER {}@'%' IDENTIFIED BY {}",
|
||||
quote_literal(db_user),
|
||||
quote_literal(password).as_str(),
|
||||
);
|
||||
|
||||
if let Some(expiry_date) = expiry {
|
||||
query.push_str(&format!(" PASSWORD EXPIRE DATE '{}'", expiry_date));
|
||||
}
|
||||
|
||||
sqlx::query(query.as_str())
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()))
|
||||
} else {
|
||||
let query = format!(
|
||||
"ALTER USER {}@'%' IDENTIFIED WITH mysql_native_password AS ''",
|
||||
quote_literal(db_user),
|
||||
);
|
||||
|
||||
sqlx::query(query.as_str())
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()))
|
||||
};
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()));
|
||||
|
||||
if result.is_err() {
|
||||
tracing::error!(
|
||||
|
||||
Reference in New Issue
Block a user