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

            
7
use crate::{
8
    client::{
9
        commands::{EditPrivsArgs, 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

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

            
34
The file should contain one line per user, starting with the
35
username and followed by ten Y/N-values seperated by whitespace.
36
Lines starting with # are ignored.
37

            
38
The 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 'muscl' 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
)]
65
pub 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/muscl/issues/29
97
#[derive(Parser)]
98
pub 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)]
120
pub 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)]
127
pub 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)]
134
pub 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)]
141
pub 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.
147
pub 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

            
177
fn 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 = EditPrivsArgs {
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

            
204
async 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

            
230
async 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

            
256
async 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]
312
fn yn(value: bool) -> &'static str {
313
    if value { "Y" } else { "N" }
314
}
315

            
316
fn 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
}