create compatibility layer for mysql-admutils commands
This commit is contained in:
@@ -124,22 +124,22 @@ pub struct DatabaseShowPermArgs {
|
||||
#[derive(Parser)]
|
||||
pub struct DatabaseEditPermArgs {
|
||||
/// The name of the database to edit permissions for.
|
||||
name: Option<String>,
|
||||
pub name: Option<String>,
|
||||
|
||||
#[arg(short, long, value_name = "[DATABASE:]USER:PERMISSIONS", num_args = 0..)]
|
||||
perm: Vec<String>,
|
||||
pub perm: Vec<String>,
|
||||
|
||||
/// Whether to output the information in JSON format.
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
pub json: bool,
|
||||
|
||||
/// Specify the text editor to use for editing permissions.
|
||||
#[arg(short, long)]
|
||||
editor: Option<String>,
|
||||
pub editor: Option<String>,
|
||||
|
||||
/// Disable interactive confirmation before saving changes.
|
||||
#[arg(short, long)]
|
||||
yes: bool,
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
pub async fn handle_command(
|
||||
@@ -410,7 +410,7 @@ fn format_privileges_line(
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn edit_permissions(
|
||||
pub async fn edit_permissions(
|
||||
args: DatabaseEditPermArgs,
|
||||
conn: &mut MySqlConnection,
|
||||
) -> anyhow::Result<()> {
|
||||
|
3
src/cli/mysql_admutils_compatibility.rs
Normal file
3
src/cli/mysql_admutils_compatibility.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod common;
|
||||
pub mod mysql_dbadm;
|
||||
pub mod mysql_useradm;
|
80
src/cli/mysql_admutils_compatibility/common.rs
Normal file
80
src/cli/mysql_admutils_compatibility/common.rs
Normal file
@@ -0,0 +1,80 @@
|
||||
use crate::core::common::{
|
||||
get_current_unix_user, validate_name_token, validate_ownership_by_user_prefix,
|
||||
};
|
||||
|
||||
/// This enum is used to differentiate between database and user operations.
|
||||
/// Their output are very similar, but there are slight differences in the words used.
|
||||
pub enum DbOrUser {
|
||||
Database,
|
||||
User,
|
||||
}
|
||||
|
||||
impl DbOrUser {
|
||||
pub fn lowercased(&self) -> String {
|
||||
match self {
|
||||
DbOrUser::Database => "database".to_string(),
|
||||
DbOrUser::User => "user".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn capitalized(&self) -> String {
|
||||
match self {
|
||||
DbOrUser::Database => "Database".to_string(),
|
||||
DbOrUser::User => "User".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_by_user_prefix(name, &unix_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_token(name) {
|
||||
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)
|
||||
}
|
220
src/cli/mysql_admutils_compatibility/mysql_dbadm.rs
Normal file
220
src/cli/mysql_admutils_compatibility/mysql_dbadm.rs
Normal file
@@ -0,0 +1,220 @@
|
||||
use clap::Parser;
|
||||
use sqlx::MySqlConnection;
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
database_command,
|
||||
mysql_admutils_compatibility::common::{filter_db_or_user_names, DbOrUser},
|
||||
},
|
||||
core::{
|
||||
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
||||
database_operations::{self, yn},
|
||||
},
|
||||
};
|
||||
|
||||
const HELP_DB_PERM: &str = r#"
|
||||
Edit permissions for the DATABASE(s). Running this command will
|
||||
spawn the editor stored in the $EDITOR environment variable.
|
||||
(pico will be used if the variable is unset)
|
||||
|
||||
The file should contain one line per user, starting with the
|
||||
username and followed by ten Y/N-values seperated by whitespace.
|
||||
Lines starting with # are ignored.
|
||||
|
||||
The Y/N-values corresponds to the following mysql privileges:
|
||||
Select - Enables use of SELECT
|
||||
Insert - Enables use of INSERT
|
||||
Update - Enables use of UPDATE
|
||||
Delete - Enables use of DELETE
|
||||
Create - Enables use of CREATE TABLE
|
||||
Drop - Enables use of DROP TABLE
|
||||
Alter - Enables use of ALTER TABLE
|
||||
Index - Enables use of CREATE INDEX and DROP INDEX
|
||||
Temp - Enables use of CREATE TEMPORARY TABLE
|
||||
Lock - Enables use of LOCK TABLE
|
||||
References - Enables use of REFERENCES
|
||||
"#;
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
#[command(flatten)]
|
||||
config_overrides: GlobalConfigArgs,
|
||||
|
||||
/// Print help for the 'editperm' subcommand.
|
||||
#[arg(long, global = true)]
|
||||
pub help_editperm: bool,
|
||||
}
|
||||
|
||||
/// Create, drop or edit permissions for the DATABASE(s),
|
||||
/// as determined by the COMMAND.
|
||||
///
|
||||
/// This is a compatibility layer for the mysql-dbadm command.
|
||||
/// Please consider using the newer mysqladm command instead.
|
||||
#[derive(Parser)]
|
||||
#[command(
|
||||
version,
|
||||
about,
|
||||
disable_help_subcommand = true,
|
||||
verbatim_doc_comment,
|
||||
)]
|
||||
pub enum Command {
|
||||
/// create the DATABASE(s).
|
||||
Create(CreateArgs),
|
||||
|
||||
/// delete the DATABASE(s).
|
||||
Drop(DatabaseDropArgs),
|
||||
|
||||
/// give information about the DATABASE(s), or, if
|
||||
/// none are given, all the ones you own.
|
||||
Show(DatabaseShowArgs),
|
||||
|
||||
// TODO: make this output more verbatim_doc_comment-like,
|
||||
// without messing up the indentation.
|
||||
|
||||
/// change permissions for the DATABASE(s). Your
|
||||
/// favorite editor will be started, allowing you
|
||||
/// to make changes to the permission table.
|
||||
/// Run 'mysql-dbadm --help-editperm' for more
|
||||
/// information.
|
||||
EditPerm(EditPermArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct CreateArgs {
|
||||
/// The name of the DATABASE(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct DatabaseDropArgs {
|
||||
/// The name of the DATABASE(s) to drop.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct DatabaseShowArgs {
|
||||
/// The name of the DATABASE(s) to show.
|
||||
#[arg(num_args = 0..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct EditPermArgs {
|
||||
/// The name of the DATABASE to edit permissions for.
|
||||
pub database: String,
|
||||
}
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
if args.help_editperm {
|
||||
println!("{}", HELP_DB_PERM);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args().next().unwrap_or("mysql-dbadm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let config = get_config(args.config_overrides)?;
|
||||
let mut connection = mysql_connection_from_config(config).await?;
|
||||
|
||||
match command {
|
||||
Command::Create(args) => {
|
||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
||||
for name in filtered_names {
|
||||
database_operations::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 {
|
||||
database_operations::drop_database(&name, &mut connection).await?;
|
||||
println!("Database {} dropped.", name);
|
||||
}
|
||||
}
|
||||
Command::Show(args) => {
|
||||
let names = if args.name.is_empty() {
|
||||
database_operations::get_database_list(&mut connection).await?
|
||||
} else {
|
||||
filter_db_or_user_names(args.name, DbOrUser::Database)?
|
||||
};
|
||||
|
||||
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_permissions_args = database_command::DatabaseEditPermArgs {
|
||||
name: Some(args.database),
|
||||
perm: vec![],
|
||||
json: false,
|
||||
editor: None,
|
||||
yes: false,
|
||||
};
|
||||
|
||||
database_command::edit_permissions(edit_permissions_args, &mut connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show_db(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||
// 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 permissions = database_operations::get_database_privileges(name, conn)
|
||||
.await
|
||||
.unwrap_or(vec![]);
|
||||
|
||||
println!(
|
||||
concat!(
|
||||
"Database '{}':\n",
|
||||
"# User Select Insert Update Delete Create Drop Alter Index Temp Lock References\n",
|
||||
"# ---------------- ------ ------ ------ ------ ------ ---- ----- ----- ---- ---- ----------"
|
||||
),
|
||||
name,
|
||||
);
|
||||
if permissions.is_empty() {
|
||||
println!("# (no permissions currently granted to any users)");
|
||||
} else {
|
||||
for permission in permissions {
|
||||
println!(
|
||||
" {:<16} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
|
||||
permission.user,
|
||||
yn(permission.select_priv),
|
||||
yn(permission.insert_priv),
|
||||
yn(permission.update_priv),
|
||||
yn(permission.delete_priv),
|
||||
yn(permission.create_priv),
|
||||
yn(permission.drop_priv),
|
||||
yn(permission.alter_priv),
|
||||
yn(permission.index_priv),
|
||||
yn(permission.create_tmp_table_priv),
|
||||
yn(permission.lock_tables_priv),
|
||||
yn(permission.references_priv)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
171
src/cli/mysql_admutils_compatibility/mysql_useradm.rs
Normal file
171
src/cli/mysql_admutils_compatibility/mysql_useradm.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use clap::Parser;
|
||||
use sqlx::MySqlConnection;
|
||||
|
||||
use crate::{
|
||||
cli::{
|
||||
mysql_admutils_compatibility::common::{filter_db_or_user_names, DbOrUser},
|
||||
user_command,
|
||||
},
|
||||
core::{
|
||||
common::get_current_unix_user,
|
||||
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
||||
user_operations::{
|
||||
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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct Args {
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
#[command(flatten)]
|
||||
config_overrides: GlobalConfigArgs,
|
||||
}
|
||||
|
||||
/// Create, delete or change password for the USER(s),
|
||||
/// as determined by the COMMAND.
|
||||
///
|
||||
/// This is a compatibility layer for the mysql-useradm command.
|
||||
/// Please consider using the newer mysqladm command instead.
|
||||
#[derive(Parser)]
|
||||
#[command(version, about, disable_help_subcommand = true, verbatim_doc_comment)]
|
||||
pub enum Command {
|
||||
/// create the USER(s).
|
||||
Create(CreateArgs),
|
||||
|
||||
/// delete the USER(s).
|
||||
Delete(DeleteArgs),
|
||||
|
||||
/// change the MySQL password for the USER(s).
|
||||
Passwd(PasswdArgs),
|
||||
|
||||
/// give information about the USERS(s), or, if
|
||||
/// none are given, all the users you have.
|
||||
Show(ShowArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct CreateArgs {
|
||||
/// The name of the USER(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct DeleteArgs {
|
||||
/// The name of the USER(s) to delete.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct PasswdArgs {
|
||||
/// The name of the USER(s) to change the password for.
|
||||
#[arg(num_args = 1..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct ShowArgs {
|
||||
/// The name of the USER(s) to show.
|
||||
#[arg(num_args = 0..)]
|
||||
name: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args()
|
||||
.next()
|
||||
.unwrap_or("mysql-useradm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let config = get_config(args.config_overrides)?;
|
||||
let mut connection = mysql_connection_from_config(config).await?;
|
||||
|
||||
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, connection).await?,
|
||||
Command::Show(args) => show(args, connection).await?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn passwd(args: PasswdArgs, mut connection: 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, &mut 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, &mut connection).await?;
|
||||
println!("Password updated for user '{}'.", name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn show(args: ShowArgs, mut connection: 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, &mut connection)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|u| u.user)
|
||||
.collect()
|
||||
} else {
|
||||
filter_db_or_user_names(args.name, DbOrUser::User)?
|
||||
};
|
||||
|
||||
for user in users {
|
||||
let password_is_set = password_is_set_for_database_user(&user, &mut connection).await?;
|
||||
|
||||
match password_is_set {
|
||||
Some(true) => println!("User '{}': password set.", user),
|
||||
Some(false) => println!("User '{}': no password set.", user),
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
@@ -4,7 +4,7 @@ use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use sqlx::{Connection, MySqlConnection};
|
||||
|
||||
use crate::core::user_operations::validate_ownership_of_user_name;
|
||||
use crate::core::user_operations::validate_user_name;
|
||||
|
||||
#[derive(Parser)]
|
||||
pub struct UserArgs {
|
||||
@@ -102,6 +102,23 @@ async fn drop_users(args: UserDeleteArgs, conn: &mut MySqlConnection) -> anyhow:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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))
|
||||
.context("Failed to read password")?;
|
||||
|
||||
let pass2 = rpassword::prompt_password(format!(
|
||||
"Retype new MySQL password for user '{}': ",
|
||||
username
|
||||
))
|
||||
.context("Failed to read password")?;
|
||||
|
||||
if pass1 != pass2 {
|
||||
anyhow::bail!("Passwords do not match");
|
||||
}
|
||||
|
||||
Ok(pass1)
|
||||
}
|
||||
|
||||
async fn change_password_for_user(
|
||||
args: UserPasswdArgs,
|
||||
conn: &mut MySqlConnection,
|
||||
@@ -109,7 +126,7 @@ async fn change_password_for_user(
|
||||
// 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 = crate::core::common::get_current_unix_user()?;
|
||||
validate_ownership_of_user_name(&args.username, &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)
|
||||
@@ -117,17 +134,7 @@ async fn change_password_for_user(
|
||||
.trim()
|
||||
.to_string()
|
||||
} else {
|
||||
let pass1 = rpassword::prompt_password("Enter new password: ")
|
||||
.context("Failed to read password")?;
|
||||
|
||||
let pass2 = rpassword::prompt_password("Re-enter new password: ")
|
||||
.context("Failed to read password")?;
|
||||
|
||||
if pass1 != pass2 {
|
||||
anyhow::bail!("Passwords do not match");
|
||||
}
|
||||
|
||||
pass1
|
||||
read_password_from_stdin_with_double_check(&args.username)?
|
||||
};
|
||||
|
||||
crate::core::user_operations::set_password_for_database_user(&args.username, &password, conn)
|
||||
@@ -144,7 +151,7 @@ async fn show_users(args: UserShowArgs, conn: &mut MySqlConnection) -> anyhow::R
|
||||
} else {
|
||||
let mut result = vec![];
|
||||
for username in args.username {
|
||||
if let Err(e) = validate_ownership_of_user_name(&username, &unix_user) {
|
||||
if let Err(e) = validate_user_name(&username, &unix_user) {
|
||||
eprintln!("{}", e);
|
||||
eprintln!("Skipping...");
|
||||
continue;
|
||||
|
Reference in New Issue
Block a user