Add dynamic completion for users and databases
All checks were successful
All checks were successful
This commit is contained in:
12
Cargo.lock
generated
12
Cargo.lock
generated
@@ -242,6 +242,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_lex",
|
||||
"is_executable",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -923,6 +926,15 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_executable"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
|
||||
@@ -22,7 +22,7 @@ async-bincode = "0.8.0"
|
||||
bincode = "2.0.1"
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ] }
|
||||
clap_complete = "4.5.61"
|
||||
clap_complete = { version = "4.5.61", features = ["unstable-dynamic"] }
|
||||
derive_more = { version = "2.0.1", features = ["display", "error"] }
|
||||
dialoguer = "0.12.0"
|
||||
futures-util = "0.3.31"
|
||||
|
||||
@@ -24,14 +24,18 @@ buildFunction {
|
||||
|
||||
nativeBuildInputs = [ installShellFiles ];
|
||||
postInstall = let
|
||||
# "$out/bin/${mainProgram}" generate-completions --shell "${shell}" --command "${command}" > "$TMP/muscl.${shell}"
|
||||
commands = lib.mapCartesianProduct ({ shell, command }: ''
|
||||
"$out/bin/${mainProgram}" generate-completions --shell "${shell}" --command "${command}" > "$TMP/muscl.${shell}"
|
||||
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/muscl.${shell}"
|
||||
COMPLETE=${shell} "$out/bin/${command}" > "$TMP/${command}.${shell}"
|
||||
installShellCompletion "--${shell}" --cmd "${command}" "$TMP/${command}.${shell}"
|
||||
'') {
|
||||
shell = [ "bash" "zsh" "fish" ];
|
||||
command = [ "muscl" "mysql-dbadm" "mysql-useradm" ];
|
||||
};
|
||||
in lib.concatStringsSep "\n" commands + ''
|
||||
in ''
|
||||
ln -sr "$out/bin/muscl" "$out/bin/mysql-dbadm"
|
||||
ln -sr "$out/bin/muscl" "$out/bin/mysql-useradm"
|
||||
'' + lib.concatStringsSep "\n" commands + ''
|
||||
install -Dm444 assets/systemd/muscl.socket -t "$out/lib/systemd/system"
|
||||
install -Dm644 assets/systemd/muscl.service -t "$out/lib/systemd/system"
|
||||
substituteInPlace "$out/lib/systemd/system/muscl.service" \
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_database_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, print_drop_databases_output_status,
|
||||
print_drop_databases_output_status_json,
|
||||
@@ -16,7 +18,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DropDbArgs {
|
||||
/// The MySQL database(s) to drop
|
||||
#[arg(num_args = 1..)]
|
||||
#[arg(num_args = 1.., add = ArgValueCompleter::new(mysql_database_completer))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, print_drop_users_output_status,
|
||||
print_drop_users_output_status_json,
|
||||
@@ -16,7 +18,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DropUserArgs {
|
||||
/// The MySQL user(s) to drop
|
||||
#[arg(num_args = 1..)]
|
||||
#[arg(num_args = 1.., add = ArgValueCompleter::new(mysql_user_completer))]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::{Confirm, Editor};
|
||||
use futures_util::SinkExt;
|
||||
use nix::unistd::{User, getuid};
|
||||
@@ -10,6 +11,7 @@ use tokio_stream::StreamExt;
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_database_completer,
|
||||
database_privileges::{
|
||||
DatabasePrivilegeEditEntry, DatabasePrivilegeRow, DatabasePrivilegeRowDiff,
|
||||
DatabasePrivilegesDiff, create_or_modify_privilege_rows, diff_privileges,
|
||||
@@ -27,6 +29,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct EditPrivsArgs {
|
||||
/// The MySQL database to edit privileges for
|
||||
#[arg(add = ArgValueCompleter::new(mysql_database_completer))]
|
||||
pub name: Option<MySQLDatabase>,
|
||||
|
||||
#[arg(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, print_lock_users_output_status,
|
||||
print_lock_users_output_status_json,
|
||||
@@ -16,7 +18,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct LockUserArgs {
|
||||
/// The MySQL user(s) to lock
|
||||
#[arg(num_args = 1..)]
|
||||
#[arg(num_args = 1.., add = ArgValueCompleter::new(mysql_user_completer))]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::Password;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -9,6 +10,7 @@ use tokio_stream::StreamExt;
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListUsersError, Request, Response,
|
||||
print_set_password_output_status,
|
||||
@@ -20,6 +22,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct PasswdUserArgs {
|
||||
/// The MySQL user whose password is to be changed
|
||||
#[arg(add = ArgValueCompleter::new(mysql_user_completer))]
|
||||
username: MySQLUser,
|
||||
|
||||
/// Read the new password from a file instead of prompting for it
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -6,6 +7,7 @@ use tokio_stream::StreamExt;
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_database_completer,
|
||||
protocol::{ClientToServerMessageStream, Request, Response},
|
||||
types::MySQLDatabase,
|
||||
},
|
||||
@@ -14,7 +16,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowDbArgs {
|
||||
/// The MySQL database(s) to show
|
||||
#[arg(num_args = 0..)]
|
||||
#[arg(num_args = 0.., add = ArgValueCompleter::new(mysql_database_completer))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use tokio_stream::StreamExt;
|
||||
@@ -7,6 +8,7 @@ use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
common::yn,
|
||||
completion::mysql_database_completer,
|
||||
database_privileges::{DATABASE_PRIVILEGE_FIELDS, db_priv_field_human_readable_name},
|
||||
protocol::{ClientToServerMessageStream, Request, Response},
|
||||
types::MySQLDatabase,
|
||||
@@ -16,7 +18,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowPrivsArgs {
|
||||
/// The MySQL database(s) to show privileges for
|
||||
#[arg(num_args = 0..)]
|
||||
#[arg(num_args = 0.., add = ArgValueCompleter::new(mysql_database_completer))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{ClientToServerMessageStream, Request, Response},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -14,7 +16,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowUserArgs {
|
||||
/// The MySQL user(s) to show
|
||||
#[arg(num_args = 0..)]
|
||||
#[arg(num_args = 0.., add = ArgValueCompleter::new(mysql_user_completer))]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, print_unlock_users_output_status,
|
||||
print_unlock_users_output_status_json,
|
||||
@@ -16,7 +18,7 @@ use crate::{
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UnlockUserArgs {
|
||||
/// The MySQL user(s) to unlock
|
||||
#[arg(num_args = 1..)]
|
||||
#[arg(num_args = 1.., add = ArgValueCompleter::new(mysql_user_completer))]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Print the information as JSON
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod bootstrap;
|
||||
pub mod common;
|
||||
pub mod completion;
|
||||
pub mod database_privileges;
|
||||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
@@ -159,7 +159,7 @@ fn connect_to_external_server(
|
||||
/// Drop privileges to the real user and group of the process.
|
||||
/// If the process is not running with elevated privileges, this function
|
||||
/// is a no-op.
|
||||
fn drop_privs() -> anyhow::Result<()> {
|
||||
pub fn drop_privs() -> anyhow::Result<()> {
|
||||
tracing::debug!("Dropping privileges");
|
||||
let real_uid = nix::unistd::getuid();
|
||||
let real_gid = nix::unistd::getgid();
|
||||
|
||||
5
src/core/completion.rs
Normal file
5
src/core/completion.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
mod mysql_database_completer;
|
||||
mod mysql_user_completer;
|
||||
|
||||
pub use mysql_database_completer::*;
|
||||
pub use mysql_user_completer::*;
|
||||
73
src/core/completion/mysql_database_completer.rs
Normal file
73
src/core/completion/mysql_database_completer.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use clap_complete::CompletionCandidate;
|
||||
use clap_verbosity_flag::Verbosity;
|
||||
use futures_util::SinkExt;
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
protocol::{Request, Response, create_client_to_server_message_stream},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn mysql_database_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(runtime) => match runtime.block_on(mysql_database_completer_(current)) {
|
||||
Ok(completions) => completions,
|
||||
Err(err) => {
|
||||
eprintln!("Error getting MySQL database completions: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Error starting Tokio runtime: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the server to get MySQL database completions.
|
||||
async fn mysql_database_completer_(
|
||||
current: &std::ffi::OsStr,
|
||||
) -> anyhow::Result<Vec<CompletionCandidate>> {
|
||||
let server_connection =
|
||||
bootstrap_server_connection_and_drop_privileges(None, None, Verbosity::new(0, 1))?;
|
||||
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let mut server_connection = create_client_to_server_message_stream(tokio_socket);
|
||||
|
||||
while let Some(Ok(message)) = server_connection.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let message = Request::CompleteDatabaseName(current.to_string_lossy().to_string());
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(anyhow::Error::from(err).context("Failed to communicate with server"));
|
||||
}
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CompleteDatabaseName(suggestions))) => suggestions,
|
||||
response => return erroneous_server_response(response).map(|_| vec![]),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
let result = result.into_iter().map(CompletionCandidate::new).collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
73
src/core/completion/mysql_user_completer.rs
Normal file
73
src/core/completion/mysql_user_completer.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use clap_complete::CompletionCandidate;
|
||||
use clap_verbosity_flag::Verbosity;
|
||||
use futures_util::SinkExt;
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
protocol::{Request, Response, create_client_to_server_message_stream},
|
||||
},
|
||||
};
|
||||
|
||||
pub fn mysql_user_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(runtime) => match runtime.block_on(mysql_user_completer_(current)) {
|
||||
Ok(completions) => completions,
|
||||
Err(err) => {
|
||||
eprintln!("Error getting MySQL user completions: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Error starting Tokio runtime: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the server to get MySQL user completions.
|
||||
async fn mysql_user_completer_(
|
||||
current: &std::ffi::OsStr,
|
||||
) -> anyhow::Result<Vec<CompletionCandidate>> {
|
||||
let server_connection =
|
||||
bootstrap_server_connection_and_drop_privileges(None, None, Verbosity::new(0, 1))?;
|
||||
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let mut server_connection = create_client_to_server_message_stream(tokio_socket);
|
||||
|
||||
while let Some(Ok(message)) = server_connection.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let message = Request::CompleteUserName(current.to_string_lossy().to_string());
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(anyhow::Error::from(err).context("Failed to communicate with server"));
|
||||
}
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CompleteUserName(suggestions))) => suggestions,
|
||||
response => return erroneous_server_response(response).map(|_| vec![]),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
let result = result.into_iter().map(CompletionCandidate::new).collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
mod check_authorization;
|
||||
mod complete_database_name;
|
||||
mod complete_user_name;
|
||||
mod create_databases;
|
||||
mod create_users;
|
||||
mod drop_databases;
|
||||
@@ -15,6 +17,8 @@ mod passwd_user;
|
||||
mod unlock_users;
|
||||
|
||||
pub use check_authorization::*;
|
||||
pub use complete_database_name::*;
|
||||
pub use complete_user_name::*;
|
||||
pub use create_databases::*;
|
||||
pub use create_users::*;
|
||||
pub use drop_databases::*;
|
||||
@@ -64,6 +68,9 @@ pub fn create_client_to_server_message_stream(socket: UnixStream) -> ClientToSer
|
||||
pub enum Request {
|
||||
CheckAuthorization(CheckAuthorizationRequest),
|
||||
|
||||
CompleteDatabaseName(CompleteDatabaseNameRequest),
|
||||
CompleteUserName(CompleteUserNameRequest),
|
||||
|
||||
CreateDatabases(CreateDatabasesRequest),
|
||||
DropDatabases(DropDatabasesRequest),
|
||||
ListDatabases(ListDatabasesRequest),
|
||||
@@ -88,6 +95,9 @@ pub enum Request {
|
||||
pub enum Response {
|
||||
CheckAuthorization(CheckAuthorizationResponse),
|
||||
|
||||
CompleteDatabaseName(CompleteDatabaseNameResponse),
|
||||
CompleteUserName(CompleteUserNameResponse),
|
||||
|
||||
// Specific data for specific commands
|
||||
CreateDatabases(CreateDatabasesResponse),
|
||||
DropDatabases(DropDatabasesResponse),
|
||||
|
||||
5
src/core/protocol/commands/complete_database_name.rs
Normal file
5
src/core/protocol/commands/complete_database_name.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::core::types::MySQLDatabase;
|
||||
|
||||
pub type CompleteDatabaseNameRequest = String;
|
||||
|
||||
pub type CompleteDatabaseNameResponse = Vec<MySQLDatabase>;
|
||||
5
src/core/protocol/commands/complete_user_name.rs
Normal file
5
src/core/protocol/commands/complete_user_name.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
use crate::core::types::MySQLUser;
|
||||
|
||||
pub type CompleteUserNameRequest = String;
|
||||
|
||||
pub type CompleteUserNameResponse = Vec<MySQLUser>;
|
||||
0
src/core/protocol/commands/user_exists.rs
Normal file
0
src/core/protocol/commands/user_exists.rs
Normal file
@@ -1,4 +1,5 @@
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
str::FromStr,
|
||||
@@ -49,6 +50,12 @@ impl From<String> for MySQLUser {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MySQLUser> for OsString {
|
||||
fn from(val: MySQLUser) -> Self {
|
||||
val.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
||||
pub struct MySQLDatabase(String);
|
||||
|
||||
@@ -92,6 +99,12 @@ impl From<String> for MySQLDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MySQLDatabase> for OsString {
|
||||
fn from(val: MySQLDatabase) -> Self {
|
||||
val.0.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
pub enum DbOrUser {
|
||||
Database(MySQLDatabase),
|
||||
|
||||
41
src/main.rs
41
src/main.rs
@@ -3,7 +3,7 @@ extern crate prettytable;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{CommandFactory, Parser, ValueEnum};
|
||||
use clap_complete::{Shell, generate};
|
||||
use clap_complete::{CompleteEnv, Shell, generate};
|
||||
use clap_verbosity_flag::Verbosity;
|
||||
|
||||
use std::path::PathBuf;
|
||||
@@ -103,6 +103,10 @@ enum ToplevelCommands {
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn main() -> anyhow::Result<()> {
|
||||
if handle_dynamic_completion()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
||||
if handle_mysql_admutils_command()?.is_some() {
|
||||
return Ok(());
|
||||
@@ -129,6 +133,41 @@ fn main() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_dynamic_completion() -> anyhow::Result<Option<()>> {
|
||||
if std::env::var_os("COMPLETE").is_some() {
|
||||
#[cfg(feature = "suid-sgid-mode")]
|
||||
if executable_is_suid_or_sgid()? {
|
||||
use crate::core::bootstrap::drop_privs;
|
||||
drop_privs()?
|
||||
}
|
||||
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.and_then(|s| {
|
||||
PathBuf::from(s)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
})
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"Could not determine executable name for completion"
|
||||
))?;
|
||||
|
||||
let command = match argv0.as_str() {
|
||||
"muscl" => Args::command(),
|
||||
"mysql-dbadm" => mysql_dbadm::Command::command(),
|
||||
"mysql-useradm" => mysql_useradm::Command::command(),
|
||||
command => anyhow::bail!("Unknown executable name: `{}`", command),
|
||||
};
|
||||
|
||||
CompleteEnv::with_factory(move || command.clone()).complete();
|
||||
|
||||
Ok(Some(()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_mysql_admutils_command() -> anyhow::Result<Option<()>> {
|
||||
let argv0 = std::env::args().next().and_then(|s| {
|
||||
|
||||
@@ -18,15 +18,16 @@ use crate::{
|
||||
authorization::check_authorization,
|
||||
sql::{
|
||||
database_operations::{
|
||||
create_databases, drop_databases, list_all_databases_for_user, list_databases,
|
||||
complete_database_name, create_databases, drop_databases,
|
||||
list_all_databases_for_user, list_databases,
|
||||
},
|
||||
database_privilege_operations::{
|
||||
apply_privilege_diffs, get_all_database_privileges, get_databases_privilege_data,
|
||||
},
|
||||
user_operations::{
|
||||
create_database_users, drop_database_users, list_all_database_users_for_unix_user,
|
||||
list_database_users, lock_database_users, set_password_for_database_user,
|
||||
unlock_database_users,
|
||||
complete_user_name, create_database_users, drop_database_users,
|
||||
list_all_database_users_for_unix_user, list_database_users, lock_database_users,
|
||||
set_password_for_database_user, unlock_database_users,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -171,6 +172,33 @@ async fn session_handler_with_db_connection(
|
||||
let result = check_authorization(dbs_or_users, unix_user).await;
|
||||
Response::CheckAuthorization(result)
|
||||
}
|
||||
Request::CompleteDatabaseName(partial_database_name) => {
|
||||
// TODO: more correct validation here
|
||||
if !partial_database_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
Response::CompleteDatabaseName(vec![])
|
||||
} else {
|
||||
let result =
|
||||
complete_database_name(partial_database_name, unix_user, db_connection)
|
||||
.await;
|
||||
Response::CompleteDatabaseName(result)
|
||||
}
|
||||
}
|
||||
Request::CompleteUserName(partial_user_name) => {
|
||||
// TODO: more correct validation here
|
||||
if !partial_user_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
Response::CompleteUserName(vec![])
|
||||
} else {
|
||||
let result =
|
||||
complete_user_name(partial_user_name, unix_user, db_connection).await;
|
||||
Response::CompleteUserName(result)
|
||||
}
|
||||
}
|
||||
Request::CreateDatabases(databases_names) => {
|
||||
let result = create_databases(databases_names, unix_user, db_connection).await;
|
||||
Response::CreateDatabases(result)
|
||||
|
||||
@@ -5,6 +5,7 @@ use sqlx::prelude::*;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::protocol::CompleteDatabaseNameResponse;
|
||||
use crate::core::types::MySQLDatabase;
|
||||
use crate::{
|
||||
core::{
|
||||
@@ -43,6 +44,45 @@ pub(super) async fn unsafe_database_exists(
|
||||
Ok(result?.is_some())
|
||||
}
|
||||
|
||||
pub async fn complete_database_name(
|
||||
database_prefix: String,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> CompleteDatabaseNameResponse {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT `SCHEMA_NAME` AS `database`
|
||||
FROM `information_schema`.`SCHEMATA`
|
||||
WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
||||
AND `SCHEMA_NAME` REGEXP ?
|
||||
AND `SCHEMA_NAME` LIKE ?
|
||||
"#,
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.bind(format!("{}%", database_prefix))
|
||||
.fetch_all(connection)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(rows) => rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let database: String = row.try_get("database").ok()?;
|
||||
Some(database.into())
|
||||
})
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Failed to complete database name for prefix '{}' and user '{}': {:?}",
|
||||
database_prefix,
|
||||
unix_user.username,
|
||||
err
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_databases(
|
||||
database_names: Vec<MySQLDatabase>,
|
||||
unix_user: &UnixUser,
|
||||
|
||||
@@ -51,6 +51,44 @@ async fn unsafe_user_exists(
|
||||
result
|
||||
}
|
||||
|
||||
pub async fn complete_user_name(
|
||||
user_prefix: String,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> Vec<MySQLUser> {
|
||||
let result = sqlx::query(
|
||||
r#"
|
||||
SELECT `User` AS `user`
|
||||
FROM `mysql`.`user`
|
||||
WHERE `User` REGEXP ?
|
||||
AND `User` LIKE ?
|
||||
"#,
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.bind(format!("{}%", user_prefix))
|
||||
.fetch_all(connection)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(rows) => rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let user: String = try_get_with_binary_fallback(&row, "user").ok()?;
|
||||
Some(user.into())
|
||||
})
|
||||
.collect(),
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"Failed to complete user name for prefix '{}' and user '{}': {:?}",
|
||||
user_prefix,
|
||||
unix_user.username,
|
||||
err
|
||||
);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_database_users(
|
||||
db_users: Vec<MySQLUser>,
|
||||
unix_user: &UnixUser,
|
||||
|
||||
Reference in New Issue
Block a user