342 lines
9.2 KiB
Rust
342 lines
9.2 KiB
Rust
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 crate::core::{
|
|
common::{close_database_connection, get_current_unix_user, CommandStatus},
|
|
database_operations::*,
|
|
user_operations::*,
|
|
};
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserArgs {
|
|
#[clap(subcommand)]
|
|
subcmd: UserCommand,
|
|
}
|
|
|
|
#[allow(clippy::enum_variant_names)]
|
|
#[derive(Parser)]
|
|
pub enum UserCommand {
|
|
/// Create one or more users
|
|
#[command()]
|
|
CreateUser(UserCreateArgs),
|
|
|
|
/// Delete one or more users
|
|
#[command()]
|
|
DropUser(UserDeleteArgs),
|
|
|
|
/// Change the MySQL password for a user
|
|
#[command()]
|
|
PasswdUser(UserPasswdArgs),
|
|
|
|
/// Give information about one or more users
|
|
///
|
|
/// If no username is provided, all users you have access will be shown.
|
|
#[command()]
|
|
ShowUser(UserShowArgs),
|
|
|
|
/// Lock account for one or more users
|
|
#[command()]
|
|
LockUser(UserLockArgs),
|
|
|
|
/// Unlock account for one or more users
|
|
#[command()]
|
|
UnlockUser(UserUnlockArgs),
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserCreateArgs {
|
|
#[arg(num_args = 1..)]
|
|
username: Vec<String>,
|
|
|
|
/// Do not ask for a password, leave it unset
|
|
#[clap(long)]
|
|
no_password: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserDeleteArgs {
|
|
#[arg(num_args = 1..)]
|
|
username: Vec<String>,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserPasswdArgs {
|
|
username: String,
|
|
|
|
#[clap(short, long)]
|
|
password_file: Option<String>,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserShowArgs {
|
|
#[arg(num_args = 0..)]
|
|
username: Vec<String>,
|
|
|
|
#[clap(short, long)]
|
|
json: bool,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserLockArgs {
|
|
#[arg(num_args = 1..)]
|
|
username: Vec<String>,
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
pub struct UserUnlockArgs {
|
|
#[arg(num_args = 1..)]
|
|
username: Vec<String>,
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
async fn create_users(
|
|
args: UserCreateArgs,
|
|
connection: &mut MySqlConnection,
|
|
) -> anyhow::Result<CommandStatus> {
|
|
if args.username.is_empty() {
|
|
anyhow::bail!("No usernames provided");
|
|
}
|
|
|
|
let mut result = CommandStatus::SuccessfullyModified;
|
|
|
|
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);
|
|
}
|
|
|
|
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,
|
|
},
|
|
connection,
|
|
)
|
|
.await?;
|
|
}
|
|
println!();
|
|
}
|
|
Ok(result)
|
|
}
|
|
|
|
async fn drop_users(
|
|
args: UserDeleteArgs,
|
|
connection: &mut MySqlConnection,
|
|
) -> anyhow::Result<CommandStatus> {
|
|
if args.username.is_empty() {
|
|
anyhow::bail!("No usernames provided");
|
|
}
|
|
|
|
let mut result = CommandStatus::SuccessfullyModified;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Result<String> {
|
|
Password::new()
|
|
.with_prompt(format!("New MySQL password for user '{}'", username))
|
|
.with_confirmation(
|
|
format!("Retype new MySQL password for user '{}'", username),
|
|
"Passwords do not match",
|
|
)
|
|
.interact()
|
|
.map_err(Into::into)
|
|
}
|
|
|
|
async fn change_password_for_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)?;
|
|
|
|
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 {
|
|
read_password_from_stdin_with_double_check(&args.username)?
|
|
};
|
|
|
|
set_password_for_database_user(&args.username, &password, connection).await?;
|
|
|
|
Ok(CommandStatus::SuccessfullyModified)
|
|
}
|
|
|
|
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?
|
|
} 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
|
|
};
|
|
|
|
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 args.json {
|
|
let users_json = 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![]),
|
|
})
|
|
})
|
|
.collect::<serde_json::Value>();
|
|
println!(
|
|
"{}",
|
|
serde_json::to_string_pretty(&users_json)
|
|
.context("Failed to serialize users to JSON")?
|
|
);
|
|
} else if users.is_empty() {
|
|
println!("No users found.");
|
|
} else {
|
|
let mut table = Table::new();
|
|
table.add_row(row![
|
|
"User",
|
|
"Password is set",
|
|
"Locked",
|
|
"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")
|
|
]);
|
|
}
|
|
table.printstd();
|
|
}
|
|
|
|
Ok(CommandStatus::NoModificationsIntended)
|
|
}
|
|
|
|
async fn lock_users(
|
|
args: UserLockArgs,
|
|
connection: &mut MySqlConnection,
|
|
) -> anyhow::Result<CommandStatus> {
|
|
if args.username.is_empty() {
|
|
anyhow::bail!("No usernames provided");
|
|
}
|
|
|
|
let mut result = CommandStatus::SuccessfullyModified;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
async fn unlock_users(
|
|
args: UserUnlockArgs,
|
|
connection: &mut MySqlConnection,
|
|
) -> anyhow::Result<CommandStatus> {
|
|
if args.username.is_empty() {
|
|
anyhow::bail!("No usernames provided");
|
|
}
|
|
|
|
let mut result = CommandStatus::SuccessfullyModified;
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
Ok(result)
|
|
}
|