mysqladm/client/mysql_admutils_compatibility/
mysql_dbadm.rs

1use clap::Parser;
2use futures_util::{SinkExt, StreamExt};
3use std::os::unix::net::UnixStream as StdUnixStream;
4use std::path::PathBuf;
5use tokio::net::UnixStream as TokioUnixStream;
6
7use crate::{
8    client::{
9        commands::{EditDbPrivsArgs, edit_database_privileges, erroneous_server_response},
10        mysql_admutils_compatibility::{
11            common::trim_db_name_to_32_chars,
12            error_messages::{
13                format_show_database_error_message, handle_create_database_error,
14                handle_drop_database_error,
15            },
16        },
17    },
18    core::{
19        bootstrap::bootstrap_server_connection_and_drop_privileges,
20        database_privileges::DatabasePrivilegeRow,
21        protocol::{
22            ClientToServerMessageStream, GetDatabasesPrivilegeDataError, Request, Response,
23            create_client_to_server_message_stream,
24        },
25        types::MySQLDatabase,
26    },
27};
28
29const HELP_DB_PERM: &str = r#"
30Edit permissions for the DATABASE(s). Running this command will
31spawn the editor stored in the $EDITOR environment variable.
32(pico will be used if the variable is unset)
33
34The file should contain one line per user, starting with the
35username and followed by ten Y/N-values seperated by whitespace.
36Lines starting with # are ignored.
37
38The Y/N-values corresponds to the following mysql privileges:
39  Select     - Enables use of SELECT
40  Insert     - Enables use of INSERT
41  Update     - Enables use of UPDATE
42  Delete     - Enables use of DELETE
43  Create     - Enables use of CREATE TABLE
44  Drop       - Enables use of DROP TABLE
45  Alter      - Enables use of ALTER TABLE
46  Index      - Enables use of CREATE INDEX and DROP INDEX
47  Temp       - Enables use of CREATE TEMPORARY TABLE
48  Lock       - Enables use of LOCK TABLE
49  References - Enables use of REFERENCES
50"#;
51
52/// Create, drop or edit permissions for the DATABASE(s),
53/// as determined by the COMMAND.
54///
55/// This is a compatibility layer for the mysql-dbadm command.
56/// Please consider using the newer mysqladm command instead.
57#[derive(Parser)]
58#[command(
59    bin_name = "mysql-dbadm",
60    version,
61    about,
62    disable_help_subcommand = true,
63    verbatim_doc_comment
64)]
65pub struct Args {
66    #[command(subcommand)]
67    pub command: Option<Command>,
68
69    /// Path to the socket of the server, if it already exists.
70    #[arg(
71        short,
72        long,
73        value_name = "PATH",
74        global = true,
75        hide_short_help = true
76    )]
77    server_socket_path: Option<PathBuf>,
78
79    /// Config file to use for the server.
80    #[arg(
81        short,
82        long,
83        value_name = "PATH",
84        global = true,
85        hide_short_help = true
86    )]
87    config: Option<PathBuf>,
88
89    /// Print help for the 'editperm' subcommand.
90    #[arg(long, global = true)]
91    pub help_editperm: bool,
92}
93
94// NOTE: mysql-dbadm explicitly calls privileges "permissions".
95//       This is something we're trying to move away from.
96//       See https://git.pvv.ntnu.no/Projects/mysqladm-rs/issues/29
97#[derive(Parser)]
98pub enum Command {
99    /// create the DATABASE(s).
100    Create(CreateArgs),
101
102    /// delete the DATABASE(s).
103    Drop(DatabaseDropArgs),
104
105    /// give information about the DATABASE(s), or, if
106    /// none are given, all the ones you own.
107    Show(DatabaseShowArgs),
108
109    // TODO: make this output more verbatim_doc_comment-like,
110    //       without messing up the indentation.
111    /// change permissions for the DATABASE(s). Your
112    /// favorite editor will be started, allowing you
113    /// to make changes to the permission table.
114    /// Run 'mysql-dbadm --help-editperm' for more
115    /// information.
116    Editperm(EditPermArgs),
117}
118
119#[derive(Parser)]
120pub struct CreateArgs {
121    /// The name of the DATABASE(s) to create.
122    #[arg(num_args = 1..)]
123    name: Vec<MySQLDatabase>,
124}
125
126#[derive(Parser)]
127pub struct DatabaseDropArgs {
128    /// The name of the DATABASE(s) to drop.
129    #[arg(num_args = 1..)]
130    name: Vec<MySQLDatabase>,
131}
132
133#[derive(Parser)]
134pub struct DatabaseShowArgs {
135    /// The name of the DATABASE(s) to show.
136    #[arg(num_args = 0..)]
137    name: Vec<MySQLDatabase>,
138}
139
140#[derive(Parser)]
141pub struct EditPermArgs {
142    /// The name of the DATABASE to edit permissions for.
143    pub database: MySQLDatabase,
144}
145
146/// **WARNING:** This function may be run with elevated privileges.
147pub fn main() -> anyhow::Result<()> {
148    let args: Args = Args::parse();
149
150    if args.help_editperm {
151        println!("{}", HELP_DB_PERM);
152        return Ok(());
153    }
154
155    let server_connection = bootstrap_server_connection_and_drop_privileges(
156        args.server_socket_path,
157        args.config,
158        Default::default(),
159    )?;
160
161    let command = match args.command {
162        Some(command) => command,
163        None => {
164            println!(
165                "Try `{} --help' for more information.",
166                std::env::args().next().unwrap_or("mysql-dbadm".to_string())
167            );
168            return Ok(());
169        }
170    };
171
172    tokio_run_command(command, server_connection)?;
173
174    Ok(())
175}
176
177fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyhow::Result<()> {
178    tokio::runtime::Builder::new_current_thread()
179        .enable_all()
180        .build()
181        .unwrap()
182        .block_on(async {
183            let tokio_socket = TokioUnixStream::from_std(server_connection)?;
184            let message_stream = create_client_to_server_message_stream(tokio_socket);
185            match command {
186                Command::Create(args) => create_databases(args, message_stream).await,
187                Command::Drop(args) => drop_databases(args, message_stream).await,
188                Command::Show(args) => show_databases(args, message_stream).await,
189                Command::Editperm(args) => {
190                    let edit_privileges_args = EditDbPrivsArgs {
191                        name: Some(args.database),
192                        privs: vec![],
193                        json: false,
194                        editor: None,
195                        yes: false,
196                    };
197
198                    edit_database_privileges(edit_privileges_args, message_stream).await
199                }
200            }
201        })
202}
203
204async fn create_databases(
205    args: CreateArgs,
206    mut server_connection: ClientToServerMessageStream,
207) -> anyhow::Result<()> {
208    let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
209
210    let message = Request::CreateDatabases(database_names);
211    server_connection.send(message).await?;
212
213    let result = match server_connection.next().await {
214        Some(Ok(Response::CreateDatabases(result))) => result,
215        response => return erroneous_server_response(response),
216    };
217
218    server_connection.send(Request::Exit).await?;
219
220    for (name, result) in result {
221        match result {
222            Ok(()) => println!("Database {} created.", name),
223            Err(err) => handle_create_database_error(err, &name),
224        }
225    }
226
227    Ok(())
228}
229
230async fn drop_databases(
231    args: DatabaseDropArgs,
232    mut server_connection: ClientToServerMessageStream,
233) -> anyhow::Result<()> {
234    let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
235
236    let message = Request::DropDatabases(database_names);
237    server_connection.send(message).await?;
238
239    let result = match server_connection.next().await {
240        Some(Ok(Response::DropDatabases(result))) => result,
241        response => return erroneous_server_response(response),
242    };
243
244    server_connection.send(Request::Exit).await?;
245
246    for (name, result) in result {
247        match result {
248            Ok(()) => println!("Database {} dropped.", name),
249            Err(err) => handle_drop_database_error(err, &name),
250        }
251    }
252
253    Ok(())
254}
255
256async fn show_databases(
257    args: DatabaseShowArgs,
258    mut server_connection: ClientToServerMessageStream,
259) -> anyhow::Result<()> {
260    let database_names: Vec<MySQLDatabase> =
261        args.name.iter().map(trim_db_name_to_32_chars).collect();
262
263    let message = if database_names.is_empty() {
264        let message = Request::ListDatabases(None);
265        server_connection.send(message).await?;
266        let response = server_connection.next().await;
267        let databases = match response {
268            Some(Ok(Response::ListAllDatabases(databases))) => databases.unwrap_or(vec![]),
269            response => return erroneous_server_response(response),
270        };
271
272        let database_names = databases.into_iter().map(|db| db.database).collect();
273
274        Request::ListPrivileges(Some(database_names))
275    } else {
276        Request::ListPrivileges(Some(database_names))
277    };
278    server_connection.send(message).await?;
279
280    let response = server_connection.next().await;
281
282    server_connection.send(Request::Exit).await?;
283
284    // NOTE: mysql-dbadm show has a quirk where valid database names
285    //       for non-existent databases will report with no users.
286    let results: Vec<Result<(MySQLDatabase, Vec<DatabasePrivilegeRow>), String>> = match response {
287        Some(Ok(Response::ListPrivileges(result))) => result
288            .into_iter()
289            .map(
290                |(name, rows)| match rows.map(|rows| (name.to_owned(), rows)) {
291                    Ok(rows) => Ok(rows),
292                    Err(GetDatabasesPrivilegeDataError::DatabaseDoesNotExist) => Ok((name, vec![])),
293                    Err(err) => Err(format_show_database_error_message(err, &name)),
294                },
295            )
296            .collect(),
297        response => return erroneous_server_response(response),
298    };
299
300    results.into_iter().try_for_each(|result| match result {
301        Ok((name, rows)) => print_db_privs(&name, rows),
302        Err(err) => {
303            eprintln!("{}", err);
304            Ok(())
305        }
306    })?;
307
308    Ok(())
309}
310
311#[inline]
312fn yn(value: bool) -> &'static str {
313    if value { "Y" } else { "N" }
314}
315
316fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) -> anyhow::Result<()> {
317    println!(
318        concat!(
319            "Database '{}':\n",
320            "# User                Select  Insert  Update  Delete  Create   Drop   Alter   Index    Temp    Lock  References\n",
321            "# ----------------    ------  ------  ------  ------  ------   ----   -----   -----    ----    ----  ----------"
322        ),
323        name,
324    );
325    if rows.is_empty() {
326        println!("# (no permissions currently granted to any users)");
327    } else {
328        for privilege in rows {
329            println!(
330                "  {:<16}      {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
331                privilege.user,
332                yn(privilege.select_priv),
333                yn(privilege.insert_priv),
334                yn(privilege.update_priv),
335                yn(privilege.delete_priv),
336                yn(privilege.create_priv),
337                yn(privilege.drop_priv),
338                yn(privilege.alter_priv),
339                yn(privilege.index_priv),
340                yn(privilege.create_tmp_table_priv),
341                yn(privilege.lock_tables_priv),
342                yn(privilege.references_priv)
343            );
344        }
345    }
346
347    Ok(())
348}