Split client and server into separate binaries
This commit is contained in:
16
Cargo.toml
16
Cargo.toml
@@ -67,10 +67,19 @@ default = ["mysql-admutils-compatibility"]
|
|||||||
mysql-admutils-compatibility = []
|
mysql-admutils-compatibility = []
|
||||||
suid-sgid-mode = []
|
suid-sgid-mode = []
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "muscl_lib"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "muscl"
|
name = "muscl"
|
||||||
bench = false
|
bench = false
|
||||||
path = "src/main.rs"
|
path = "src/entrypoints/muscl.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "muscl-server"
|
||||||
|
bench = false
|
||||||
|
path = "src/entrypoints/muscl_server.rs"
|
||||||
|
|
||||||
[profile.release-lto]
|
[profile.release-lto]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
@@ -120,6 +129,11 @@ assets = [
|
|||||||
"usr/bin/",
|
"usr/bin/",
|
||||||
"755",
|
"755",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
"target/release/muscl-server",
|
||||||
|
"usr/bin/",
|
||||||
|
"755",
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"target/release/mysql-useradm",
|
"target/release/mysql-useradm",
|
||||||
"usr/bin/",
|
"usr/bin/",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ After=mysql.service mariadb.service
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=notify
|
||||||
ExecStart=/usr/bin/muscl server --systemd --disable-landlock socket-activate
|
ExecStart=/usr/bin/muscl-server --systemd --disable-landlock socket-activate
|
||||||
ExecReload=/usr/bin/kill -HUP $MAINPID
|
ExecReload=/usr/bin/kill -HUP $MAINPID
|
||||||
|
|
||||||
WatchdogSec=15
|
WatchdogSec=15
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ buildFunction ({
|
|||||||
install -Dm444 assets/systemd/muscl.socket -t "$out/lib/systemd/system"
|
install -Dm444 assets/systemd/muscl.socket -t "$out/lib/systemd/system"
|
||||||
install -Dm644 assets/systemd/muscl.service -t "$out/lib/systemd/system"
|
install -Dm644 assets/systemd/muscl.service -t "$out/lib/systemd/system"
|
||||||
substituteInPlace "$out/lib/systemd/system/muscl.service" \
|
substituteInPlace "$out/lib/systemd/system/muscl.service" \
|
||||||
--replace-fail '/usr/bin/muscl' "$out/bin/muscl"
|
--replace-fail '/usr/bin/muscl-server' "$out/bin/muscl-server"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
meta = with lib; {
|
meta = with lib; {
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ in
|
|||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = [
|
ExecStart = [
|
||||||
""
|
""
|
||||||
"${lib.getExe cfg.package} ${cfg.logLevel} server --systemd --disable-landlock socket-activate"
|
"${lib.getExe' cfg.package "muscl-server"} ${cfg.logLevel} --systemd --disable-landlock socket-activate"
|
||||||
];
|
];
|
||||||
|
|
||||||
ExecReload = [
|
ExecReload = [
|
||||||
|
|||||||
@@ -24,153 +24,12 @@ pub use show_privs::*;
|
|||||||
pub use show_user::*;
|
pub use show_user::*;
|
||||||
pub use unlock_user::*;
|
pub use unlock_user::*;
|
||||||
|
|
||||||
use clap::Subcommand;
|
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
use crate::core::protocol::{ClientToServerMessageStream, Request, Response};
|
use crate::core::protocol::{ClientToServerMessageStream, Request, Response};
|
||||||
|
|
||||||
const EDIT_PRIVS_EXAMPLES: &str = color_print::cstr!(
|
|
||||||
r#"
|
|
||||||
<bold><underline>Examples:</underline></bold>
|
|
||||||
# Open interactive editor to edit privileges
|
|
||||||
muscl edit-privs
|
|
||||||
|
|
||||||
# Set privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`
|
|
||||||
muscl edit-privs my_db my_user siu
|
|
||||||
|
|
||||||
# Set all privileges for user `my_other_user` on database `my_other_db`
|
|
||||||
muscl edit-privs my_other_db my_other_user A
|
|
||||||
|
|
||||||
# Add the `DELETE` privilege for user `my_user` on database `my_db`
|
|
||||||
muscl edit-privs my_db my_user +d
|
|
||||||
|
|
||||||
# Set miscellaneous privileges for multiple users on database `my_db`
|
|
||||||
muscl edit-privs -p my_db:my_user:siu -p my_db:my_other_user:+ct -p my_db:yet_another_user:-d
|
|
||||||
"#
|
|
||||||
);
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone)]
|
|
||||||
#[command(subcommand_required = true)]
|
|
||||||
pub enum ClientCommand {
|
|
||||||
/// Check whether you are authorized to manage the specified databases or users.
|
|
||||||
CheckAuth(CheckAuthArgs),
|
|
||||||
|
|
||||||
/// Create one or more databases
|
|
||||||
CreateDb(CreateDbArgs),
|
|
||||||
|
|
||||||
/// Delete one or more databases
|
|
||||||
DropDb(DropDbArgs),
|
|
||||||
|
|
||||||
/// Print information about one or more databases
|
|
||||||
///
|
|
||||||
/// If no database name is provided, all databases you have access will be shown.
|
|
||||||
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.
|
|
||||||
ShowPrivs(ShowPrivsArgs),
|
|
||||||
|
|
||||||
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
|
||||||
///
|
|
||||||
/// This command has three modes of operation:
|
|
||||||
///
|
|
||||||
/// 1. Interactive mode:
|
|
||||||
///
|
|
||||||
/// If no arguments are provided, the user will be prompted to edit the privileges using a text editor.
|
|
||||||
///
|
|
||||||
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
|
||||||
///
|
|
||||||
/// Follow the instructions inside the editor for more information.
|
|
||||||
///
|
|
||||||
/// 2. Non-interactive human-friendly mode:
|
|
||||||
///
|
|
||||||
/// You can provide the command with three positional arguments:
|
|
||||||
///
|
|
||||||
/// - `<DB_NAME>`: The name of the database for which you want to edit privileges.
|
|
||||||
/// - `<USER_NAME>`: The name of the user whose privileges you want to edit.
|
|
||||||
/// - `<[+-]PRIVILEGES>`: A string representing the privileges to set for the user.
|
|
||||||
///
|
|
||||||
/// The `<[+-]PRIVILEGES>` argument is a string of characters, each representing a single privilege.
|
|
||||||
/// The character `A` is an exception - it represents all privileges.
|
|
||||||
/// The optional leading character can be either `+` to grant additional privileges or `-` to revoke privileges.
|
|
||||||
/// If omitted, the privileges will be set exactly as specified, removing any privileges not listed, and adding any that are.
|
|
||||||
///
|
|
||||||
/// The character-to-privilege mapping is defined as follows:
|
|
||||||
///
|
|
||||||
/// - `s` - SELECT
|
|
||||||
/// - `i` - INSERT
|
|
||||||
/// - `u` - UPDATE
|
|
||||||
/// - `d` - DELETE
|
|
||||||
/// - `c` - CREATE
|
|
||||||
/// - `D` - DROP
|
|
||||||
/// - `a` - ALTER
|
|
||||||
/// - `I` - INDEX
|
|
||||||
/// - `t` - CREATE TEMPORARY TABLES
|
|
||||||
/// - `l` - LOCK TABLES
|
|
||||||
/// - `r` - REFERENCES
|
|
||||||
/// - `A` - ALL PRIVILEGES
|
|
||||||
///
|
|
||||||
/// 3. Non-interactive batch mode:
|
|
||||||
///
|
|
||||||
/// By using the `-p` flag, you can provide multiple privilege edits in a single command.
|
|
||||||
///
|
|
||||||
/// The flag value should be formatted as `DB_NAME:USER_NAME:[+-]PRIVILEGES`
|
|
||||||
/// where the privileges are a string of characters, each representing a single privilege.
|
|
||||||
/// (See the character-to-privilege mapping above.)
|
|
||||||
///
|
|
||||||
#[command(
|
|
||||||
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,
|
|
||||||
)]
|
|
||||||
EditPrivs(EditPrivsArgs),
|
|
||||||
|
|
||||||
/// Create one or more users
|
|
||||||
CreateUser(CreateUserArgs),
|
|
||||||
|
|
||||||
/// Delete one or more users
|
|
||||||
DropUser(DropUserArgs),
|
|
||||||
|
|
||||||
/// Change the MySQL password for a user
|
|
||||||
PasswdUser(PasswdUserArgs),
|
|
||||||
|
|
||||||
/// Print information about one or more users
|
|
||||||
///
|
|
||||||
/// If no username is provided, all users you have access will be shown.
|
|
||||||
ShowUser(ShowUserArgs),
|
|
||||||
|
|
||||||
/// Lock account for one or more users
|
|
||||||
LockUser(LockUserArgs),
|
|
||||||
|
|
||||||
/// Unlock account for one or more users
|
|
||||||
UnlockUser(UnlockUserArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_command(
|
|
||||||
command: ClientCommand,
|
|
||||||
server_connection: ClientToServerMessageStream,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
match command {
|
|
||||||
ClientCommand::CheckAuth(args) => check_authorization(args, server_connection).await,
|
|
||||||
ClientCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
|
||||||
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
|
||||||
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
|
||||||
ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await,
|
|
||||||
ClientCommand::EditPrivs(args) => {
|
|
||||||
edit_database_privileges(args, None, server_connection).await
|
|
||||||
}
|
|
||||||
ClientCommand::CreateUser(args) => create_users(args, server_connection).await,
|
|
||||||
ClientCommand::DropUser(args) => drop_users(args, server_connection).await,
|
|
||||||
ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
|
||||||
ClientCommand::ShowUser(args) => show_users(args, server_connection).await,
|
|
||||||
ClientCommand::LockUser(args) => lock_users(args, server_connection).await,
|
|
||||||
ClientCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle an unexpected or erroneous response from the server.
|
/// Handle an unexpected or erroneous response from the server.
|
||||||
///
|
///
|
||||||
/// This function checks the provided response and returns an appropriate error message.
|
/// This function checks the provided response and returns an appropriate error message.
|
||||||
@@ -199,7 +58,7 @@ pub fn erroneous_server_response(
|
|||||||
///
|
///
|
||||||
/// This function should be used when an authorization error occurs,
|
/// This function should be used when an authorization error occurs,
|
||||||
/// to help the user understand which databases or users they are allowed to manage.
|
/// to help the user understand which databases or users they are allowed to manage.
|
||||||
pub async fn print_authorization_owner_hint(
|
async fn print_authorization_owner_hint(
|
||||||
server_connection: &mut ClientToServerMessageStream,
|
server_connection: &mut ClientToServerMessageStream,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
server_connection
|
server_connection
|
||||||
|
|||||||
382
src/entrypoints/muscl.rs
Normal file
382
src/entrypoints/muscl.rs
Normal file
@@ -0,0 +1,382 @@
|
|||||||
|
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use clap::{CommandFactory, Parser, Subcommand, crate_version};
|
||||||
|
use clap_complete::CompleteEnv;
|
||||||
|
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||||
|
use tokio::net::UnixStream as TokioUnixStream;
|
||||||
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
|
use muscl_lib::{
|
||||||
|
client::{
|
||||||
|
commands::{
|
||||||
|
CheckAuthArgs, CreateDbArgs, CreateUserArgs, DropDbArgs, DropUserArgs, EditPrivsArgs,
|
||||||
|
LockUserArgs, PasswdUserArgs, ShowDbArgs, ShowPrivsArgs, ShowUserArgs, UnlockUserArgs,
|
||||||
|
check_authorization, create_databases, create_users, drop_databases, drop_users,
|
||||||
|
edit_database_privileges, lock_users, passwd_user, show_database_privileges,
|
||||||
|
show_databases, show_users, unlock_users,
|
||||||
|
},
|
||||||
|
mysql_admutils_compatibility::{mysql_dbadm, mysql_useradm},
|
||||||
|
},
|
||||||
|
core::{
|
||||||
|
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||||
|
common::{ASCII_BANNER, KIND_REGARDS},
|
||||||
|
protocol::{ClientToServerMessageStream, Response, create_client_to_server_message_stream},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const fn long_version() -> &'static str {
|
||||||
|
macro_rules! feature {
|
||||||
|
($title:expr, $flag:expr) => {
|
||||||
|
if cfg!(feature = $flag) {
|
||||||
|
concat!($title, ": enabled")
|
||||||
|
} else {
|
||||||
|
concat!($title, ": disabled")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const_format::concatcp!(
|
||||||
|
crate_version!(),
|
||||||
|
"\n",
|
||||||
|
"build profile: ",
|
||||||
|
env!("BUILD_PROFILE"),
|
||||||
|
"\n",
|
||||||
|
"commit: ",
|
||||||
|
env!("GIT_COMMIT"),
|
||||||
|
"\n\n",
|
||||||
|
"[features]\n",
|
||||||
|
feature!("SUID/SGID mode", "suid-sgid-mode"),
|
||||||
|
"\n",
|
||||||
|
feature!(
|
||||||
|
"mysql-admutils compatibility",
|
||||||
|
"mysql-admutils-compatibility"
|
||||||
|
),
|
||||||
|
"\n",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LONG_VERSION: &str = long_version();
|
||||||
|
|
||||||
|
const EXAMPLES: &str = const_format::concatcp!(
|
||||||
|
color_print::cstr!("<bold><underline>Examples:</underline></bold>"),
|
||||||
|
r#"
|
||||||
|
# Display help information for any specific command
|
||||||
|
muscl <command> --help
|
||||||
|
|
||||||
|
# Create two users 'alice_user1' and 'alice_user2'
|
||||||
|
muscl create-user alice_user1 alice_user2
|
||||||
|
|
||||||
|
# Create two databases 'alice_db1' and 'alice_db2'
|
||||||
|
muscl create-db alice_db1 alice_db2
|
||||||
|
|
||||||
|
# Grant Select, Update, Insert and Delete privileges on 'alice_db1' to 'alice_user1'
|
||||||
|
muscl edit-privs alice_db1 alice_user1 +suid
|
||||||
|
|
||||||
|
# Show all databases
|
||||||
|
muscl show-db
|
||||||
|
|
||||||
|
# Show which users have privileges on which databases
|
||||||
|
muscl show-privs
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
const BEFORE_LONG_HELP: &str = const_format::concatcp!("\x1b[1m", ASCII_BANNER, "\x1b[0m");
|
||||||
|
const AFTER_LONG_HELP: &str = const_format::concatcp!(EXAMPLES, "\n", KIND_REGARDS,);
|
||||||
|
|
||||||
|
/// Database administration tool for non-admin users to manage their own MySQL databases and users.
|
||||||
|
///
|
||||||
|
/// This tool allows you to manage users and databases in MySQL.
|
||||||
|
///
|
||||||
|
/// You are only allowed to manage databases and users that are prefixed with
|
||||||
|
/// either your username, or a group that you are a member of.
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(
|
||||||
|
bin_name = "muscl",
|
||||||
|
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
||||||
|
version,
|
||||||
|
about,
|
||||||
|
disable_help_subcommand = true,
|
||||||
|
propagate_version = true,
|
||||||
|
before_long_help = BEFORE_LONG_HELP,
|
||||||
|
after_long_help = AFTER_LONG_HELP,
|
||||||
|
long_version = LONG_VERSION,
|
||||||
|
// NOTE: All non-registered "subcommands" are processed before Arg::parse() is called.
|
||||||
|
subcommand_required = true,
|
||||||
|
)]
|
||||||
|
struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: ClientCommand,
|
||||||
|
|
||||||
|
// NOTE: be careful not to add short options that collide with the `edit-privs` privilege
|
||||||
|
// characters. It should in theory be possible for `edit-privs` to ignore any options
|
||||||
|
// specified here, but in practice clap is being difficult to work with.
|
||||||
|
/// Path to the socket of the server.
|
||||||
|
#[arg(
|
||||||
|
long = "server-socket",
|
||||||
|
value_name = "PATH",
|
||||||
|
value_hint = clap::ValueHint::FilePath,
|
||||||
|
global = true,
|
||||||
|
hide_short_help = true
|
||||||
|
)]
|
||||||
|
server_socket_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
// TODO: conditionally include this only when compiling with SUID/SGID support
|
||||||
|
/// Config file to use for the server.
|
||||||
|
///
|
||||||
|
/// This is only useful when running in SUID/SGID mode.
|
||||||
|
#[arg(
|
||||||
|
long = "config",
|
||||||
|
value_name = "PATH",
|
||||||
|
value_hint = clap::ValueHint::FilePath,
|
||||||
|
global = true,
|
||||||
|
hide_short_help = true
|
||||||
|
)]
|
||||||
|
config_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
verbose: Verbosity<InfoLevel>,
|
||||||
|
}
|
||||||
|
|
||||||
|
const EDIT_PRIVS_EXAMPLES: &str = color_print::cstr!(
|
||||||
|
r#"
|
||||||
|
<bold><underline>Examples:</underline></bold>
|
||||||
|
# Open interactive editor to edit privileges
|
||||||
|
muscl edit-privs
|
||||||
|
|
||||||
|
# Set privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`
|
||||||
|
muscl edit-privs my_db my_user siu
|
||||||
|
|
||||||
|
# Set all privileges for user `my_other_user` on database `my_other_db`
|
||||||
|
muscl edit-privs my_other_db my_other_user A
|
||||||
|
|
||||||
|
# Add the `DELETE` privilege for user `my_user` on database `my_db`
|
||||||
|
muscl edit-privs my_db my_user +d
|
||||||
|
|
||||||
|
# Set miscellaneous privileges for multiple users on database `my_db`
|
||||||
|
muscl edit-privs -p my_db:my_user:siu -p my_db:my_other_user:+ct -p my_db:yet_another_user:-d
|
||||||
|
"#
|
||||||
|
);
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
|
#[command(subcommand_required = true)]
|
||||||
|
pub enum ClientCommand {
|
||||||
|
/// Check whether you are authorized to manage the specified databases or users.
|
||||||
|
CheckAuth(CheckAuthArgs),
|
||||||
|
|
||||||
|
/// Create one or more databases
|
||||||
|
CreateDb(CreateDbArgs),
|
||||||
|
|
||||||
|
/// Delete one or more databases
|
||||||
|
DropDb(DropDbArgs),
|
||||||
|
|
||||||
|
/// Print information about one or more databases
|
||||||
|
///
|
||||||
|
/// If no database name is provided, all databases you have access will be shown.
|
||||||
|
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.
|
||||||
|
ShowPrivs(ShowPrivsArgs),
|
||||||
|
|
||||||
|
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
||||||
|
///
|
||||||
|
/// This command has three modes of operation:
|
||||||
|
///
|
||||||
|
/// 1. Interactive mode:
|
||||||
|
///
|
||||||
|
/// If no arguments are provided, the user will be prompted to edit the privileges using a text editor.
|
||||||
|
///
|
||||||
|
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
||||||
|
///
|
||||||
|
/// Follow the instructions inside the editor for more information.
|
||||||
|
///
|
||||||
|
/// 2. Non-interactive human-friendly mode:
|
||||||
|
///
|
||||||
|
/// You can provide the command with three positional arguments:
|
||||||
|
///
|
||||||
|
/// - `<DB_NAME>`: The name of the database for which you want to edit privileges.
|
||||||
|
/// - `<USER_NAME>`: The name of the user whose privileges you want to edit.
|
||||||
|
/// - `<[+-]PRIVILEGES>`: A string representing the privileges to set for the user.
|
||||||
|
///
|
||||||
|
/// The `<[+-]PRIVILEGES>` argument is a string of characters, each representing a single privilege.
|
||||||
|
/// The character `A` is an exception - it represents all privileges.
|
||||||
|
/// The optional leading character can be either `+` to grant additional privileges or `-` to revoke privileges.
|
||||||
|
/// If omitted, the privileges will be set exactly as specified, removing any privileges not listed, and adding any that are.
|
||||||
|
///
|
||||||
|
/// The character-to-privilege mapping is defined as follows:
|
||||||
|
///
|
||||||
|
/// - `s` - SELECT
|
||||||
|
/// - `i` - INSERT
|
||||||
|
/// - `u` - UPDATE
|
||||||
|
/// - `d` - DELETE
|
||||||
|
/// - `c` - CREATE
|
||||||
|
/// - `D` - DROP
|
||||||
|
/// - `a` - ALTER
|
||||||
|
/// - `I` - INDEX
|
||||||
|
/// - `t` - CREATE TEMPORARY TABLES
|
||||||
|
/// - `l` - LOCK TABLES
|
||||||
|
/// - `r` - REFERENCES
|
||||||
|
/// - `A` - ALL PRIVILEGES
|
||||||
|
///
|
||||||
|
/// 3. Non-interactive batch mode:
|
||||||
|
///
|
||||||
|
/// By using the `-p` flag, you can provide multiple privilege edits in a single command.
|
||||||
|
///
|
||||||
|
/// The flag value should be formatted as `DB_NAME:USER_NAME:[+-]PRIVILEGES`
|
||||||
|
/// where the privileges are a string of characters, each representing a single privilege.
|
||||||
|
/// (See the character-to-privilege mapping above.)
|
||||||
|
///
|
||||||
|
#[command(
|
||||||
|
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,
|
||||||
|
)]
|
||||||
|
EditPrivs(EditPrivsArgs),
|
||||||
|
|
||||||
|
/// Create one or more users
|
||||||
|
CreateUser(CreateUserArgs),
|
||||||
|
|
||||||
|
/// Delete one or more users
|
||||||
|
DropUser(DropUserArgs),
|
||||||
|
|
||||||
|
/// Change the MySQL password for a user
|
||||||
|
PasswdUser(PasswdUserArgs),
|
||||||
|
|
||||||
|
/// Print information about one or more users
|
||||||
|
///
|
||||||
|
/// If no username is provided, all users you have access will be shown.
|
||||||
|
ShowUser(ShowUserArgs),
|
||||||
|
|
||||||
|
/// Lock account for one or more users
|
||||||
|
LockUser(LockUserArgs),
|
||||||
|
|
||||||
|
/// Unlock account for one or more users
|
||||||
|
UnlockUser(UnlockUserArgs),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle_command(
|
||||||
|
command: ClientCommand,
|
||||||
|
server_connection: ClientToServerMessageStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
match command {
|
||||||
|
ClientCommand::CheckAuth(args) => check_authorization(args, server_connection).await,
|
||||||
|
ClientCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
||||||
|
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
||||||
|
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
||||||
|
ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await,
|
||||||
|
ClientCommand::EditPrivs(args) => {
|
||||||
|
edit_database_privileges(args, None, server_connection).await
|
||||||
|
}
|
||||||
|
ClientCommand::CreateUser(args) => create_users(args, server_connection).await,
|
||||||
|
ClientCommand::DropUser(args) => drop_users(args, server_connection).await,
|
||||||
|
ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
||||||
|
ClientCommand::ShowUser(args) => show_users(args, server_connection).await,
|
||||||
|
ClientCommand::LockUser(args) => lock_users(args, server_connection).await,
|
||||||
|
ClientCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **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(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let args: Args = Args::parse();
|
||||||
|
|
||||||
|
let connection = bootstrap_server_connection_and_drop_privileges(
|
||||||
|
args.server_socket_path,
|
||||||
|
args.config_path,
|
||||||
|
args.verbose,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
tokio_run_command(args.command, connection)?;
|
||||||
|
|
||||||
|
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 executing_in_suid_sgid_mode()? {
|
||||||
|
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::Args::command(),
|
||||||
|
"mysql-useradm" => mysql_useradm::Args::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| {
|
||||||
|
PathBuf::from(s)
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
});
|
||||||
|
|
||||||
|
match argv0.as_deref() {
|
||||||
|
Some("mysql-dbadm") => mysql_dbadm::main().map(Some),
|
||||||
|
Some("mysql-useradm") => mysql_useradm::main().map(Some),
|
||||||
|
_ => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the given commmand (from the client side) using Tokio.
|
||||||
|
fn tokio_run_command(
|
||||||
|
command: ClientCommand,
|
||||||
|
server_connection: StdUnixStream,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.context("Failed to start Tokio runtime")?
|
||||||
|
.block_on(async {
|
||||||
|
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||||
|
let mut message_stream = create_client_to_server_message_stream(tokio_socket);
|
||||||
|
|
||||||
|
while let Some(Ok(message)) = message_stream.next().await {
|
||||||
|
match message {
|
||||||
|
Response::Error(err) => {
|
||||||
|
anyhow::bail!("{}", err);
|
||||||
|
}
|
||||||
|
Response::Ready => break,
|
||||||
|
message => {
|
||||||
|
eprintln!("Unexpected message from server: {:?}", message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_command(command, message_stream).await
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,11 +3,11 @@ use std::path::PathBuf;
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||||
use tracing_subscriber::prelude::*;
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
|
||||||
use crate::{
|
use muscl_lib::{
|
||||||
core::common::{ASCII_BANNER, DEFAULT_CONFIG_PATH, KIND_REGARDS},
|
core::common::{ASCII_BANNER, DEFAULT_CONFIG_PATH, KIND_REGARDS},
|
||||||
server::supervisor::Supervisor,
|
server::{landlock::landlock_restrict_server, supervisor::Supervisor},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
@@ -25,6 +25,29 @@ pub struct ServerArgs {
|
|||||||
/// This is useful if you are planning to reload the server's configuration.
|
/// This is useful if you are planning to reload the server's configuration.
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub disable_landlock: bool,
|
pub disable_landlock: bool,
|
||||||
|
|
||||||
|
// NOTE: be careful not to add short options that collide with the `edit-privs` privilege
|
||||||
|
// characters. It should in theory be possible for `edit-privs` to ignore any options
|
||||||
|
// specified here, but in practice clap is being difficult to work with.
|
||||||
|
/// Path to where the server's unix socket should be created. This is only relevant when
|
||||||
|
/// not using systemd socket activation.
|
||||||
|
#[arg(
|
||||||
|
long = "socket",
|
||||||
|
value_name = "PATH",
|
||||||
|
value_hint = clap::ValueHint::FilePath,
|
||||||
|
)]
|
||||||
|
socket_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Config file to use for the server.
|
||||||
|
#[arg(
|
||||||
|
long = "config",
|
||||||
|
value_name = "PATH",
|
||||||
|
value_hint = clap::ValueHint::FilePath,
|
||||||
|
)]
|
||||||
|
config_path: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
verbosity: Verbosity<InfoLevel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone)]
|
#[derive(Subcommand, Debug, Clone)]
|
||||||
@@ -48,16 +71,34 @@ const LOG_LEVEL_WARNING: &str = r#"
|
|||||||
===================================================
|
===================================================
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
pub fn trace_server_prelude() {
|
const MIN_TOKIO_WORKER_THREADS: usize = 4;
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let args = ServerArgs::parse();
|
||||||
|
|
||||||
|
if !args.disable_landlock {
|
||||||
|
landlock_restrict_server(args.config_path.as_deref())
|
||||||
|
.context("Failed to apply Landlock restrictions to the server process")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let worker_thread_count = std::cmp::max(num_cpus::get(), MIN_TOKIO_WORKER_THREADS);
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.worker_threads(worker_thread_count)
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.context("Failed to start Tokio runtime")?
|
||||||
|
.block_on(handle_command(args))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trace_server_prelude() {
|
||||||
let message = [ASCII_BANNER, "", KIND_REGARDS, ""].join("\n");
|
let message = [ASCII_BANNER, "", KIND_REGARDS, ""].join("\n");
|
||||||
tracing::info!(message);
|
tracing::info!(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_command(
|
async fn handle_command(args: ServerArgs) -> anyhow::Result<()> {
|
||||||
config_path: Option<PathBuf>,
|
|
||||||
verbosity: Verbosity<InfoLevel>,
|
|
||||||
args: ServerArgs,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let mut auto_detected_systemd_mode = false;
|
let mut auto_detected_systemd_mode = false;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -77,7 +118,7 @@ pub async fn handle_command(
|
|||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
let subscriber = tracing_subscriber::Registry::default()
|
let subscriber = tracing_subscriber::Registry::default()
|
||||||
.with(verbosity.tracing_level_filter())
|
.with(args.verbosity.tracing_level_filter())
|
||||||
.with(tracing_journald::layer()?);
|
.with(tracing_journald::layer()?);
|
||||||
|
|
||||||
tracing::subscriber::set_global_default(subscriber)
|
tracing::subscriber::set_global_default(subscriber)
|
||||||
@@ -85,7 +126,7 @@ pub async fn handle_command(
|
|||||||
|
|
||||||
trace_server_prelude();
|
trace_server_prelude();
|
||||||
|
|
||||||
if verbosity.tracing_level_filter() >= tracing::Level::TRACE {
|
if args.verbosity.tracing_level_filter() >= tracing::Level::TRACE {
|
||||||
tracing::warn!("{}", LOG_LEVEL_WARNING.trim());
|
tracing::warn!("{}", LOG_LEVEL_WARNING.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +138,7 @@ pub async fn handle_command(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let subscriber = tracing_subscriber::Registry::default()
|
let subscriber = tracing_subscriber::Registry::default()
|
||||||
.with(verbosity.tracing_level_filter())
|
.with(args.verbosity.tracing_level_filter())
|
||||||
.with(
|
.with(
|
||||||
tracing_subscriber::fmt::layer()
|
tracing_subscriber::fmt::layer()
|
||||||
.with_line_number(cfg!(debug_assertions))
|
.with_line_number(cfg!(debug_assertions))
|
||||||
@@ -114,7 +155,9 @@ pub async fn handle_command(
|
|||||||
tracing::debug!("Running in standalone mode");
|
tracing::debug!("Running in standalone mode");
|
||||||
}
|
}
|
||||||
|
|
||||||
let config_path = config_path.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH));
|
let config_path = args
|
||||||
|
.config_path
|
||||||
|
.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH));
|
||||||
|
|
||||||
match args.subcmd {
|
match args.subcmd {
|
||||||
ServerCommand::Listen => {
|
ServerCommand::Listen => {
|
||||||
6
src/lib.rs
Normal file
6
src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate prettytable;
|
||||||
|
|
||||||
|
pub mod client;
|
||||||
|
pub mod core;
|
||||||
|
pub mod server;
|
||||||
308
src/main.rs
308
src/main.rs
@@ -1,308 +0,0 @@
|
|||||||
#[macro_use]
|
|
||||||
extern crate prettytable;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use clap::{CommandFactory, Parser, crate_version};
|
|
||||||
use clap_complete::CompleteEnv;
|
|
||||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
|
||||||
use tokio::net::UnixStream as TokioUnixStream;
|
|
||||||
|
|
||||||
use futures_util::StreamExt;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
core::{
|
|
||||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
|
||||||
common::{ASCII_BANNER, KIND_REGARDS, executing_in_suid_sgid_mode},
|
|
||||||
protocol::{Response, create_client_to_server_message_stream},
|
|
||||||
},
|
|
||||||
server::{command::ServerArgs, landlock::landlock_restrict_server},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
|
||||||
use crate::client::mysql_admutils_compatibility::{mysql_dbadm, mysql_useradm};
|
|
||||||
|
|
||||||
mod server;
|
|
||||||
|
|
||||||
mod client;
|
|
||||||
mod core;
|
|
||||||
|
|
||||||
const fn long_version() -> &'static str {
|
|
||||||
macro_rules! feature {
|
|
||||||
($title:expr, $flag:expr) => {
|
|
||||||
if cfg!(feature = $flag) {
|
|
||||||
concat!($title, ": enabled")
|
|
||||||
} else {
|
|
||||||
concat!($title, ": disabled")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const_format::concatcp!(
|
|
||||||
crate_version!(),
|
|
||||||
"\n",
|
|
||||||
"build profile: ",
|
|
||||||
env!("BUILD_PROFILE"),
|
|
||||||
"\n",
|
|
||||||
"commit: ",
|
|
||||||
env!("GIT_COMMIT"),
|
|
||||||
"\n\n",
|
|
||||||
"[features]\n",
|
|
||||||
feature!("SUID/SGID mode", "suid-sgid-mode"),
|
|
||||||
"\n",
|
|
||||||
feature!(
|
|
||||||
"mysql-admutils compatibility",
|
|
||||||
"mysql-admutils-compatibility"
|
|
||||||
),
|
|
||||||
"\n",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const LONG_VERSION: &str = long_version();
|
|
||||||
|
|
||||||
const EXAMPLES: &str = const_format::concatcp!(
|
|
||||||
color_print::cstr!("<bold><underline>Examples:</underline></bold>"),
|
|
||||||
r#"
|
|
||||||
# Display help information for any specific command
|
|
||||||
muscl <command> --help
|
|
||||||
|
|
||||||
# Create two users 'alice_user1' and 'alice_user2'
|
|
||||||
muscl create-user alice_user1 alice_user2
|
|
||||||
|
|
||||||
# Create two databases 'alice_db1' and 'alice_db2'
|
|
||||||
muscl create-db alice_db1 alice_db2
|
|
||||||
|
|
||||||
# Grant Select, Update, Insert and Delete privileges on 'alice_db1' to 'alice_user1'
|
|
||||||
muscl edit-privs alice_db1 alice_user1 +suid
|
|
||||||
|
|
||||||
# Show all databases
|
|
||||||
muscl show-db
|
|
||||||
|
|
||||||
# Show which users have privileges on which databases
|
|
||||||
muscl show-privs
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
const BEFORE_LONG_HELP: &str = const_format::concatcp!("\x1b[1m", ASCII_BANNER, "\x1b[0m");
|
|
||||||
const AFTER_LONG_HELP: &str = const_format::concatcp!(EXAMPLES, "\n", KIND_REGARDS,);
|
|
||||||
|
|
||||||
/// Database administration tool for non-admin users to manage their own MySQL databases and users.
|
|
||||||
///
|
|
||||||
/// This tool allows you to manage users and databases in MySQL.
|
|
||||||
///
|
|
||||||
/// You are only allowed to manage databases and users that are prefixed with
|
|
||||||
/// either your username, or a group that you are a member of.
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(
|
|
||||||
bin_name = "muscl",
|
|
||||||
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
|
||||||
version,
|
|
||||||
about,
|
|
||||||
disable_help_subcommand = true,
|
|
||||||
propagate_version = true,
|
|
||||||
before_long_help = BEFORE_LONG_HELP,
|
|
||||||
after_long_help = AFTER_LONG_HELP,
|
|
||||||
long_version = LONG_VERSION,
|
|
||||||
// NOTE: All non-registered "subcommands" are processed before Arg::parse() is called.
|
|
||||||
subcommand_required = true,
|
|
||||||
)]
|
|
||||||
struct Args {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Command,
|
|
||||||
|
|
||||||
// NOTE: be careful not to add short options that collide with the `edit-privs` privilege
|
|
||||||
// characters. It should in theory be possible for `edit-privs` to ignore any options
|
|
||||||
// specified here, but in practice clap is being difficult to work with.
|
|
||||||
/// Path to the socket of the server, if it already exists.
|
|
||||||
#[arg(
|
|
||||||
long,
|
|
||||||
value_name = "PATH",
|
|
||||||
value_hint = clap::ValueHint::FilePath,
|
|
||||||
global = true,
|
|
||||||
hide_short_help = true
|
|
||||||
)]
|
|
||||||
server_socket_path: Option<PathBuf>,
|
|
||||||
|
|
||||||
/// Config file to use for the server.
|
|
||||||
#[arg(
|
|
||||||
long,
|
|
||||||
value_name = "PATH",
|
|
||||||
value_hint = clap::ValueHint::FilePath,
|
|
||||||
global = true,
|
|
||||||
hide_short_help = true
|
|
||||||
)]
|
|
||||||
config: Option<PathBuf>,
|
|
||||||
|
|
||||||
#[command(flatten)]
|
|
||||||
verbose: Verbosity<InfoLevel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
|
||||||
enum Command {
|
|
||||||
#[command(flatten)]
|
|
||||||
Client(client::commands::ClientCommand),
|
|
||||||
|
|
||||||
/// Run the server
|
|
||||||
#[command(hide = true)]
|
|
||||||
Server(server::command::ServerArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **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(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let args: Args = Args::parse();
|
|
||||||
|
|
||||||
if handle_server_command(&args)?.is_some() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection = bootstrap_server_connection_and_drop_privileges(
|
|
||||||
args.server_socket_path,
|
|
||||||
args.config,
|
|
||||||
args.verbose,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
tokio_run_command(args.command, connection)?;
|
|
||||||
|
|
||||||
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 executing_in_suid_sgid_mode()? {
|
|
||||||
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::Args::command(),
|
|
||||||
"mysql-useradm" => mysql_useradm::Args::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| {
|
|
||||||
PathBuf::from(s)
|
|
||||||
.file_name()
|
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
|
||||||
});
|
|
||||||
|
|
||||||
match argv0.as_deref() {
|
|
||||||
Some("mysql-dbadm") => mysql_dbadm::main().map(Some),
|
|
||||||
Some("mysql-useradm") => mysql_useradm::main().map(Some),
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **WARNING:** This function may be run with elevated privileges.
|
|
||||||
fn handle_server_command(args: &Args) -> anyhow::Result<Option<()>> {
|
|
||||||
match args.command {
|
|
||||||
Command::Server(ref command) => {
|
|
||||||
assert!(
|
|
||||||
!executing_in_suid_sgid_mode()?,
|
|
||||||
"The executable should not be SUID or SGID when running the server manually"
|
|
||||||
);
|
|
||||||
|
|
||||||
if !command.disable_landlock {
|
|
||||||
landlock_restrict_server(args.config.as_deref())
|
|
||||||
.context("Failed to apply Landlock restrictions to the server process")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
tokio_start_server(
|
|
||||||
args.config.to_owned(),
|
|
||||||
args.verbose.to_owned(),
|
|
||||||
command.to_owned(),
|
|
||||||
)?;
|
|
||||||
Ok(Some(()))
|
|
||||||
}
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const MIN_TOKIO_WORKER_THREADS: usize = 4;
|
|
||||||
|
|
||||||
/// Start a long-lived server using Tokio.
|
|
||||||
fn tokio_start_server(
|
|
||||||
config_path: Option<PathBuf>,
|
|
||||||
verbosity: Verbosity<InfoLevel>,
|
|
||||||
args: ServerArgs,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let worker_thread_count = std::cmp::max(num_cpus::get(), MIN_TOKIO_WORKER_THREADS);
|
|
||||||
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.worker_threads(worker_thread_count)
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.context("Failed to start Tokio runtime")?
|
|
||||||
.block_on(server::command::handle_command(
|
|
||||||
config_path,
|
|
||||||
verbosity,
|
|
||||||
args,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run the given commmand (from the client side) using Tokio.
|
|
||||||
///
|
|
||||||
/// **WARNING:** This function may be run with elevated privileges.
|
|
||||||
fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyhow::Result<()> {
|
|
||||||
tokio::runtime::Builder::new_current_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.context("Failed to start Tokio runtime")?
|
|
||||||
.block_on(async {
|
|
||||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
|
||||||
let mut message_stream = create_client_to_server_message_stream(tokio_socket);
|
|
||||||
|
|
||||||
while let Some(Ok(message)) = message_stream.next().await {
|
|
||||||
match message {
|
|
||||||
Response::Error(err) => {
|
|
||||||
anyhow::bail!("{}", err);
|
|
||||||
}
|
|
||||||
Response::Ready => break,
|
|
||||||
message => {
|
|
||||||
eprintln!("Unexpected message from server: {:?}", message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match command {
|
|
||||||
Command::Client(client_args) => {
|
|
||||||
client::commands::handle_command(client_args, message_stream).await
|
|
||||||
}
|
|
||||||
Command::Server(_) => unreachable!(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod authorization;
|
pub mod authorization;
|
||||||
pub mod command;
|
|
||||||
mod common;
|
mod common;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod landlock;
|
pub mod landlock;
|
||||||
|
|||||||
Reference in New Issue
Block a user