1
use clap::{Parser, Subcommand};
2
use clap_complete::ArgValueCompleter;
3
use clap_verbosity_flag::Verbosity;
4
use futures_util::{SinkExt, StreamExt};
5
use std::os::unix::net::UnixStream as StdUnixStream;
6
use std::path::PathBuf;
7
use tokio::net::UnixStream as TokioUnixStream;
8

            
9
use crate::{
10
    client::{
11
        commands::{EditPrivsArgs, edit_database_privileges, erroneous_server_response},
12
        mysql_admutils_compatibility::{
13
            common::trim_db_name_to_32_chars,
14
            error_messages::{
15
                format_show_database_error_message, handle_create_database_error,
16
                handle_drop_database_error,
17
            },
18
        },
19
    },
20
    core::{
21
        bootstrap::bootstrap_server_connection_and_drop_privileges,
22
        completion::{mysql_database_completer, prefix_completer},
23
        database_privileges::DatabasePrivilegeRow,
24
        protocol::{
25
            ClientToServerMessageStream, ListDatabasesRequest, ListPrivilegesError, Request,
26
            Response, create_client_to_server_message_stream,
27
        },
28
        types::MySQLDatabase,
29
    },
30
};
31

            
32
const HELP_DB_PERM: &str = r"
33
Edit permissions for the DATABASE(s). Running this command will
34
spawn the editor stored in the $EDITOR environment variable.
35
(pico will be used if the variable is unset)
36

            
37
The file should contain one line per user, starting with the
38
username and followed by fourteen Y/N-values separated by whitespace.
39
Lines starting with # are ignored.
40

            
41
The Y/N-values corresponds to the following mysql privileges:
42
  Select     - Enables use of SELECT
43
  Insert     - Enables use of INSERT
44
  Update     - Enables use of UPDATE
45
  Delete     - Enables use of DELETE
46
  Create     - Enables use of CREATE TABLE
47
  Drop       - Enables use of DROP TABLE
48
  Alter      - Enables use of ALTER TABLE
49
  Index      - Enables use of CREATE INDEX and DROP INDEX
50
  Temp       - Enables use of CREATE TEMPORARY TABLE
51
  Lock       - Enables use of LOCK TABLE
52
  References - Enables use of REFERENCES
53
  Create view - Enables use of CREATE VIEW
54
  Show view   - Enables use of SHOW CREATE VIEW
55
  Trigger     - Enables use of CREATE TRIGGER and DROP TRIGGER
56
";
57

            
58
/// Create, drop or edit permissions for the DATABASE(s),
59
/// as determined by the COMMAND.
60
///
61
/// This is a compatibility layer for the 'mysql-dbadm' command.
62
/// Please consider using the newer 'muscl' command instead.
63
#[derive(Parser)]
64
#[command(
65
    bin_name = "mysql-dbadm",
66
    version,
67
    about,
68
    disable_help_subcommand = true,
69
    verbatim_doc_comment
70
)]
71
pub struct Args {
72
    #[command(subcommand)]
73
    pub command: Option<Command>,
74

            
75
    /// Path to the socket of the server, if it already exists.
76
    #[arg(
77
        short,
78
        long,
79
        value_name = "PATH",
80
        value_hint = clap::ValueHint::FilePath,
81
        global = true,
82
        hide_short_help = true
83
    )]
84
    server_socket_path: Option<PathBuf>,
85

            
86
    /// Config file to use for the server.
87
    #[arg(
88
        short,
89
        long,
90
        value_name = "PATH",
91
        value_hint = clap::ValueHint::FilePath,
92
        global = true,
93
        hide_short_help = true
94
    )]
95
    config: Option<PathBuf>,
96

            
97
    /// Print help for the 'editperm' subcommand.
98
    #[arg(long, global = true)]
99
    pub help_editperm: bool,
