create compatibility layer for mysql-admutils commands

This commit is contained in:
2024-08-05 22:37:23 +02:00
parent c473a4823e
commit 4353689a03
14 changed files with 688 additions and 79 deletions

View File

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

View File

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

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

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

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

View File

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