client/edit-privs: use a more human-friendly interface
All checks were successful
All checks were successful
This commit is contained in:
@@ -52,23 +52,28 @@ pub enum ClientCommand {
|
|||||||
|
|
||||||
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
||||||
///
|
///
|
||||||
/// This command has two modes of operation:
|
/// This command has three modes of operation:
|
||||||
///
|
///
|
||||||
/// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the privileges using a text editor.
|
/// 1. Interactive mode:
|
||||||
|
///
|
||||||
|
/// If no arguments are provided, the user will be prompted to edit the privileges using a text editor.
|
||||||
///
|
///
|
||||||
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
||||||
///
|
///
|
||||||
/// Follow the instructions inside the editor for more information.
|
/// Follow the instructions inside the editor for more information.
|
||||||
///
|
///
|
||||||
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write privileges using arguments.
|
/// 2. Non-interactive human-friendly mode:
|
||||||
///
|
///
|
||||||
/// The privilege arguments should be formatted as `<db>:<user>:<op><privileges>`
|
/// You can provide the command with three positional arguments:
|
||||||
/// where the privileges are a string of characters, each representing a single privilege.
|
///
|
||||||
|
/// - `<DB_NAME>`: The name of the database for which you want to edit privileges.
|
||||||
|
/// - `<USER_NAME>`: The name of the user whose privileges you want to edit.
|
||||||
|
/// - `<[+-]PRIVILEGES>`: A string representing the privileges to set for the user.
|
||||||
|
///
|
||||||
|
/// The `<[+-]PRIVILEGES>` argument is a string of characters, each representing a single privilege.
|
||||||
/// The character `A` is an exception - it represents all privileges.
|
/// The character `A` is an exception - it represents all privileges.
|
||||||
///
|
/// The optional leading character can be either `+` to grant additional privileges or `-` to revoke privileges.
|
||||||
/// The `<op>` character is optional and can be either `+` to grant additional privileges
|
/// If omitted, the privileges will be set exactly as specified, removing any privileges not listed, and adding any that are.
|
||||||
/// or `-` to revoke privileges. If omitted, the privileges will be set exactly as specified,
|
|
||||||
/// removing any privileges not listed, and adding any that are.
|
|
||||||
///
|
///
|
||||||
/// The character-to-privilege mapping is defined as follows:
|
/// The character-to-privilege mapping is defined as follows:
|
||||||
///
|
///
|
||||||
@@ -85,30 +90,36 @@ pub enum ClientCommand {
|
|||||||
/// - `r` - REFERENCES
|
/// - `r` - REFERENCES
|
||||||
/// - `A` - ALL PRIVILEGES
|
/// - `A` - ALL PRIVILEGES
|
||||||
///
|
///
|
||||||
/// If you provide a database name, you can omit it from the privilege string,
|
/// 3. Non-interactive batch mode:
|
||||||
/// e.g. `edit-privs my_db -p my_user:siu` is equivalent to `edit-privs -p my_db:my_user:siu`.
|
///
|
||||||
/// While it doesn't make much of a difference for a single edit, it can be useful for editing multiple users
|
/// By using the `-p` flag, you can provide multiple privilege edits in a single command.
|
||||||
/// on the same database at once.
|
///
|
||||||
|
/// The flag value should be formatted as `DB_NAME:USER_NAME:[+-]PRIVILEGES`
|
||||||
|
/// where the privileges are a string of characters, each representing a single privilege.
|
||||||
|
/// (See the character-to-privilege mapping above.)
|
||||||
///
|
///
|
||||||
/// Example usage of non-interactive mode:
|
/// Example usage of non-interactive mode:
|
||||||
///
|
///
|
||||||
/// Enable privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
|
/// Set privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
|
||||||
///
|
///
|
||||||
/// `muscl edit-privs -p my_db:my_user:siu`
|
/// `muscl edit-privs my_db my_user siu`
|
||||||
///
|
///
|
||||||
/// Enable all privileges for user `my_other_user` on database `my_other_db`:
|
/// Set all privileges for user `my_other_user` on database `my_other_db`:
|
||||||
///
|
///
|
||||||
/// `muscl edit-privs -p my_other_db:my_other_user:A`
|
/// `muscl edit-privs my_other_db my_other_user A`
|
||||||
///
|
|
||||||
/// Set miscellaneous privileges for multiple users on database `my_db`:
|
|
||||||
///
|
|
||||||
/// `muscl edit-privs my_db -p my_user:siu my_other_user:ct``
|
|
||||||
///
|
///
|
||||||
/// Add the `DELETE` privilege for user `my_user` on database `my_db`:
|
/// Add the `DELETE` privilege for user `my_user` on database `my_db`:
|
||||||
///
|
///
|
||||||
/// `muscl edit-privs my_db -p my_user:+d
|
/// `muscl edit-privs -p my_db my_user +d
|
||||||
///
|
///
|
||||||
#[command(verbatim_doc_comment)]
|
/// Set miscellaneous privileges for multiple users on database `my_db`:
|
||||||
|
///
|
||||||
|
/// `muscl edit-privs -p my_db:my_user:siu -p my_db:my_other_user:+ct`
|
||||||
|
///
|
||||||
|
#[command(
|
||||||
|
verbatim_doc_comment,
|
||||||
|
override_usage = "muscl edit-privs [OPTIONS] [ -p <DB_NAME:USER_NAME:[+-]PRIVILEGES>... | <DB_NAME> <USER_NAME> <[+-]PRIVILEGES> ]"
|
||||||
|
)]
|
||||||
EditPrivs(EditPrivsArgs),
|
EditPrivs(EditPrivsArgs),
|
||||||
|
|
||||||
/// Create one or more users
|
/// Create one or more users
|
||||||
@@ -142,7 +153,9 @@ pub async fn handle_command(
|
|||||||
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
||||||
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
||||||
ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await,
|
ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await,
|
||||||
ClientCommand::EditPrivs(args) => edit_database_privileges(args, server_connection).await,
|
ClientCommand::EditPrivs(args) => {
|
||||||
|
edit_database_privileges(args, None, server_connection).await
|
||||||
|
}
|
||||||
ClientCommand::CreateUser(args) => create_users(args, server_connection).await,
|
ClientCommand::CreateUser(args) => create_users(args, server_connection).await,
|
||||||
ClientCommand::DropUser(args) => drop_users(args, server_connection).await,
|
ClientCommand::DropUser(args) => drop_users(args, server_connection).await,
|
||||||
ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::{Args, Parser};
|
||||||
use clap_complete::ArgValueCompleter;
|
use clap_complete::ArgValueCompleter;
|
||||||
use dialoguer::{Confirm, Editor};
|
use dialoguer::{Confirm, Editor};
|
||||||
use futures_util::SinkExt;
|
use futures_util::SinkExt;
|
||||||
@@ -11,11 +11,11 @@ use tokio_stream::StreamExt;
|
|||||||
use crate::{
|
use crate::{
|
||||||
client::commands::erroneous_server_response,
|
client::commands::erroneous_server_response,
|
||||||
core::{
|
core::{
|
||||||
completion::mysql_database_completer,
|
completion::{mysql_database_completer, mysql_user_completer},
|
||||||
database_privileges::{
|
database_privileges::{
|
||||||
DatabasePrivilegeEditEntry, DatabasePrivilegeRow, DatabasePrivilegeRowDiff,
|
DatabasePrivilegeEdit, DatabasePrivilegeEditEntry, DatabasePrivilegeRow,
|
||||||
DatabasePrivilegesDiff, create_or_modify_privilege_rows, diff_privileges,
|
DatabasePrivilegeRowDiff, DatabasePrivilegesDiff, create_or_modify_privilege_rows,
|
||||||
display_privilege_diffs, generate_editor_content_from_privilege_data,
|
diff_privileges, display_privilege_diffs, generate_editor_content_from_privilege_data,
|
||||||
parse_privilege_data_from_editor_content, reduce_privilege_diffs,
|
parse_privilege_data_from_editor_content, reduce_privilege_diffs,
|
||||||
},
|
},
|
||||||
protocol::{
|
protocol::{
|
||||||
@@ -28,20 +28,24 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Parser, Debug, Clone)]
|
#[derive(Parser, Debug, Clone)]
|
||||||
pub struct EditPrivsArgs {
|
pub struct EditPrivsArgs {
|
||||||
/// The MySQL database to edit privileges for
|
/// The privileges to set, grant or revoke, in the format `DATABASE:USER:[+-]PRIVILEGES`
|
||||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
///
|
||||||
#[arg(value_name = "DB_NAME")]
|
/// This option allows for changing privileges for multiple databases and users in batch.
|
||||||
pub name: Option<MySQLDatabase>,
|
///
|
||||||
|
/// This can not be used together with the positional `DB_NAME`, `USER_NAME` and `PRIVILEGES` arguments.
|
||||||
#[arg(
|
#[arg(
|
||||||
short,
|
short,
|
||||||
long,
|
long,
|
||||||
value_name = "[DATABASE:]USER:[+-]PRIVILEGES",
|
value_name = "DB_NAME:USER_NAME:[+-]PRIVILEGES",
|
||||||
num_args = 0..,
|
num_args = 0..,
|
||||||
value_parser = DatabasePrivilegeEditEntry::parse_from_str,
|
value_parser = DatabasePrivilegeEditEntry::parse_from_str,
|
||||||
|
conflicts_with("single_priv"),
|
||||||
)]
|
)]
|
||||||
pub privs: Vec<DatabasePrivilegeEditEntry>,
|
pub privs: Vec<DatabasePrivilegeEditEntry>,
|
||||||
|
|
||||||
|
#[command(flatten)]
|
||||||
|
pub single_priv: Option<SinglePrivilegeEditArgs>,
|
||||||
|
|
||||||
/// Print the information as JSON
|
/// Print the information as JSON
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
@@ -60,6 +64,31 @@ pub struct EditPrivsArgs {
|
|||||||
pub yes: bool,
|
pub yes: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args, Debug, Clone)]
|
||||||
|
pub struct SinglePrivilegeEditArgs {
|
||||||
|
/// The MySQL database to edit privileges for
|
||||||
|
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
||||||
|
#[arg(
|
||||||
|
value_name = "DB_NAME",
|
||||||
|
requires = "user_name",
|
||||||
|
requires = "single_priv"
|
||||||
|
)]
|
||||||
|
pub db_name: Option<MySQLDatabase>,
|
||||||
|
|
||||||
|
/// The MySQL database to edit privileges for
|
||||||
|
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||||
|
#[arg(value_name = "USER_NAME")]
|
||||||
|
pub user_name: Option<MySQLUser>,
|
||||||
|
|
||||||
|
/// The privileges to set, grant or revoke
|
||||||
|
#[arg(
|
||||||
|
allow_hyphen_values = true,
|
||||||
|
value_name = "[+-]PRIVILEGES",
|
||||||
|
value_parser = DatabasePrivilegeEdit::parse_from_str,
|
||||||
|
)]
|
||||||
|
pub single_priv: Option<DatabasePrivilegeEdit>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn users_exist(
|
async fn users_exist(
|
||||||
server_connection: &mut ClientToServerMessageStream,
|
server_connection: &mut ClientToServerMessageStream,
|
||||||
privilege_diff: &BTreeSet<DatabasePrivilegesDiff>,
|
privilege_diff: &BTreeSet<DatabasePrivilegesDiff>,
|
||||||
@@ -120,12 +149,42 @@ async fn databases_exist(
|
|||||||
|
|
||||||
pub async fn edit_database_privileges(
|
pub async fn edit_database_privileges(
|
||||||
args: EditPrivsArgs,
|
args: EditPrivsArgs,
|
||||||
|
// NOTE: this is only used for backwards compat with mysql-admutils
|
||||||
|
use_database: Option<MySQLDatabase>,
|
||||||
mut server_connection: ClientToServerMessageStream,
|
mut server_connection: ClientToServerMessageStream,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let message = Request::ListPrivileges(args.name.to_owned().map(|name| vec![name]));
|
let message = Request::ListPrivileges(use_database.clone().map(|db| vec![db]));
|
||||||
|
|
||||||
server_connection.send(message).await?;
|
server_connection.send(message).await?;
|
||||||
|
|
||||||
|
debug_assert!(args.privs.is_empty() ^ args.single_priv.is_none());
|
||||||
|
|
||||||
|
let privs = if let Some(single_priv_entry) = &args.single_priv {
|
||||||
|
let database = single_priv_entry.db_name.clone().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"DB_NAME must be specified when editing privileges in single privilege mode"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let user = single_priv_entry.user_name.clone().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"USER_NAME must be specified when DB_NAME is specified in single privilege mode"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let privilege_edit = single_priv_entry.single_priv.clone().ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"PRIVILEGES must be specified when DB_NAME is specified in single privilege mode"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
vec![DatabasePrivilegeEditEntry {
|
||||||
|
database,
|
||||||
|
user,
|
||||||
|
privilege_edit,
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
args.privs.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let existing_privilege_rows = match server_connection.next().await {
|
let existing_privilege_rows = match server_connection.next().await {
|
||||||
Some(Ok(Response::ListPrivileges(databases))) => databases
|
Some(Ok(Response::ListPrivileges(databases))) => databases
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -151,12 +210,12 @@ pub async fn edit_database_privileges(
|
|||||||
response => return erroneous_server_response(response),
|
response => return erroneous_server_response(response),
|
||||||
};
|
};
|
||||||
|
|
||||||
let diffs: BTreeSet<DatabasePrivilegesDiff> = if !args.privs.is_empty() {
|
let diffs: BTreeSet<DatabasePrivilegesDiff> = if !privs.is_empty() {
|
||||||
let privileges_to_change = parse_privilege_tables_from_args(&args)?;
|
let privileges_to_change = parse_privilege_tables(&privs)?;
|
||||||
create_or_modify_privilege_rows(&existing_privilege_rows, &privileges_to_change)?
|
create_or_modify_privilege_rows(&existing_privilege_rows, &privileges_to_change)?
|
||||||
} else {
|
} else {
|
||||||
let privileges_to_change =
|
let privileges_to_change =
|
||||||
edit_privileges_with_editor(&existing_privilege_rows, args.name.as_ref())?;
|
edit_privileges_with_editor(&existing_privilege_rows, use_database.as_ref())?;
|
||||||
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -220,15 +279,15 @@ pub async fn edit_database_privileges(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_privilege_tables_from_args(
|
fn parse_privilege_tables(
|
||||||
args: &EditPrivsArgs,
|
privs: &[DatabasePrivilegeEditEntry],
|
||||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>> {
|
) -> anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>> {
|
||||||
debug_assert!(!args.privs.is_empty());
|
debug_assert!(!privs.is_empty());
|
||||||
args.privs
|
privs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|priv_edit_entry| {
|
.map(|priv_edit_entry| {
|
||||||
priv_edit_entry
|
priv_edit_entry
|
||||||
.as_database_privileges_diff(args.name.as_ref())
|
.as_database_privileges_diff()
|
||||||
.context(format!(
|
.context(format!(
|
||||||
"Failed parsing database privileges: `{}`",
|
"Failed parsing database privileges: `{}`",
|
||||||
priv_edit_entry
|
priv_edit_entry
|
||||||
@@ -239,6 +298,7 @@ fn parse_privilege_tables_from_args(
|
|||||||
|
|
||||||
fn edit_privileges_with_editor(
|
fn edit_privileges_with_editor(
|
||||||
privilege_data: &[DatabasePrivilegeRow],
|
privilege_data: &[DatabasePrivilegeRow],
|
||||||
|
// NOTE: this is only used for backwards compat with mysql-admtools
|
||||||
database_name: Option<&MySQLDatabase>,
|
database_name: Option<&MySQLDatabase>,
|
||||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||||
let unix_user = User::from_uid(getuid())
|
let unix_user = User::from_uid(getuid())
|
||||||
|
|||||||
@@ -208,14 +208,19 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
|||||||
Command::Show(args) => show_databases(args, message_stream).await,
|
Command::Show(args) => show_databases(args, message_stream).await,
|
||||||
Command::Editperm(args) => {
|
Command::Editperm(args) => {
|
||||||
let edit_privileges_args = EditPrivsArgs {
|
let edit_privileges_args = EditPrivsArgs {
|
||||||
name: Some(args.database),
|
single_priv: None,
|
||||||
privs: vec![],
|
privs: vec![],
|
||||||
json: false,
|
json: false,
|
||||||
editor: None,
|
editor: None,
|
||||||
yes: false,
|
yes: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
edit_database_privileges(edit_privileges_args, message_stream).await
|
edit_database_privileges(
|
||||||
|
edit_privileges_args,
|
||||||
|
Some(args.database),
|
||||||
|
message_stream,
|
||||||
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
//! This module contains serialization and deserialization logic for
|
//! This module contains serialization and deserialization logic for
|
||||||
//! database privileges related CLI commands.
|
//! database privileges related CLI commands.
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff};
|
use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff};
|
||||||
use crate::core::types::{MySQLDatabase, MySQLUser};
|
use crate::core::types::{MySQLDatabase, MySQLUser};
|
||||||
|
|
||||||
|
const VALID_PRIVILEGE_EDIT_CHARS: &[char] = &[
|
||||||
|
's', 'i', 'u', 'd', 'c', 'D', 'a', 'A', 'I', 't', 'l', 'r', 'A',
|
||||||
|
];
|
||||||
|
|
||||||
/// This enum represents a part of a CLI argument for editing database privileges,
|
/// This enum represents a part of a CLI argument for editing database privileges,
|
||||||
/// indicating whether privileges are to be added, set, or removed.
|
/// indicating whether privileges are to be added, set, or removed.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -13,17 +19,76 @@ pub enum DatabasePrivilegeEditEntryType {
|
|||||||
Remove,
|
Remove,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct DatabasePrivilegeEdit {
|
||||||
|
pub type_: DatabasePrivilegeEditEntryType,
|
||||||
|
pub privileges: Vec<char>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabasePrivilegeEdit {
|
||||||
|
pub fn parse_from_str(input: &str) -> anyhow::Result<Self> {
|
||||||
|
let (edit_type, privs_str) = if let Some(privs_str) = input.strip_prefix('+') {
|
||||||
|
(DatabasePrivilegeEditEntryType::Add, privs_str)
|
||||||
|
} else if let Some(privs_str) = input.strip_prefix('-') {
|
||||||
|
(DatabasePrivilegeEditEntryType::Remove, privs_str)
|
||||||
|
} else {
|
||||||
|
(DatabasePrivilegeEditEntryType::Set, input)
|
||||||
|
};
|
||||||
|
|
||||||
|
let privileges: Vec<char> = privs_str.chars().collect();
|
||||||
|
|
||||||
|
if privileges
|
||||||
|
.iter()
|
||||||
|
.any(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c))
|
||||||
|
{
|
||||||
|
let invalid_chars: String = privileges
|
||||||
|
.iter()
|
||||||
|
.filter(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c))
|
||||||
|
.map(|c| format!("'{c}'"))
|
||||||
|
.join(", ");
|
||||||
|
let valid_characters: String = VALID_PRIVILEGE_EDIT_CHARS
|
||||||
|
.iter()
|
||||||
|
.map(|c| format!("'{c}'"))
|
||||||
|
.join(", ");
|
||||||
|
anyhow::bail!(
|
||||||
|
"Invalid character(s) in privilege edit entry: {}\n\nValid characters are: {}",
|
||||||
|
invalid_chars,
|
||||||
|
valid_characters,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(DatabasePrivilegeEdit {
|
||||||
|
type_: edit_type,
|
||||||
|
privileges,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for DatabasePrivilegeEdit {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self.type_ {
|
||||||
|
DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
|
||||||
|
DatabasePrivilegeEditEntryType::Set => {}
|
||||||
|
DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
|
||||||
|
}
|
||||||
|
for priv_char in &self.privileges {
|
||||||
|
write!(f, "{}", priv_char)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// This struct represents a single CLI argument for editing database privileges.
|
/// This struct represents a single CLI argument for editing database privileges.
|
||||||
///
|
///
|
||||||
/// This is typically parsed from a string looking like:
|
/// This is typically parsed from a string looking like:
|
||||||
///
|
///
|
||||||
/// `[database_name:]username:[+|-]privileges`
|
/// `database_name:username:[+|-]privileges`
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct DatabasePrivilegeEditEntry {
|
pub struct DatabasePrivilegeEditEntry {
|
||||||
pub database: Option<MySQLDatabase>,
|
pub database: MySQLDatabase,
|
||||||
pub user: MySQLUser,
|
pub user: MySQLUser,
|
||||||
pub type_: DatabasePrivilegeEditEntryType,
|
pub privilege_edit: DatabasePrivilegeEdit,
|
||||||
pub privileges: Vec<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DatabasePrivilegeEditEntry {
|
impl DatabasePrivilegeEditEntry {
|
||||||
@@ -31,80 +96,41 @@ impl DatabasePrivilegeEditEntry {
|
|||||||
///
|
///
|
||||||
/// The expected format is:
|
/// The expected format is:
|
||||||
///
|
///
|
||||||
/// `[database_name:]username:[+|-]privileges`
|
/// `database_name:username:[+|-]privileges`
|
||||||
///
|
///
|
||||||
/// where:
|
/// where:
|
||||||
/// - database_name is optional, if omitted the entry applies to all databases
|
/// - database_name is the name of the database to edit privileges for
|
||||||
/// - username is the name of the user to edit privileges for
|
/// - username is the name of the user to edit privileges for
|
||||||
/// - privileges is a string of characters representing the privileges to add, set or remove
|
/// - privileges is a string of characters representing the privileges to add, set or remove
|
||||||
/// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly
|
/// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly
|
||||||
/// - privileges characters are: siudcDaAItlrA
|
/// - privileges characters are: siudcDaAItlrA
|
||||||
pub fn parse_from_str(arg: &str) -> anyhow::Result<DatabasePrivilegeEditEntry> {
|
pub fn parse_from_str(arg: &str) -> anyhow::Result<Self> {
|
||||||
let parts: Vec<&str> = arg.split(':').collect();
|
let parts: Vec<&str> = arg.split(':').collect();
|
||||||
if parts.len() < 2 || parts.len() > 3 {
|
if parts.len() != 3 {
|
||||||
anyhow::bail!("Invalid privilege edit entry format: {}", arg);
|
anyhow::bail!("Invalid privilege edit entry format: {}", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (database, user, user_privs) = if parts.len() == 3 {
|
let (database, user, user_privs) = (parts[0].to_string(), parts[1].to_string(), parts[2]);
|
||||||
(Some(parts[0].to_string()), parts[1].to_string(), parts[2])
|
|
||||||
} else {
|
|
||||||
(None, parts[0].to_string(), parts[1])
|
|
||||||
};
|
|
||||||
|
|
||||||
if user.is_empty() {
|
if user.is_empty() {
|
||||||
anyhow::bail!("Username cannot be empty in privilege edit entry: {}", arg);
|
anyhow::bail!("Username cannot be empty in privilege edit entry: {}", arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (edit_type, privs_str) = if let Some(privs_str) = user_privs.strip_prefix('+') {
|
let privilege_edit = DatabasePrivilegeEdit::parse_from_str(user_privs)?;
|
||||||
(DatabasePrivilegeEditEntryType::Add, privs_str)
|
|
||||||
} else if let Some(privs_str) = user_privs.strip_prefix('-') {
|
|
||||||
(DatabasePrivilegeEditEntryType::Remove, privs_str)
|
|
||||||
} else {
|
|
||||||
(DatabasePrivilegeEditEntryType::Set, user_privs)
|
|
||||||
};
|
|
||||||
|
|
||||||
let privileges: Vec<String> = privs_str.chars().map(|c| c.to_string()).collect();
|
|
||||||
if privileges.iter().any(|c| !"siudcDaAItlrA".contains(c)) {
|
|
||||||
let invalid_chars: String = privileges
|
|
||||||
.iter()
|
|
||||||
.filter(|c| !"siudcDaAItlrA".contains(c.as_str()))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
anyhow::bail!(
|
|
||||||
"Invalid character(s) in privilege edit entry: {}",
|
|
||||||
invalid_chars
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(DatabasePrivilegeEditEntry {
|
Ok(DatabasePrivilegeEditEntry {
|
||||||
database: database.map(MySQLDatabase::from),
|
database: MySQLDatabase::from(database),
|
||||||
user: MySQLUser::from(user),
|
user: MySQLUser::from(user),
|
||||||
type_: edit_type,
|
privilege_edit,
|
||||||
privileges,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_database_privileges_diff(
|
pub fn as_database_privileges_diff(&self) -> anyhow::Result<DatabasePrivilegeRowDiff> {
|
||||||
&self,
|
|
||||||
external_database_name: Option<&MySQLDatabase>,
|
|
||||||
) -> anyhow::Result<DatabasePrivilegeRowDiff> {
|
|
||||||
let database = match self.database.as_ref() {
|
|
||||||
Some(db) => db.clone(),
|
|
||||||
None => {
|
|
||||||
if let Some(external_db) = external_database_name {
|
|
||||||
external_db.clone()
|
|
||||||
} else {
|
|
||||||
anyhow::bail!(
|
|
||||||
"Database name must be specified either in the privilege edit entry or as an external argument."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut diff;
|
let mut diff;
|
||||||
match self.type_ {
|
match self.privilege_edit.type_ {
|
||||||
DatabasePrivilegeEditEntryType::Set => {
|
DatabasePrivilegeEditEntryType::Set => {
|
||||||
diff = DatabasePrivilegeRowDiff {
|
diff = DatabasePrivilegeRowDiff {
|
||||||
db: database,
|
db: self.database.clone(),
|
||||||
user: self.user.clone(),
|
user: self.user.clone(),
|
||||||
select_priv: Some(DatabasePrivilegeChange::YesToNo),
|
select_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||||
insert_priv: Some(DatabasePrivilegeChange::YesToNo),
|
insert_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||||
@@ -118,20 +144,20 @@ impl DatabasePrivilegeEditEntry {
|
|||||||
lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo),
|
lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||||
references_priv: Some(DatabasePrivilegeChange::YesToNo),
|
references_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||||
};
|
};
|
||||||
for priv_char in &self.privileges {
|
for priv_char in &self.privilege_edit.privileges {
|
||||||
match priv_char.as_str() {
|
match priv_char {
|
||||||
"s" => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
|
's' => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"i" => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'i' => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"u" => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'u' => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"d" => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'd' => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"c" => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'c' => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"D" => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'D' => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"a" => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'a' => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"I" => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'I' => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"t" => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
|
't' => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"l" => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'l' => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"r" => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
|
'r' => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||||
"A" => {
|
'A' => {
|
||||||
diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
|
diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||||
diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
|
diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||||
diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
|
diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||||
@@ -150,7 +176,7 @@ impl DatabasePrivilegeEditEntry {
|
|||||||
}
|
}
|
||||||
DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
|
DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
|
||||||
diff = DatabasePrivilegeRowDiff {
|
diff = DatabasePrivilegeRowDiff {
|
||||||
db: database,
|
db: self.database.clone(),
|
||||||
user: self.user.clone(),
|
user: self.user.clone(),
|
||||||
select_priv: None,
|
select_priv: None,
|
||||||
insert_priv: None,
|
insert_priv: None,
|
||||||
@@ -164,25 +190,25 @@ impl DatabasePrivilegeEditEntry {
|
|||||||
lock_tables_priv: None,
|
lock_tables_priv: None,
|
||||||
references_priv: None,
|
references_priv: None,
|
||||||
};
|
};
|
||||||
let value = match self.type_ {
|
let value = match self.privilege_edit.type_ {
|
||||||
DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
|
DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
|
||||||
DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
|
DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
for priv_char in &self.privileges {
|
for priv_char in &self.privilege_edit.privileges {
|
||||||
match priv_char.as_str() {
|
match priv_char {
|
||||||
"s" => diff.select_priv = Some(value),
|
's' => diff.select_priv = Some(value),
|
||||||
"i" => diff.insert_priv = Some(value),
|
'i' => diff.insert_priv = Some(value),
|
||||||
"u" => diff.update_priv = Some(value),
|
'u' => diff.update_priv = Some(value),
|
||||||
"d" => diff.delete_priv = Some(value),
|
'd' => diff.delete_priv = Some(value),
|
||||||
"c" => diff.create_priv = Some(value),
|
'c' => diff.create_priv = Some(value),
|
||||||
"D" => diff.drop_priv = Some(value),
|
'D' => diff.drop_priv = Some(value),
|
||||||
"a" => diff.alter_priv = Some(value),
|
'a' => diff.alter_priv = Some(value),
|
||||||
"I" => diff.index_priv = Some(value),
|
'I' => diff.index_priv = Some(value),
|
||||||
"t" => diff.create_tmp_table_priv = Some(value),
|
't' => diff.create_tmp_table_priv = Some(value),
|
||||||
"l" => diff.lock_tables_priv = Some(value),
|
'l' => diff.lock_tables_priv = Some(value),
|
||||||
"r" => diff.references_priv = Some(value),
|
'r' => diff.references_priv = Some(value),
|
||||||
"A" => {
|
'A' => {
|
||||||
diff.select_priv = Some(value);
|
diff.select_priv = Some(value);
|
||||||
diff.insert_priv = Some(value);
|
diff.insert_priv = Some(value);
|
||||||
diff.update_priv = Some(value);
|
diff.update_priv = Some(value);
|
||||||
@@ -207,19 +233,9 @@ impl DatabasePrivilegeEditEntry {
|
|||||||
|
|
||||||
impl std::fmt::Display for DatabasePrivilegeEditEntry {
|
impl std::fmt::Display for DatabasePrivilegeEditEntry {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if let Some(db) = &self.database {
|
write!(f, "{}:, ", self.database)?;
|
||||||
write!(f, "{}:, ", db)?;
|
|
||||||
}
|
|
||||||
write!(f, "{}: ", self.user)?;
|
write!(f, "{}: ", self.user)?;
|
||||||
match self.type_ {
|
write!(f, "{}", self.privilege_edit)?;
|
||||||
DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
|
|
||||||
DatabasePrivilegeEditEntryType::Set => {}
|
|
||||||
DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
|
|
||||||
}
|
|
||||||
for priv_char in &self.privileges {
|
|
||||||
write!(f, "{}", priv_char)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,10 +250,12 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.ok(),
|
result.ok(),
|
||||||
Some(DatabasePrivilegeEditEntry {
|
Some(DatabasePrivilegeEditEntry {
|
||||||
database: Some("db".into()),
|
database: "db".into(),
|
||||||
user: "user".into(),
|
user: "user".into(),
|
||||||
type_: DatabasePrivilegeEditEntryType::Set,
|
privilege_edit: DatabasePrivilegeEdit {
|
||||||
privileges: vec!["A".into()],
|
type_: DatabasePrivilegeEditEntryType::Set,
|
||||||
|
privileges: vec!['A'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -248,10 +266,12 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.ok(),
|
result.ok(),
|
||||||
Some(DatabasePrivilegeEditEntry {
|
Some(DatabasePrivilegeEditEntry {
|
||||||
database: Some("db".into()),
|
database: "db".into(),
|
||||||
user: "user".into(),
|
user: "user".into(),
|
||||||
type_: DatabasePrivilegeEditEntryType::Set,
|
privilege_edit: DatabasePrivilegeEdit {
|
||||||
privileges: vec![],
|
type_: DatabasePrivilegeEditEntryType::Set,
|
||||||
|
privileges: vec![],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -262,28 +282,16 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.ok(),
|
result.ok(),
|
||||||
Some(DatabasePrivilegeEditEntry {
|
Some(DatabasePrivilegeEditEntry {
|
||||||
database: Some("db".into()),
|
database: "db".into(),
|
||||||
user: "user".into(),
|
user: "user".into(),
|
||||||
type_: DatabasePrivilegeEditEntryType::Set,
|
privilege_edit: DatabasePrivilegeEdit {
|
||||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
type_: DatabasePrivilegeEditEntryType::Set,
|
||||||
|
privileges: vec!['s', 'i', 'u', 'd'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_cli_arg_parse_set_user_nonexistent_misc() {
|
|
||||||
let result = DatabasePrivilegeEditEntry::parse_from_str("user:siud");
|
|
||||||
assert_eq!(
|
|
||||||
result.ok(),
|
|
||||||
Some(DatabasePrivilegeEditEntry {
|
|
||||||
database: None,
|
|
||||||
user: "user".into(),
|
|
||||||
type_: DatabasePrivilegeEditEntryType::Set,
|
|
||||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
|
fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
|
||||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
|
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
|
||||||
@@ -308,10 +316,12 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.ok(),
|
result.ok(),
|
||||||
Some(DatabasePrivilegeEditEntry {
|
Some(DatabasePrivilegeEditEntry {
|
||||||
database: Some("db".into()),
|
database: "db".into(),
|
||||||
user: "user".into(),
|
user: "user".into(),
|
||||||
type_: DatabasePrivilegeEditEntryType::Add,
|
privilege_edit: DatabasePrivilegeEdit {
|
||||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
type_: DatabasePrivilegeEditEntryType::Add,
|
||||||
|
privileges: vec!['s', 'i', 'u', 'd'],
|
||||||
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -322,10 +332,12 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
result.ok(),
|
result.ok(),
|
||||||
Some(DatabasePrivilegeEditEntry {
|
Some(DatabasePrivilegeEditEntry {
|
||||||
database: Some("db".into()),
|
database: "db".into(),
|
||||||
user: "user".into(),
|
user: "user".into(),
|
||||||
type_: DatabasePrivilegeEditEntryType::Remove,
|
privilege_edit: DatabasePrivilegeEdit {
|
||||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
type_: DatabasePrivilegeEditEntryType::Remove,
|
||||||
|
privileges: vec!['s', 'i', 'u', 'd'],
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ struct Args {
|
|||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Command,
|
command: Command,
|
||||||
|
|
||||||
|
// NOTE: be careful not to add short options that collide with the `edit-privs` privilege
|
||||||
|
// characters. It should in theory be possible for `edit-privs` to ignore any options
|
||||||
|
// specified here, but in practice clap is being difficult to work with.
|
||||||
/// Path to the socket of the server, if it already exists.
|
/// Path to the socket of the server, if it already exists.
|
||||||
#[arg(
|
#[arg(
|
||||||
short,
|
|
||||||
long,
|
long,
|
||||||
value_name = "PATH",
|
value_name = "PATH",
|
||||||
value_hint = clap::ValueHint::FilePath,
|
value_hint = clap::ValueHint::FilePath,
|
||||||
@@ -99,7 +101,6 @@ struct Args {
|
|||||||
|
|
||||||
/// Config file to use for the server.
|
/// Config file to use for the server.
|
||||||
#[arg(
|
#[arg(
|
||||||
short,
|
|
||||||
long,
|
long,
|
||||||
value_name = "PATH",
|
value_name = "PATH",
|
||||||
value_hint = clap::ValueHint::FilePath,
|
value_hint = clap::ValueHint::FilePath,
|
||||||
|
|||||||
Reference in New Issue
Block a user