100
}
101

            
102
// NOTE: mysql-dbadm explicitly calls privileges "permissions".
103
//       This is something we're trying to move away from.
104
//       See https://git.pvv.ntnu.no/Projects/muscl/issues/29
105
#[derive(Subcommand)]
106
pub enum Command {
107
    /// create the DATABASE(s).
108
    Create(CreateArgs),
109

            
110
    /// delete the DATABASE(s).
111
    Drop(DatabaseDropArgs),
112

            
113
    /// give information about the DATABASE(s), or, if
114
    /// none are given, all the ones you own.
115
    Show(DatabaseShowArgs),
116

            
117
    // TODO: make this output more verbatim_doc_comment-like,
118
    //       without messing up the indentation.
119
    /// change permissions for the DATABASE(s). Your
120
    /// favorite editor will be started, allowing you
121
    /// to make changes to the permission table.
122
    /// Run 'mysql-dbadm --help-editperm' for more
123
    /// information.
124
    Editperm(EditPermArgs),
125
}
126

            
127
#[derive(Parser)]
128
pub struct CreateArgs {
129
    /// The name of the DATABASE(s) to create.
130
    #[arg(num_args = 1..)]
131
    #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(prefix_completer)))]
132
    name: Vec<MySQLDatabase>,
133
}
134

            
135
#[derive(Parser)]
136
pub struct DatabaseDropArgs {
137
    /// The name of the DATABASE(s) to drop.
138
    #[arg(num_args = 1..)]
139
    #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
140
    name: Vec<MySQLDatabase>,
141
}
142

            
143
#[derive(Parser)]
144
pub struct DatabaseShowArgs {
145
    /// The name of the DATABASE(s) to show.
146
    #[arg(num_args = 0..)]
147
    #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
148
    name: Vec<MySQLDatabase>,
149
}
150

            
151
#[derive(Parser)]
152
pub struct EditPermArgs {
153
    /// The name of the DATABASE to edit permissions for.
154
    #[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
155
    pub database: MySQLDatabase,
156
}
157

            
158
/// **WARNING:** This function may be run with elevated privileges.
159
pub fn main() -> anyhow::Result<()> {
160
    let args: Args = Args::parse();
161

            
162
    if args.help_editperm {
163
        println!("{HELP_DB_PERM}");
164
        return Ok(());
165
    }
166

            
167
    let server_connection = bootstrap_server_connection_and_drop_privileges(
168
        args.server_socket_path,
169
        args.config,
170
        Verbosity::default(),
171
    )?;
172

            
173
    let Some(command) = args.command else {
174
        println!(
175
            "Try `{} --help' for more information.",
176
            std::env::args().next().unwrap_or("mysql-dbadm".to_string())
177
        );
178
        return Ok(());
179
    };
180

            
181
    tokio_run_command(command, server_connection)?;
182

            
183
    Ok(())
184
}
185

            
186
fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyhow::Result<()> {
187
    tokio::runtime::Builder::new_current_thread()
188
        .enable_all()
189
        .build()
190
        .unwrap()
191
        .block_on(async {
192
            let tokio_socket = TokioUnixStream::from_std(server_connection)?;
193
            let mut message_stream = create_client_to_server_message_stream(tokio_socket);
194

            
195
            while let Some(Ok(message)) = message_stream.next().await {
196
                match message {
197
                    Response::Error(err) => {
198
                        anyhow::bail!("{err}");
199
                    }
200
                    Response::Ready => break,
201
                    message => {
202
                        eprintln!("Unexpected message from server: {message:?}");
203
                    }
204
                }
205
            }
206

            
207
            match command {
208
                Command::Create(args) => create_databases(args, message_stream).await,
209
                Command::Drop(args) => drop_databases(args, message_stream).await,
210
                Command::Show(args) => show_databases(args, message_stream).await,
211
                Command::Editperm(args) => {
212
                    let edit_privileges_args = EditPrivsArgs {
213
                        single_priv: None,
214
                        privs: vec![],
215
                        json: false,
216
                        editor: None,
217
                        yes: false,
218
                    };
219

            
220
                    edit_database_privileges(
221
                        edit_privileges_args,
222
                        Some(args.database),
223
                        message_stream,
224
                    )
225
                    .await
226
                }
227
            }
228
        })
229
}
230

            
231
async fn create_databases(
232
    args: CreateArgs,
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::CreateDatabases(database_names);
238
    server_connection.send(message).await?;
239

            
240
    let result = match server_connection.next().await {
241
        Some(Ok(Response::CreateDatabases(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 {name} created."),
250
            Err(err) => handle_create_database_error(&err, &name),
251
        }
252
    }
253

            
254
    Ok(())
255
}
256

            
257
async fn drop_databases(
258
    args: DatabaseDropArgs,
259
    mut server_connection: ClientToServerMessageStream,
260
) -> anyhow::Result<()> {
261
    let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
262

            
263
    let message = Request::DropDatabases(database_names);
264
    server_connection.send(message).await?;
265

            
266
    let result = match server_connection.next().await {
267
        Some(Ok(Response::DropDatabases(result))) => result,
268
        response => return erroneous_server_response(response),
269
    };
270

            
271
    server_connection.send(Request::Exit).await?;
272

            
273
    for (name, result) in result {
274
        match result {
275
            Ok(()) => println!("Database {name} dropped."),
276
            Err(err) => handle_drop_database_error(&err, &name),
277
        }
278
    }
279

            
280
    Ok(())
281
}
282

            
283
async fn show_databases(
284
    args: DatabaseShowArgs,
285
    mut server_connection: ClientToServerMessageStream,
286
) -> anyhow::Result<()> {
287
    let database_names: Vec<MySQLDatabase> =
288
        args.name.iter().map(trim_db_name_to_32_chars).collect();
289

            
290
    let message = if database_names.is_empty() {
291
        let message = Request::ListDatabases(ListDatabasesRequest::new(None, false));
292
        server_connection.send(message).await?;
293
        let response = server_connection.next().await;
294
        let databases = match response {
295
            Some(Ok(Response::ListAllDatabases(databases))) => databases.unwrap_or(vec![]),
296
            response => return erroneous_server_response(response),
297
        };
298

            
299
        let database_names = databases.into_iter().map(|db| db.database).collect();
300

            
301
        Request::ListPrivileges(Some(database_names))
302
    } else {
303
        Request::ListPrivileges(Some(database_names))
304
    };
305
    server_connection.send(message).await?;
306

            
307
    let response = server_connection.next().await;
308

            
309
    server_connection.send(Request::Exit).await?;
310

            
311
    // NOTE: mysql-dbadm show has a quirk where valid database names
312
    //       for non-existent databases will report with no users.
313
    let results: Vec<Result<(MySQLDatabase, Vec<DatabasePrivilegeRow>), String>> = match response {
314
        Some(Ok(Response::ListPrivileges(result))) => result
315
            .into_iter()
316
            .map(|(name, rows)| match rows.map(|rows| (name.clone(), rows)) {
317
                Ok(rows) => Ok(rows),
318
                Err(ListPrivilegesError::DatabaseDoesNotExist) => Ok((name, vec![])),
319
                Err(err) => Err(format_show_database_error_message(&err, &name)),
320
            })
321
            .collect(),
322
        response => return erroneous_server_response(response),
323
    };
324

            
325
    for result in results {
326
        match result {
327
            Ok((name, rows)) => print_db_privs(&name, rows),
328
            Err(err) => eprintln!("{err}"),
329
        }
330
    }
331

            
332
    Ok(())
333
}
334

            
335
#[inline]
336
fn yn(value: bool) -> &'static str {
337
    if value { "Y" } else { "N" }
338
}
339

            
340
fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) {
341
    println!(
342
        concat!(
343
            "Database '{}':\n",
344
            "# User                Select  Insert  Update  Delete  Create   Drop   Alter   Index    Temp    Lock  References  Create view  Show view  Trigger\n",
345
            "# ----------------    ------  ------  ------  ------  ------   ----   -----   -----    ----    ----  ----------  -----------  ---------  -------"
346
        ),
347
        name,
348
    );
349
    if rows.is_empty() {
350
        println!("# (no permissions currently granted to any users)");
351
    } else {
352
        for privilege in rows {
353
            println!(
354
                "  {:<16}      {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<11} {:<12} {:<10} {}",
355
                privilege.user,
356
                yn(privilege.select_priv),
357
                yn(privilege.insert_priv),
358
                yn(privilege.update_priv),
359
                yn(privilege.delete_priv),
360
                yn(privilege.create_priv),
361
                yn(privilege.drop_priv),
362
                yn(privilege.alter_priv),
363
                yn(privilege.index_priv),
364
                yn(privilege.create_tmp_table_priv),
365
                yn(privilege.lock_tables_priv),
366
                yn(privilege.references_priv),
367
                yn(privilege.create_view_priv),
368
                yn(privilege.show_view_priv),
369
                yn(privilege.trigger_priv)
370
            );
371
        }
372
    }
373
}