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, ListPrivilegesError, Request, Response,
26
            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 ten 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
";
54

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

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

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

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

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

            
107
    /// delete the DATABASE(s).
108
    Drop(DatabaseDropArgs),
109

            
110
    /// give information about the DATABASE(s), or, if
111
    /// none are given, all the ones you own.
112
    Show(DatabaseShowArgs),
113

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

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

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

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

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

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

            
159
    if args.help_editperm {
160
        println!("{HELP_DB_PERM}");
161
        return Ok(());
162
    }
163

            
164
    let server_connection = bootstrap_server_connection_and_drop_privileges(
165
        args.server_socket_path,
166
        args.config,
167
        Verbosity::default(),
168
    )?;
169

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

            
178
    tokio_run_command(command, server_connection)?;
179

            
180
    Ok(())
181
}
182

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

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

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

            
217
                    edit_database_privileges(
218
                        edit_privileges_args,
219
                        Some(args.database),
220
                        message_stream,
221
                    )
222
                    .await
223
                }
224
            }
225
        })
226
}
227

            
228
async fn create_databases(
229
    args: CreateArgs,
230
    mut server_connection: ClientToServerMessageStream,
231
) -> anyhow::Result<()> {
232
    let database_names = args.name.iter().map(trim_db_name_to_32_chars).collect();
233

            
234
    let message = Request::CreateDatabases(database_names);
235
    server_connection.send(message).await?;
236

            
237
    let result = match server_connection.next().await {
238
        Some(Ok(Response::CreateDatabases(result))) => result,
239
        response => return erroneous_server_response(response),
240
    };
241

            
242
    server_connection.send(Request::Exit).await?;
243

            
244
    for (name, result) in result {
245
        match result {
246
            Ok(()) => println!("Database {name} created."),
247
            Err(err) => handle_create_database_error(&err, &name),
248
        }
249
    }
250

            
251
    Ok(())
252
}
253

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

            
260
    let message = Request::DropDatabases(database_names);
261
    server_connection.send(message).await?;
262

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

            
268
    server_connection.send(Request::Exit).await?;
269

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

            
277
    Ok(())
278
}
279

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

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

            
296
        let database_names = databases.into_iter().map(|db| db.database).collect();
297

            
298
        Request::ListPrivileges(Some(database_names))
299
    } else {
300
        Request::ListPrivileges(Some(database_names))
301
    };
302
    server_connection.send(message).await?;
303

            
304
    let response = server_connection.next().await;
305

            
306
    server_connection.send(Request::Exit).await?;
307

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

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

            
329
    Ok(())
330
}
331

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

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