mysqladm/cli/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    cli::{
9        common::erroneous_server_response,
10        database_command,
11        mysql_admutils_compatibility::{
12            common::trim_db_name_to_32_chars,
13            error_messages::{
14                format_show_database_error_message, handle_create_database_error,
15                handle_drop_database_error,
16            },
17        },
18    },
19    core::{
20        bootstrap::bootstrap_server_connection_and_drop_privileges,
21        database_privileges::DatabasePrivilegeRow,
22        protocol::{
23            ClientToServerMessageStream, GetDatabasesPrivilegeDataError, MySQLDatabase, Request,
24            Response, create_client_to_server_message_stream,
25        },
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 = database_command::DatabaseEditPrivsArgs {
191                        name: Some(args.database),
192                        privs: vec![],
193                        json: false,
194                        editor: None,
195                        yes: false,
196                    };
197
198                    database_command::edit_database_privileges(edit_privileges_args, message_stream)
199                        .await
200                }
201            }
202        })
203}
204
205async fn create_databases(
206    args: CreateArgs,
207    mut server_connection: ClientToServerMessageStream,
208) -> anyhow::Result<()> {
209    let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
210
211    let message = Request::CreateDatabases(database_names);
212    server_connection.send(message).await?;
213
214    let result = match server_connection.next().await {
215        Some(Ok(Response::CreateDatabases(result))) => result,
216        response => return erroneous_server_response(response),
217    };
218
219    server_connection.send(Request::Exit).await?;
220
221    for (name, result) in result {
222        match result {
223            Ok(()) => println!("Database {} created.", name),
224            Err(err) => handle_create_database_error(err, &name),
225        }
226    }
227
228    Ok(())
229}
230
231async fn drop_databases(
232    args: DatabaseDropArgs,
233    mut server_connection: ClientToServerMessageStream,
234) -> anyhow::Result<()> {
235    let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
236
237    let message = Request::DropDatabases(database_names);
238    server_connection.send(message).await?;
239
240    let result = match server_connection.next().await {
241        Some(Ok(Response::DropDatabases(result))) => result,
242        response => return erroneous_server_response(response),
243    };
244
245    server_connection.send(Request::Exit).await?;
246
247    for (name, result) in result {
248        match result {
249            Ok(()) => println!("Database {} dropped.", name),
250            Err(err) => handle_drop_database_error(err, &name),
251        }
252    }
253
254    Ok(())
255}
256
257async fn show_databases(
258    args: DatabaseShowArgs,
259    mut server_connection: ClientToServerMessageStream,
260) -> anyhow::Result<()> {
261    let database_names: Vec<MySQLDatabase> =
262        args.name.iter().map(trim_db_name_to_32_chars).collect();
263
264    let message = if database_names.is_empty() {
265        let message = Request::ListDatabases(None);
266        server_connection.send(message).await?;
267        let response = server_connection.next().await;
268        let databases = match response {
269            Some(Ok(Response::ListAllDatabases(databases))) => databases.unwrap_or(vec![]),
270            response => return erroneous_server_response(response),
271        };
272
273        let database_names = databases.into_iter().map(|db| db.database).collect();
274
275        Request::ListPrivileges(Some(database_names))
276    } else {
277        Request::ListPrivileges(Some(database_names))
278    };
279    server_connection.send(message).await?;
280
281    let response = server_connection.next().await;
282
283    server_connection.send(Request::Exit).await?;
284
285    // NOTE: mysql-dbadm show has a quirk where valid database names
286    //       for non-existent databases will report with no users.
287    let results: Vec<Result<(MySQLDatabase, Vec<DatabasePrivilegeRow>), String>> = match response {
288        Some(Ok(Response::ListPrivileges(result))) => result
289            .into_iter()
290            .map(
291                |(name, rows)| match rows.map(|rows| (name.to_owned(), rows)) {
292                    Ok(rows) => Ok(rows),
293                    Err(GetDatabasesPrivilegeDataError::DatabaseDoesNotExist) => Ok((name, vec![])),
294                    Err(err) => Err(format_show_database_error_message(err, &name)),
295                },
296            )
297            .collect(),
298        response => return erroneous_server_response(response),
299    };
300
301    results.into_iter().try_for_each(|result| match result {
302        Ok((name, rows)) => print_db_privs(&name, rows),
303        Err(err) => {
304            eprintln!("{}", err);
305            Ok(())
306        }
307    })?;
308
309    Ok(())
310}
311
312#[inline]
313fn yn(value: bool) -> &'static str {
314    if value { "Y" } else { "N" }
315}
316
317fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) -> anyhow::Result<()> {
318    println!(
319        concat!(
320            "Database '{}':\n",
321            "# User                Select  Insert  Update  Delete  Create   Drop   Alter   Index    Temp    Lock  References\n",
322            "# ----------------    ------  ------  ------  ------  ------   ----   -----   -----    ----    ----  ----------"
323        ),
324        name,
325    );
326    if rows.is_empty() {
327        println!("# (no permissions currently granted to any users)");
328    } else {
329        for privilege in rows {
330            println!(
331                "  {:<16}      {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
332                privilege.user,
333                yn(privilege.select_priv),
334                yn(privilege.insert_priv),
335                yn(privilege.update_priv),
336                yn(privilege.delete_priv),
337                yn(privilege.create_priv),
338                yn(privilege.drop_priv),
339                yn(privilege.alter_priv),
340                yn(privilege.index_priv),
341                yn(privilege.create_tmp_table_priv),
342                yn(privilege.lock_tables_priv),
343                yn(privilege.references_priv)
344            );
345        }
346    }
347
348    Ok(())
349}