diff --git a/Cargo.toml b/Cargo.toml
index b8d4f64..942e8ec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -67,10 +67,19 @@ default = ["mysql-admutils-compatibility"]
mysql-admutils-compatibility = []
suid-sgid-mode = []
+[lib]
+name = "muscl_lib"
+path = "src/lib.rs"
+
[[bin]]
name = "muscl"
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]
inherits = "release"
@@ -120,6 +129,11 @@ assets = [
"usr/bin/",
"755",
],
+ [
+ "target/release/muscl-server",
+ "usr/bin/",
+ "755",
+ ],
[
"target/release/mysql-useradm",
"usr/bin/",
diff --git a/assets/systemd/muscl.service b/assets/systemd/muscl.service
index c3bc7cd..5d5585d 100644
--- a/assets/systemd/muscl.service
+++ b/assets/systemd/muscl.service
@@ -5,7 +5,7 @@ After=mysql.service mariadb.service
[Service]
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
WatchdogSec=15
diff --git a/nix/default.nix b/nix/default.nix
index c2f36bd..734fa5b 100644
--- a/nix/default.nix
+++ b/nix/default.nix
@@ -84,7 +84,7 @@ buildFunction ({
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" \
- --replace-fail '/usr/bin/muscl' "$out/bin/muscl"
+ --replace-fail '/usr/bin/muscl-server' "$out/bin/muscl-server"
'';
meta = with lib; {
diff --git a/nix/module.nix b/nix/module.nix
index 723e978..748b461 100644
--- a/nix/module.nix
+++ b/nix/module.nix
@@ -132,7 +132,7 @@ in
serviceConfig = {
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 = [
diff --git a/src/client/commands.rs b/src/client/commands.rs
index 8dbdbec..1d4b79f 100644
--- a/src/client/commands.rs
+++ b/src/client/commands.rs
@@ -24,153 +24,12 @@ pub use show_privs::*;
pub use show_user::*;
pub use unlock_user::*;
-use clap::Subcommand;
use futures_util::SinkExt;
use itertools::Itertools;
use tokio_stream::StreamExt;
use crate::core::protocol::{ClientToServerMessageStream, Request, Response};
-const EDIT_PRIVS_EXAMPLES: &str = color_print::cstr!(
- r#"
-Examples:
- # 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:
- ///
- /// - ``: The name of the database for which you want to edit privileges.
- /// - ``: 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 ... | <[+-]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.
///
/// 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,
/// 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,
) -> anyhow::Result<()> {
server_connection
diff --git a/src/entrypoints/muscl.rs b/src/entrypoints/muscl.rs
new file mode 100644
index 0000000..f716015
--- /dev/null
+++ b/src/entrypoints/muscl.rs
@@ -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!("Examples:"),
+ r#"
+ # Display help information for any specific command
+ muscl --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 ",
+ 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,
+
+ // 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,
+
+ #[command(flatten)]
+ verbose: Verbosity,
+}
+
+const EDIT_PRIVS_EXAMPLES: &str = color_print::cstr!(
+ r#"
+Examples:
+ # 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:
+ ///
+ /// - ``: The name of the database for which you want to edit privileges.
+ /// - ``: 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 ... | <[+-]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