Refactor privilege handling
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "mysqladm-rs"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
license = "BSD3"
|
||||
authors = [
|
||||
"oysteikt@pvv.ntnu.no",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::{Confirm, Editor};
|
||||
@@ -10,9 +12,11 @@ use crate::{
|
||||
core::{
|
||||
common::yn,
|
||||
database_privileges::{
|
||||
DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeEditEntry, DatabasePrivilegeRow,
|
||||
DatabasePrivilegeRowDiff, DatabasePrivilegesDiff, create_or_modify_privilege_rows,
|
||||
db_priv_field_human_readable_name, diff_privileges, display_privilege_diffs,
|
||||
generate_editor_content_from_privilege_data, parse_privilege_data_from_editor_content,
|
||||
parse_privilege_table_cli_arg,
|
||||
reduce_privilege_diffs,
|
||||
},
|
||||
protocol::{
|
||||
ClientToServerMessageStream, MySQLDatabase, Request, Response,
|
||||
@@ -21,7 +25,6 @@ use crate::{
|
||||
print_modify_database_privileges_output_status,
|
||||
},
|
||||
},
|
||||
server::sql::database_privilege_operations::{DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow},
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
@@ -59,7 +62,7 @@ pub enum DatabaseCommand {
|
||||
///
|
||||
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write privileges using arguments.
|
||||
///
|
||||
/// The privilege arguments should be formatted as `<db>:<user>:<privileges>`
|
||||
/// The privilege arguments should be formatted as `<db>:<user>+<privileges>-<privileges>`
|
||||
/// where the privileges are a string of characters, each representing a single privilege.
|
||||
/// The character `A` is an exception - it represents all privileges.
|
||||
///
|
||||
@@ -79,7 +82,7 @@ pub enum DatabaseCommand {
|
||||
/// - `A` - ALL PRIVILEGES
|
||||
///
|
||||
/// If you provide a database name, you can omit it from the privilege string,
|
||||
/// e.g. `edit-db-privs my_db -p my_user:siu` is equivalent to `edit-db-privs -p my_db:my_user:siu`.
|
||||
/// e.g. `edit-db-privs my_db -p my_user+siu` is equivalent to `edit-db-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
|
||||
/// on the same database at once.
|
||||
///
|
||||
@@ -150,8 +153,8 @@ pub struct DatabaseEditPrivsArgs {
|
||||
/// The name of the database to edit privileges for
|
||||
pub name: Option<MySQLDatabase>,
|
||||
|
||||
#[arg(short, long, value_name = "[DATABASE:]USER:PRIVILEGES", num_args = 0..)]
|
||||
pub privs: Vec<String>,
|
||||
#[arg(short, long, value_name = "[DATABASE:]USER:[+-]PRIVILEGES", num_args = 0.., value_parser = DatabasePrivilegeEditEntry::parse_from_str)]
|
||||
pub privs: Vec<DatabasePrivilegeEditEntry>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
@@ -377,7 +380,7 @@ pub async fn edit_database_privileges(
|
||||
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let privilege_data = match server_connection.next().await {
|
||||
let existing_privilege_rows = match server_connection.next().await {
|
||||
Some(Ok(Response::ListPrivileges(databases))) => databases
|
||||
.into_iter()
|
||||
.filter_map(|(database_name, result)| match result {
|
||||
@@ -402,13 +405,15 @@ pub async fn edit_database_privileges(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
let privileges_to_change = if !args.privs.is_empty() {
|
||||
parse_privilege_tables_from_args(&args)?
|
||||
let diffs: BTreeSet<DatabasePrivilegesDiff> = if !args.privs.is_empty() {
|
||||
let privileges_to_change = parse_privilege_tables_from_args(&args)?;
|
||||
create_or_modify_privilege_rows(&existing_privilege_rows, &privileges_to_change)?
|
||||
} else {
|
||||
edit_privileges_with_editor(&privilege_data, args.name.as_ref())?
|
||||
let privileges_to_change =
|
||||
edit_privileges_with_editor(&existing_privilege_rows, args.name.as_ref())?;
|
||||
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
||||
};
|
||||
|
||||
let diffs = diff_privileges(&privilege_data, &privileges_to_change);
|
||||
let diffs = reduce_privilege_diffs(&existing_privilege_rows, diffs)?;
|
||||
|
||||
if diffs.is_empty() {
|
||||
println!("No changes to make.");
|
||||
@@ -447,26 +452,19 @@ pub async fn edit_database_privileges(
|
||||
|
||||
fn parse_privilege_tables_from_args(
|
||||
args: &DatabaseEditPrivsArgs,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>> {
|
||||
debug_assert!(!args.privs.is_empty());
|
||||
let result = if let Some(name) = &args.name {
|
||||
args.privs
|
||||
.iter()
|
||||
.map(|p| {
|
||||
parse_privilege_table_cli_arg(&format!("{}:{}", name, &p))
|
||||
.context(format!("Failed parsing database privileges: `{}`", &p))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
|
||||
} else {
|
||||
args.privs
|
||||
.iter()
|
||||
.map(|p| {
|
||||
parse_privilege_table_cli_arg(p)
|
||||
.context(format!("Failed parsing database privileges: `{}`", &p))
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
|
||||
};
|
||||
Ok(result)
|
||||
args.privs
|
||||
.iter()
|
||||
.map(|priv_edit_entry| {
|
||||
priv_edit_entry
|
||||
.as_database_privileges_diff(args.name.as_ref())
|
||||
.context(format!(
|
||||
"Failed parsing database privileges: `{}`",
|
||||
priv_edit_entry
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>>>()
|
||||
}
|
||||
|
||||
fn edit_privileges_with_editor(
|
||||
|
||||
@@ -18,12 +18,12 @@ use crate::{
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
database_privileges::DatabasePrivilegeRow,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, GetDatabasesPrivilegeDataError, MySQLDatabase, Request,
|
||||
Response, create_client_to_server_message_stream,
|
||||
},
|
||||
},
|
||||
server::sql::database_privilege_operations::DatabasePrivilegeRow,
|
||||
};
|
||||
|
||||
const HELP_DB_PERM: &str = r#"
|
||||
|
||||
@@ -1,767 +1,9 @@
|
||||
use anyhow::{Context, anyhow};
|
||||
use itertools::Itertools;
|
||||
use prettytable::Table;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
cmp::max,
|
||||
collections::{BTreeSet, HashMap},
|
||||
};
|
||||
|
||||
use super::{
|
||||
common::{rev_yn, yn},
|
||||
protocol::{MySQLDatabase, MySQLUser},
|
||||
};
|
||||
use crate::server::sql::database_privilege_operations::{
|
||||
DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow,
|
||||
};
|
||||
|
||||
pub fn db_priv_field_human_readable_name(name: &str) -> String {
|
||||
match name {
|
||||
"Db" => "Database".to_owned(),
|
||||
"User" => "User".to_owned(),
|
||||
"select_priv" => "Select".to_owned(),
|
||||
"insert_priv" => "Insert".to_owned(),
|
||||
"update_priv" => "Update".to_owned(),
|
||||
"delete_priv" => "Delete".to_owned(),
|
||||
"create_priv" => "Create".to_owned(),
|
||||
"drop_priv" => "Drop".to_owned(),
|
||||
"alter_priv" => "Alter".to_owned(),
|
||||
"index_priv" => "Index".to_owned(),
|
||||
"create_tmp_table_priv" => "Temp".to_owned(),
|
||||
"lock_tables_priv" => "Lock".to_owned(),
|
||||
"references_priv" => "References".to_owned(),
|
||||
_ => format!("Unknown({})", name),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn diff(row1: &DatabasePrivilegeRow, row2: &DatabasePrivilegeRow) -> DatabasePrivilegeRowDiff {
|
||||
debug_assert!(row1.db == row2.db && row1.user == row2.user);
|
||||
|
||||
DatabasePrivilegeRowDiff {
|
||||
db: row1.db.to_owned(),
|
||||
user: row1.user.to_owned(),
|
||||
diff: DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.skip(2)
|
||||
.filter_map(|field| {
|
||||
DatabasePrivilegeChange::new(
|
||||
row1.get_privilege_by_name(field),
|
||||
row2.get_privilege_by_name(field),
|
||||
field,
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/*************************/
|
||||
/* CLI INTERFACE PARSING */
|
||||
/*************************/
|
||||
|
||||
/// See documentation for [`DatabaseCommand::EditDbPrivs`].
|
||||
pub fn parse_privilege_table_cli_arg(arg: &str) -> anyhow::Result<DatabasePrivilegeRow> {
|
||||
let parts: Vec<&str> = arg.split(':').collect();
|
||||
if parts.len() != 3 {
|
||||
anyhow::bail!("Invalid argument format. See `edit-db-privs --help` for more information.");
|
||||
}
|
||||
|
||||
if parts[0].is_empty() {
|
||||
anyhow::bail!("Database name cannot be empty.");
|
||||
}
|
||||
|
||||
if parts[1].is_empty() {
|
||||
anyhow::bail!("Username cannot be empty.");
|
||||
}
|
||||
|
||||
let db = parts[0].into();
|
||||
let user = parts[1].into();
|
||||
let privs = parts[2].to_string();
|
||||
|
||||
let mut result = DatabasePrivilegeRow {
|
||||
db,
|
||||
user,
|
||||
select_priv: false,
|
||||
insert_priv: false,
|
||||
update_priv: false,
|
||||
delete_priv: false,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
};
|
||||
|
||||
for char in privs.chars() {
|
||||
match char {
|
||||
's' => result.select_priv = true,
|
||||
'i' => result.insert_priv = true,
|
||||
'u' => result.update_priv = true,
|
||||
'd' => result.delete_priv = true,
|
||||
'c' => result.create_priv = true,
|
||||
'D' => result.drop_priv = true,
|
||||
'a' => result.alter_priv = true,
|
||||
'I' => result.index_priv = true,
|
||||
't' => result.create_tmp_table_priv = true,
|
||||
'l' => result.lock_tables_priv = true,
|
||||
'r' => result.references_priv = true,
|
||||
'A' => {
|
||||
result.select_priv = true;
|
||||
result.insert_priv = true;
|
||||
result.update_priv = true;
|
||||
result.delete_priv = true;
|
||||
result.create_priv = true;
|
||||
result.drop_priv = true;
|
||||
result.alter_priv = true;
|
||||
result.index_priv = true;
|
||||
result.create_tmp_table_priv = true;
|
||||
result.lock_tables_priv = true;
|
||||
result.references_priv = true;
|
||||
}
|
||||
_ => anyhow::bail!("Invalid privilege character: {}", char),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/**********************************/
|
||||
/* EDITOR CONTENT DISPLAY/DISPLAY */
|
||||
/**********************************/
|
||||
|
||||
/// Generates a single row of the privileges table for the editor.
|
||||
pub fn format_privileges_line_for_editor(
|
||||
privs: &DatabasePrivilegeRow,
|
||||
username_len: usize,
|
||||
database_name_len: usize,
|
||||
) -> String {
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.map(|field| match field {
|
||||
"Db" => format!("{:width$}", privs.db, width = database_name_len),
|
||||
"User" => format!("{:width$}", privs.user, width = username_len),
|
||||
privilege => format!(
|
||||
"{:width$}",
|
||||
yn(privs.get_privilege_by_name(privilege)),
|
||||
width = db_priv_field_human_readable_name(privilege).len()
|
||||
),
|
||||
})
|
||||
.join(" ")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
const EDITOR_COMMENT: &str = r#"
|
||||
# Welcome to the privilege editor.
|
||||
# Each line defines what privileges a single user has on a single database.
|
||||
# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.
|
||||
# If the user should have a certain privilege, write 'Y', otherwise write 'N'.
|
||||
#
|
||||
# Lines starting with '#' are comments and will be ignored.
|
||||
"#;
|
||||
|
||||
/// Generates the content for the privilege editor.
|
||||
///
|
||||
/// The unix user is used in case there are no privileges to edit,
|
||||
/// so that the user can see an example line based on their username.
|
||||
pub fn generate_editor_content_from_privilege_data(
|
||||
privilege_data: &[DatabasePrivilegeRow],
|
||||
unix_user: &str,
|
||||
database_name: Option<&MySQLDatabase>,
|
||||
) -> String {
|
||||
let example_user = format!("{}_user", unix_user);
|
||||
let example_db = database_name
|
||||
.unwrap_or(&format!("{}_db", unix_user).into())
|
||||
.to_string();
|
||||
|
||||
// NOTE: `.max()`` fails when the iterator is empty.
|
||||
// In this case, we know that the only fields in the
|
||||
// editor will be the example user and example db name.
|
||||
// Hence, it's put as the fallback value, despite not really
|
||||
// being a "fallback" in the normal sense.
|
||||
let longest_username = max(
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|p| p.user.len())
|
||||
.max()
|
||||
.unwrap_or(example_user.len()),
|
||||
"User".len(),
|
||||
);
|
||||
|
||||
let longest_database_name = max(
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|p| p.db.len())
|
||||
.max()
|
||||
.unwrap_or(example_db.len()),
|
||||
"Database".len(),
|
||||
);
|
||||
|
||||
let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.map(db_priv_field_human_readable_name)
|
||||
.collect();
|
||||
|
||||
// Pad the first two columns with spaces to align the privileges.
|
||||
header[0] = format!("{:width$}", header[0], width = longest_database_name);
|
||||
header[1] = format!("{:width$}", header[1], width = longest_username);
|
||||
|
||||
let example_line = format_privileges_line_for_editor(
|
||||
&DatabasePrivilegeRow {
|
||||
db: example_db.into(),
|
||||
user: example_user.into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
},
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
);
|
||||
|
||||
format!(
|
||||
"{}\n{}\n{}",
|
||||
EDITOR_COMMENT,
|
||||
header.join(" "),
|
||||
if privilege_data.is_empty() {
|
||||
format!("# {}", example_line)
|
||||
} else {
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|privs| {
|
||||
format_privileges_line_for_editor(
|
||||
privs,
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PrivilegeRowParseResult {
|
||||
PrivilegeRow(DatabasePrivilegeRow),
|
||||
ParserError(anyhow::Error),
|
||||
TooFewFields(usize),
|
||||
TooManyFields(usize),
|
||||
Header,
|
||||
Comment,
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_privilege_cell_from_editor(yn: &str, name: &str) -> anyhow::Result<bool> {
|
||||
rev_yn(yn)
|
||||
.ok_or_else(|| anyhow!("Expected Y or N, found {}", yn))
|
||||
.context(format!("Could not parse {} privilege", name))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn editor_row_is_header(row: &str) -> bool {
|
||||
row.split_ascii_whitespace()
|
||||
.zip(DATABASE_PRIVILEGE_FIELDS.iter())
|
||||
.map(|(field, priv_name)| (field, db_priv_field_human_readable_name(priv_name)))
|
||||
.all(|(field, header_field)| field == header_field)
|
||||
}
|
||||
|
||||
/// Parse a single row of the privileges table from the editor.
|
||||
fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
|
||||
if row.starts_with('#') || row.starts_with("//") {
|
||||
return PrivilegeRowParseResult::Comment;
|
||||
}
|
||||
|
||||
if row.trim().is_empty() {
|
||||
return PrivilegeRowParseResult::Empty;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = row.trim().split_ascii_whitespace().collect();
|
||||
|
||||
match parts.len() {
|
||||
n if (n < DATABASE_PRIVILEGE_FIELDS.len()) => {
|
||||
return PrivilegeRowParseResult::TooFewFields(n);
|
||||
}
|
||||
n if (n > DATABASE_PRIVILEGE_FIELDS.len()) => {
|
||||
return PrivilegeRowParseResult::TooManyFields(n);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if editor_row_is_header(row) {
|
||||
return PrivilegeRowParseResult::Header;
|
||||
}
|
||||
|
||||
let row = DatabasePrivilegeRow {
|
||||
db: (*parts.first().unwrap()).into(),
|
||||
user: (*parts.get(1).unwrap()).into(),
|
||||
select_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(2).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[2],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
insert_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(3).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[3],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
update_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(4).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[4],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
delete_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(5).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[5],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
create_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(6).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[6],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
drop_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(7).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[7],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
alter_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(8).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[8],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
index_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(9).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[9],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
create_tmp_table_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(10).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[10],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
lock_tables_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(11).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[11],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
references_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(12).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[12],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
};
|
||||
|
||||
PrivilegeRowParseResult::PrivilegeRow(row)
|
||||
}
|
||||
|
||||
// TODO: return better errors
|
||||
|
||||
pub fn parse_privilege_data_from_editor_content(
|
||||
content: String,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.map(parse_privilege_row_from_editor)
|
||||
.map(|result| match result {
|
||||
PrivilegeRowParseResult::PrivilegeRow(row) => Ok(Some(row)),
|
||||
PrivilegeRowParseResult::ParserError(e) => Err(e),
|
||||
PrivilegeRowParseResult::TooFewFields(n) => Err(anyhow!(
|
||||
"Too few fields in line. Expected to find {} fields, found {}",
|
||||
DATABASE_PRIVILEGE_FIELDS.len(),
|
||||
n
|
||||
)),
|
||||
PrivilegeRowParseResult::TooManyFields(n) => Err(anyhow!(
|
||||
"Too many fields in line. Expected to find {} fields, found {}",
|
||||
DATABASE_PRIVILEGE_FIELDS.len(),
|
||||
n
|
||||
)),
|
||||
PrivilegeRowParseResult::Header => Ok(None),
|
||||
PrivilegeRowParseResult::Comment => Ok(None),
|
||||
PrivilegeRowParseResult::Empty => Ok(None),
|
||||
})
|
||||
.filter_map(|result| result.transpose())
|
||||
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
|
||||
}
|
||||
|
||||
/*****************************/
|
||||
/* CALCULATE PRIVILEGE DIFFS */
|
||||
/*****************************/
|
||||
|
||||
/// This struct represents encapsulates the differences between two
|
||||
/// instances of privilege sets for a single user on a single database.
|
||||
///
|
||||
/// The `User` and `Database` are the same for both instances.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub struct DatabasePrivilegeRowDiff {
|
||||
pub db: MySQLDatabase,
|
||||
pub user: MySQLUser,
|
||||
pub diff: BTreeSet<DatabasePrivilegeChange>,
|
||||
}
|
||||
|
||||
/// This enum represents a change for a single privilege.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub enum DatabasePrivilegeChange {
|
||||
YesToNo(String),
|
||||
NoToYes(String),
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeChange {
|
||||
pub fn new(p1: bool, p2: bool, name: &str) -> Option<DatabasePrivilegeChange> {
|
||||
match (p1, p2) {
|
||||
(true, false) => Some(DatabasePrivilegeChange::YesToNo(name.to_owned())),
|
||||
(false, true) => Some(DatabasePrivilegeChange::NoToYes(name.to_owned())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum encapsulates whether a [`DatabasePrivilegeRow`] was intrduced, modified or deleted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub enum DatabasePrivilegesDiff {
|
||||
New(DatabasePrivilegeRow),
|
||||
Modified(DatabasePrivilegeRowDiff),
|
||||
Deleted(DatabasePrivilegeRow),
|
||||
}
|
||||
|
||||
impl DatabasePrivilegesDiff {
|
||||
pub fn get_database_name(&self) -> &MySQLDatabase {
|
||||
match self {
|
||||
DatabasePrivilegesDiff::New(p) => &p.db,
|
||||
DatabasePrivilegesDiff::Modified(p) => &p.db,
|
||||
DatabasePrivilegesDiff::Deleted(p) => &p.db,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_name(&self) -> &MySQLUser {
|
||||
match self {
|
||||
DatabasePrivilegesDiff::New(p) => &p.user,
|
||||
DatabasePrivilegesDiff::Modified(p) => &p.user,
|
||||
DatabasePrivilegesDiff::Deleted(p) => &p.user,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This function calculates the differences between two sets of database privileges.
|
||||
/// It returns a set of [`DatabasePrivilegesDiff`] that can be used to display or
|
||||
/// apply a set of privilege modifications to the database.
|
||||
pub fn diff_privileges(
|
||||
from: &[DatabasePrivilegeRow],
|
||||
to: &[DatabasePrivilegeRow],
|
||||
) -> BTreeSet<DatabasePrivilegesDiff> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
to.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result = BTreeSet::new();
|
||||
|
||||
for p in to {
|
||||
if let Some(old_p) = from_lookup_table.get(&(p.db.to_owned(), p.user.to_owned())) {
|
||||
let diff = diff(old_p, p);
|
||||
if !diff.diff.is_empty() {
|
||||
result.insert(DatabasePrivilegesDiff::Modified(diff));
|
||||
}
|
||||
} else {
|
||||
result.insert(DatabasePrivilegesDiff::New(p.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
for p in from {
|
||||
if !to_lookup_table.contains_key(&(p.db.to_owned(), p.user.to_owned())) {
|
||||
result.insert(DatabasePrivilegesDiff::Deleted(p.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn display_privilege_cell(diff: &DatabasePrivilegeRowDiff) -> String {
|
||||
diff.diff
|
||||
.iter()
|
||||
.map(|change| match change {
|
||||
DatabasePrivilegeChange::YesToNo(name) => {
|
||||
format!("{}: Y -> N", db_priv_field_human_readable_name(name))
|
||||
}
|
||||
DatabasePrivilegeChange::NoToYes(name) => {
|
||||
format!("{}: N -> Y", db_priv_field_human_readable_name(name))
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn display_new_privileges_list(row: &DatabasePrivilegeRow) -> String {
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.skip(2)
|
||||
.map(|field| {
|
||||
if row.get_privilege_by_name(field) {
|
||||
format!("{}: Y", db_priv_field_human_readable_name(field))
|
||||
} else {
|
||||
format!("{}: N", db_priv_field_human_readable_name(field))
|
||||
}
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
/// Displays the difference between two sets of database privileges.
|
||||
pub fn display_privilege_diffs(diffs: &BTreeSet<DatabasePrivilegesDiff>) -> String {
|
||||
let mut table = Table::new();
|
||||
table.set_titles(row!["Database", "User", "Privilege diff",]);
|
||||
for row in diffs {
|
||||
match row {
|
||||
DatabasePrivilegesDiff::New(p) => {
|
||||
table.add_row(row![
|
||||
p.db,
|
||||
p.user,
|
||||
"(Previously unprivileged)\n".to_string() + &display_new_privileges_list(p)
|
||||
]);
|
||||
}
|
||||
DatabasePrivilegesDiff::Modified(p) => {
|
||||
table.add_row(row![p.db, p.user, display_privilege_cell(p),]);
|
||||
}
|
||||
DatabasePrivilegesDiff::Deleted(p) => {
|
||||
table.add_row(row![p.db, p.user, "Removed".to_string()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.to_string()
|
||||
}
|
||||
|
||||
/*********/
|
||||
/* TESTS */
|
||||
/*********/
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_database_privilege_change_creation() {
|
||||
assert_eq!(
|
||||
DatabasePrivilegeChange::new(true, false, "test"),
|
||||
Some(DatabasePrivilegeChange::YesToNo("test".to_owned()))
|
||||
);
|
||||
assert_eq!(
|
||||
DatabasePrivilegeChange::new(false, true, "test"),
|
||||
Some(DatabasePrivilegeChange::NoToYes("test".to_owned()))
|
||||
);
|
||||
assert_eq!(DatabasePrivilegeChange::new(true, true, "test"), None);
|
||||
assert_eq!(DatabasePrivilegeChange::new(false, false, "test"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_privilege_table_cli_arg() {
|
||||
let result = parse_privilege_table_cli_arg("db:user:A");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: true,
|
||||
drop_priv: true,
|
||||
alter_priv: true,
|
||||
index_priv: true,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: true,
|
||||
references_priv: true,
|
||||
})
|
||||
);
|
||||
|
||||
let result = parse_privilege_table_cli_arg("db:user:");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: false,
|
||||
insert_priv: false,
|
||||
update_priv: false,
|
||||
delete_priv: false,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
})
|
||||
);
|
||||
|
||||
let result = parse_privilege_table_cli_arg("db:user:siud");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
})
|
||||
);
|
||||
|
||||
let result = parse_privilege_table_cli_arg("db:user:F");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_privilege_table_cli_arg("db:s");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_privilege_table_cli_arg("::");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_privilege_table_cli_arg("db::");
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = parse_privilege_table_cli_arg(":user:");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_diff_privileges() {
|
||||
let row_to_be_modified = DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: true,
|
||||
drop_priv: true,
|
||||
alter_priv: true,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: true,
|
||||
references_priv: false,
|
||||
};
|
||||
|
||||
let mut row_to_be_deleted = row_to_be_modified.to_owned();
|
||||
"user2".clone_into(&mut row_to_be_deleted.user);
|
||||
|
||||
let from = vec![row_to_be_modified.to_owned(), row_to_be_deleted.to_owned()];
|
||||
|
||||
let mut modified_row = row_to_be_modified.to_owned();
|
||||
modified_row.select_priv = false;
|
||||
modified_row.insert_priv = false;
|
||||
modified_row.index_priv = true;
|
||||
|
||||
let mut new_row = row_to_be_modified.to_owned();
|
||||
"user3".clone_into(&mut new_row.user);
|
||||
|
||||
let to = vec![modified_row.to_owned(), new_row.to_owned()];
|
||||
|
||||
let diffs = diff_privileges(&from, &to);
|
||||
|
||||
assert_eq!(
|
||||
diffs,
|
||||
BTreeSet::from_iter(vec![
|
||||
DatabasePrivilegesDiff::Deleted(row_to_be_deleted),
|
||||
DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
diff: BTreeSet::from_iter(vec![
|
||||
DatabasePrivilegeChange::YesToNo("select_priv".to_owned()),
|
||||
DatabasePrivilegeChange::YesToNo("insert_priv".to_owned()),
|
||||
DatabasePrivilegeChange::NoToYes("index_priv".to_owned()),
|
||||
]),
|
||||
}),
|
||||
DatabasePrivilegesDiff::New(new_row),
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_generated_and_parsed_editor_content_is_equal() {
|
||||
let permissions = vec![
|
||||
DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: true,
|
||||
drop_priv: true,
|
||||
alter_priv: true,
|
||||
index_priv: true,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: true,
|
||||
references_priv: true,
|
||||
},
|
||||
DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: false,
|
||||
insert_priv: false,
|
||||
update_priv: false,
|
||||
delete_priv: false,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
},
|
||||
];
|
||||
|
||||
let content = generate_editor_content_from_privilege_data(&permissions, "user", None);
|
||||
|
||||
let parsed_permissions = parse_privilege_data_from_editor_content(content).unwrap();
|
||||
|
||||
assert_eq!(permissions, parsed_permissions);
|
||||
}
|
||||
}
|
||||
mod base;
|
||||
mod cli;
|
||||
mod diff;
|
||||
mod editor;
|
||||
|
||||
pub use base::*;
|
||||
pub use cli::*;
|
||||
pub use diff::*;
|
||||
pub use editor::*;
|
||||
|
||||
103
src/core/database_privileges/base.rs
Normal file
103
src/core/database_privileges/base.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
//! This module contains some base datastructures and functionality for dealing with
|
||||
//! database privileges in MySQL.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use crate::core::protocol::{MySQLDatabase, MySQLUser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// This is the list of fields that are used to fetch the db + user + privileges
|
||||
/// from the `db` table in the database. If you need to add or remove privilege
|
||||
/// fields, this is a good place to start.
|
||||
pub const DATABASE_PRIVILEGE_FIELDS: [&str; 13] = [
|
||||
"Db",
|
||||
"User",
|
||||
"select_priv",
|
||||
"insert_priv",
|
||||
"update_priv",
|
||||
"delete_priv",
|
||||
"create_priv",
|
||||
"drop_priv",
|
||||
"alter_priv",
|
||||
"index_priv",
|
||||
"create_tmp_table_priv",
|
||||
"lock_tables_priv",
|
||||
"references_priv",
|
||||
];
|
||||
|
||||
// NOTE: ord is needed for BTreeSet to accept the type, but it
|
||||
// doesn't have any natural implementation semantics.
|
||||
|
||||
/// Representation of the set of privileges for a single user on a single database.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub struct DatabasePrivilegeRow {
|
||||
// TODO: don't store the db and user here, let the type be stored in a mapping
|
||||
pub db: MySQLDatabase,
|
||||
pub user: MySQLUser,
|
||||
pub select_priv: bool,
|
||||
pub insert_priv: bool,
|
||||
pub update_priv: bool,
|
||||
pub delete_priv: bool,
|
||||
pub create_priv: bool,
|
||||
pub drop_priv: bool,
|
||||
pub alter_priv: bool,
|
||||
pub index_priv: bool,
|
||||
pub create_tmp_table_priv: bool,
|
||||
pub lock_tables_priv: bool,
|
||||
pub references_priv: bool,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeRow {
|
||||
/// Gets the value of a privilege by its name as a &str.
|
||||
pub fn get_privilege_by_name(&self, name: &str) -> Option<bool> {
|
||||
match name {
|
||||
"select_priv" => Some(self.select_priv),
|
||||
"insert_priv" => Some(self.insert_priv),
|
||||
"update_priv" => Some(self.update_priv),
|
||||
"delete_priv" => Some(self.delete_priv),
|
||||
"create_priv" => Some(self.create_priv),
|
||||
"drop_priv" => Some(self.drop_priv),
|
||||
"alter_priv" => Some(self.alter_priv),
|
||||
"index_priv" => Some(self.index_priv),
|
||||
"create_tmp_table_priv" => Some(self.create_tmp_table_priv),
|
||||
"lock_tables_priv" => Some(self.lock_tables_priv),
|
||||
"references_priv" => Some(self.references_priv),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabasePrivilegeRow {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for field in DATABASE_PRIVILEGE_FIELDS.into_iter().skip(2) {
|
||||
if self.get_privilege_by_name(field).unwrap() {
|
||||
f.write_str(db_priv_field_human_readable_name(field).as_str())?;
|
||||
f.write_str(": Y\n")?;
|
||||
} else {
|
||||
f.write_str(db_priv_field_human_readable_name(field).as_str())?;
|
||||
f.write_str(": N\n")?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a database privilege field name to a human-readable name.
|
||||
pub fn db_priv_field_human_readable_name(name: &str) -> String {
|
||||
match name {
|
||||
"Db" => "Database".to_owned(),
|
||||
"User" => "User".to_owned(),
|
||||
"select_priv" => "Select".to_owned(),
|
||||
"insert_priv" => "Insert".to_owned(),
|
||||
"update_priv" => "Update".to_owned(),
|
||||
"delete_priv" => "Delete".to_owned(),
|
||||
"create_priv" => "Create".to_owned(),
|
||||
"drop_priv" => "Drop".to_owned(),
|
||||
"alter_priv" => "Alter".to_owned(),
|
||||
"index_priv" => "Index".to_owned(),
|
||||
"create_tmp_table_priv" => "Temp".to_owned(),
|
||||
"lock_tables_priv" => "Lock".to_owned(),
|
||||
"references_priv" => "References".to_owned(),
|
||||
_ => format!("Unknown({})", name),
|
||||
}
|
||||
}
|
||||
332
src/core/database_privileges/cli.rs
Normal file
332
src/core/database_privileges/cli.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
//! This module contains serialization and deserialization logic for
|
||||
//! database privileges related CLI commands.
|
||||
|
||||
use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff};
|
||||
use crate::core::protocol::{MySQLDatabase, MySQLUser};
|
||||
|
||||
/// This enum represents a part of a CLI argument for editing database privileges,
|
||||
/// indicating whether privileges are to be added, set, or removed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DatabasePrivilegeEditEntryType {
|
||||
Add,
|
||||
Set,
|
||||
Remove,
|
||||
}
|
||||
|
||||
/// This struct represents a single CLI argument for editing database privileges.
|
||||
///
|
||||
/// This is typically parsed from a string looking like:
|
||||
///
|
||||
/// `[database_name:]username:[+|-]privileges`
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DatabasePrivilegeEditEntry {
|
||||
pub database: Option<MySQLDatabase>,
|
||||
pub user: MySQLUser,
|
||||
pub type_: DatabasePrivilegeEditEntryType,
|
||||
pub privileges: Vec<String>,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeEditEntry {
|
||||
/// Parses a privilege edit entry from a string.
|
||||
///
|
||||
/// The expected format is:
|
||||
///
|
||||
/// `[database_name:]username:[+|-]privileges`
|
||||
///
|
||||
/// where:
|
||||
/// - database_name is optional, if omitted the entry applies to all databases
|
||||
/// - 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
|
||||
/// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly
|
||||
/// - privileges characters are: siudcDaAItlrA
|
||||
pub fn parse_from_str(arg: &str) -> anyhow::Result<DatabasePrivilegeEditEntry> {
|
||||
let parts: Vec<&str> = arg.split(':').collect();
|
||||
if parts.len() < 2 || parts.len() > 3 {
|
||||
anyhow::bail!("Invalid privilege edit entry format: {}", arg);
|
||||
}
|
||||
|
||||
let (database, user, user_privs) = if parts.len() == 3 {
|
||||
(Some(parts[0].to_string()), parts[1].to_string(), parts[2])
|
||||
} else {
|
||||
(None, parts[0].to_string(), parts[1])
|
||||
};
|
||||
|
||||
if user.is_empty() {
|
||||
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('+') {
|
||||
(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 {
|
||||
database: database.map(MySQLDatabase::from),
|
||||
user: MySQLUser::from(user),
|
||||
type_: edit_type,
|
||||
privileges,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_database_privileges_diff(
|
||||
&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;
|
||||
match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Set => {
|
||||
diff = DatabasePrivilegeRowDiff {
|
||||
db: database,
|
||||
user: self.user.clone(),
|
||||
select_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
insert_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
update_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
delete_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
create_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
drop_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
alter_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
index_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
create_tmp_table_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
references_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
};
|
||||
for priv_char in &self.privileges {
|
||||
match priv_char.as_str() {
|
||||
"s" => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"i" => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"u" => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"d" => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"c" => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"D" => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"a" => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"I" => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"t" => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"l" => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"r" => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"A" => {
|
||||
diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.create_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.index_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.references_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
|
||||
diff = DatabasePrivilegeRowDiff {
|
||||
db: database,
|
||||
user: self.user.clone(),
|
||||
select_priv: None,
|
||||
insert_priv: None,
|
||||
update_priv: None,
|
||||
delete_priv: None,
|
||||
create_priv: None,
|
||||
drop_priv: None,
|
||||
alter_priv: None,
|
||||
index_priv: None,
|
||||
create_tmp_table_priv: None,
|
||||
lock_tables_priv: None,
|
||||
references_priv: None,
|
||||
};
|
||||
let value = match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
|
||||
DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
for priv_char in &self.privileges {
|
||||
match priv_char.as_str() {
|
||||
"s" => diff.select_priv = Some(value),
|
||||
"i" => diff.insert_priv = Some(value),
|
||||
"u" => diff.update_priv = Some(value),
|
||||
"d" => diff.delete_priv = Some(value),
|
||||
"c" => diff.create_priv = Some(value),
|
||||
"D" => diff.drop_priv = Some(value),
|
||||
"a" => diff.alter_priv = Some(value),
|
||||
"I" => diff.index_priv = Some(value),
|
||||
"t" => diff.create_tmp_table_priv = Some(value),
|
||||
"l" => diff.lock_tables_priv = Some(value),
|
||||
"r" => diff.references_priv = Some(value),
|
||||
"A" => {
|
||||
diff.select_priv = Some(value);
|
||||
diff.insert_priv = Some(value);
|
||||
diff.update_priv = Some(value);
|
||||
diff.delete_priv = Some(value);
|
||||
diff.create_priv = Some(value);
|
||||
diff.drop_priv = Some(value);
|
||||
diff.alter_priv = Some(value);
|
||||
diff.index_priv = Some(value);
|
||||
diff.create_tmp_table_priv = Some(value);
|
||||
diff.lock_tables_priv = Some(value);
|
||||
diff.references_priv = Some(value);
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DatabasePrivilegeEditEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(db) = &self.database {
|
||||
write!(f, "{}:, ", db)?;
|
||||
}
|
||||
write!(f, "{}: ", self.user)?;
|
||||
match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
|
||||
DatabasePrivilegeEditEntryType::Set => {}
|
||||
DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
|
||||
}
|
||||
for priv_char in &self.privileges {
|
||||
write!(f, "{}", priv_char)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_db_user_all() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:A");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!["A".into()],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_db_user_none() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec![],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_db_user_misc() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:siud");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[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]
|
||||
fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_user_empty_string() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("::");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_db_user_empty_string() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db::");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_add_db_user_misc() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:+siud");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
type_: DatabasePrivilegeEditEntryType::Add,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_remove_db_user_misc() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:-siud");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
type_: DatabasePrivilegeEditEntryType::Remove,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
686
src/core/database_privileges/diff.rs
Normal file
686
src/core/database_privileges/diff.rs
Normal file
@@ -0,0 +1,686 @@
|
||||
//! This module contains datastructures and logic for comparing database privileges,
|
||||
//! generating, validating and reducing diffs between two sets of database privileges.
|
||||
|
||||
use super::base::{DatabasePrivilegeRow, db_priv_field_human_readable_name};
|
||||
use crate::core::protocol::{MySQLDatabase, MySQLUser};
|
||||
use prettytable::Table;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap, hash_map::Entry},
|
||||
fmt,
|
||||
};
|
||||
|
||||
/// This enum represents a change for a single privilege.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub enum DatabasePrivilegeChange {
|
||||
YesToNo,
|
||||
NoToYes,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeChange {
|
||||
pub fn new(p1: bool, p2: bool) -> Option<DatabasePrivilegeChange> {
|
||||
match (p1, p2) {
|
||||
(true, false) => Some(DatabasePrivilegeChange::YesToNo),
|
||||
(false, true) => Some(DatabasePrivilegeChange::NoToYes),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct encapsulates the before and after states of the
|
||||
/// access privileges for a single user on a single database.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Default)]
|
||||
pub struct DatabasePrivilegeRowDiff {
|
||||
// TODO: don't store the db and user here, let the type be stored in a mapping
|
||||
pub db: MySQLDatabase,
|
||||
pub user: MySQLUser,
|
||||
pub select_priv: Option<DatabasePrivilegeChange>,
|
||||
pub insert_priv: Option<DatabasePrivilegeChange>,
|
||||
pub update_priv: Option<DatabasePrivilegeChange>,
|
||||
pub delete_priv: Option<DatabasePrivilegeChange>,
|
||||
pub create_priv: Option<DatabasePrivilegeChange>,
|
||||
pub drop_priv: Option<DatabasePrivilegeChange>,
|
||||
pub alter_priv: Option<DatabasePrivilegeChange>,
|
||||
pub index_priv: Option<DatabasePrivilegeChange>,
|
||||
pub create_tmp_table_priv: Option<DatabasePrivilegeChange>,
|
||||
pub lock_tables_priv: Option<DatabasePrivilegeChange>,
|
||||
pub references_priv: Option<DatabasePrivilegeChange>,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeRowDiff {
|
||||
/// Calculates the difference between two [`DatabasePrivilegeRow`] instances.
|
||||
pub fn from_rows(
|
||||
row1: &DatabasePrivilegeRow,
|
||||
row2: &DatabasePrivilegeRow,
|
||||
) -> DatabasePrivilegeRowDiff {
|
||||
debug_assert!(row1.db == row2.db && row1.user == row2.user);
|
||||
|
||||
DatabasePrivilegeRowDiff {
|
||||
db: row1.db.to_owned(),
|
||||
user: row1.user.to_owned(),
|
||||
select_priv: DatabasePrivilegeChange::new(row1.select_priv, row2.select_priv),
|
||||
insert_priv: DatabasePrivilegeChange::new(row1.insert_priv, row2.insert_priv),
|
||||
update_priv: DatabasePrivilegeChange::new(row1.update_priv, row2.update_priv),
|
||||
delete_priv: DatabasePrivilegeChange::new(row1.delete_priv, row2.delete_priv),
|
||||
create_priv: DatabasePrivilegeChange::new(row1.create_priv, row2.create_priv),
|
||||
drop_priv: DatabasePrivilegeChange::new(row1.drop_priv, row2.drop_priv),
|
||||
alter_priv: DatabasePrivilegeChange::new(row1.alter_priv, row2.alter_priv),
|
||||
index_priv: DatabasePrivilegeChange::new(row1.index_priv, row2.index_priv),
|
||||
create_tmp_table_priv: DatabasePrivilegeChange::new(
|
||||
row1.create_tmp_table_priv,
|
||||
row2.create_tmp_table_priv,
|
||||
),
|
||||
lock_tables_priv: DatabasePrivilegeChange::new(
|
||||
row1.lock_tables_priv,
|
||||
row2.lock_tables_priv,
|
||||
),
|
||||
references_priv: DatabasePrivilegeChange::new(
|
||||
row1.references_priv,
|
||||
row2.references_priv,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if there are no changes in this diff.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.select_priv.is_none()
|
||||
&& self.insert_priv.is_none()
|
||||
&& self.update_priv.is_none()
|
||||
&& self.delete_priv.is_none()
|
||||
&& self.create_priv.is_none()
|
||||
&& self.drop_priv.is_none()
|
||||
&& self.alter_priv.is_none()
|
||||
&& self.index_priv.is_none()
|
||||
&& self.create_tmp_table_priv.is_none()
|
||||
&& self.lock_tables_priv.is_none()
|
||||
&& self.references_priv.is_none()
|
||||
}
|
||||
|
||||
/// Retrieves the privilege change for a given privilege name.
|
||||
pub fn get_privilege_change_by_name(
|
||||
&self,
|
||||
privilege_name: &str,
|
||||
) -> anyhow::Result<Option<DatabasePrivilegeChange>> {
|
||||
match privilege_name {
|
||||
"select_priv" => Ok(self.select_priv),
|
||||
"insert_priv" => Ok(self.insert_priv),
|
||||
"update_priv" => Ok(self.update_priv),
|
||||
"delete_priv" => Ok(self.delete_priv),
|
||||
"create_priv" => Ok(self.create_priv),
|
||||
"drop_priv" => Ok(self.drop_priv),
|
||||
"alter_priv" => Ok(self.alter_priv),
|
||||
"index_priv" => Ok(self.index_priv),
|
||||
"create_tmp_table_priv" => Ok(self.create_tmp_table_priv),
|
||||
"lock_tables_priv" => Ok(self.lock_tables_priv),
|
||||
"references_priv" => Ok(self.references_priv),
|
||||
_ => anyhow::bail!("Unknown privilege name: {}", privilege_name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges another diff into this one, combining them in a sequential manner.
|
||||
fn mappend(&mut self, other: &DatabasePrivilegeRowDiff) {
|
||||
debug_assert!(self.db == other.db && self.user == other.user);
|
||||
|
||||
if other.select_priv.is_some() {
|
||||
self.select_priv = other.select_priv;
|
||||
}
|
||||
if other.insert_priv.is_some() {
|
||||
self.insert_priv = other.insert_priv;
|
||||
}
|
||||
if other.update_priv.is_some() {
|
||||
self.update_priv = other.update_priv;
|
||||
}
|
||||
if other.delete_priv.is_some() {
|
||||
self.delete_priv = other.delete_priv;
|
||||
}
|
||||
if other.create_priv.is_some() {
|
||||
self.create_priv = other.create_priv;
|
||||
}
|
||||
if other.drop_priv.is_some() {
|
||||
self.drop_priv = other.drop_priv;
|
||||
}
|
||||
if other.alter_priv.is_some() {
|
||||
self.alter_priv = other.alter_priv;
|
||||
}
|
||||
if other.index_priv.is_some() {
|
||||
self.index_priv = other.index_priv;
|
||||
}
|
||||
if other.create_tmp_table_priv.is_some() {
|
||||
self.create_tmp_table_priv = other.create_tmp_table_priv;
|
||||
}
|
||||
if other.lock_tables_priv.is_some() {
|
||||
self.lock_tables_priv = other.lock_tables_priv;
|
||||
}
|
||||
if other.references_priv.is_some() {
|
||||
self.references_priv = other.references_priv;
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes any no-op changes from the diff, based on the original privilege row.
|
||||
fn remove_noops(&mut self, from: &DatabasePrivilegeRow) {
|
||||
fn new_value(
|
||||
change: &Option<DatabasePrivilegeChange>,
|
||||
from_value: bool,
|
||||
) -> Option<DatabasePrivilegeChange> {
|
||||
change.as_ref().and_then(|c| match c {
|
||||
DatabasePrivilegeChange::YesToNo if from_value => {
|
||||
Some(DatabasePrivilegeChange::YesToNo)
|
||||
}
|
||||
DatabasePrivilegeChange::NoToYes if !from_value => {
|
||||
Some(DatabasePrivilegeChange::NoToYes)
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
self.select_priv = new_value(&self.select_priv, from.select_priv);
|
||||
self.insert_priv = new_value(&self.insert_priv, from.insert_priv);
|
||||
self.update_priv = new_value(&self.update_priv, from.update_priv);
|
||||
self.delete_priv = new_value(&self.delete_priv, from.delete_priv);
|
||||
self.create_priv = new_value(&self.create_priv, from.create_priv);
|
||||
self.drop_priv = new_value(&self.drop_priv, from.drop_priv);
|
||||
self.alter_priv = new_value(&self.alter_priv, from.alter_priv);
|
||||
self.index_priv = new_value(&self.index_priv, from.index_priv);
|
||||
self.create_tmp_table_priv =
|
||||
new_value(&self.create_tmp_table_priv, from.create_tmp_table_priv);
|
||||
self.lock_tables_priv = new_value(&self.lock_tables_priv, from.lock_tables_priv);
|
||||
self.references_priv = new_value(&self.references_priv, from.references_priv);
|
||||
}
|
||||
|
||||
fn apply(&self, base: &mut DatabasePrivilegeRow) {
|
||||
fn apply_change(change: &Option<DatabasePrivilegeChange>, target: &mut bool) {
|
||||
match change {
|
||||
Some(DatabasePrivilegeChange::YesToNo) => *target = false,
|
||||
Some(DatabasePrivilegeChange::NoToYes) => *target = true,
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
||||
apply_change(&self.select_priv, &mut base.select_priv);
|
||||
apply_change(&self.insert_priv, &mut base.insert_priv);
|
||||
apply_change(&self.update_priv, &mut base.update_priv);
|
||||
apply_change(&self.delete_priv, &mut base.delete_priv);
|
||||
apply_change(&self.create_priv, &mut base.create_priv);
|
||||
apply_change(&self.drop_priv, &mut base.drop_priv);
|
||||
apply_change(&self.alter_priv, &mut base.alter_priv);
|
||||
apply_change(&self.index_priv, &mut base.index_priv);
|
||||
apply_change(&self.create_tmp_table_priv, &mut base.create_tmp_table_priv);
|
||||
apply_change(&self.lock_tables_priv, &mut base.lock_tables_priv);
|
||||
apply_change(&self.references_priv, &mut base.references_priv);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for DatabasePrivilegeRowDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn format_change(
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
change: &Option<DatabasePrivilegeChange>,
|
||||
field_name: &str,
|
||||
) -> fmt::Result {
|
||||
if let Some(change) = change {
|
||||
match change {
|
||||
DatabasePrivilegeChange::YesToNo => f.write_fmt(format_args!(
|
||||
"{}: Y -> N\n",
|
||||
db_priv_field_human_readable_name(field_name)
|
||||
)),
|
||||
DatabasePrivilegeChange::NoToYes => f.write_fmt(format_args!(
|
||||
"{}: N -> Y\n",
|
||||
db_priv_field_human_readable_name(field_name)
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
format_change(f, &self.select_priv, "select_priv")?;
|
||||
format_change(f, &self.insert_priv, "insert_priv")?;
|
||||
format_change(f, &self.update_priv, "update_priv")?;
|
||||
format_change(f, &self.delete_priv, "delete_priv")?;
|
||||
format_change(f, &self.create_priv, "create_priv")?;
|
||||
format_change(f, &self.drop_priv, "drop_priv")?;
|
||||
format_change(f, &self.alter_priv, "alter_priv")?;
|
||||
format_change(f, &self.index_priv, "index_priv")?;
|
||||
format_change(f, &self.create_tmp_table_priv, "create_tmp_table_priv")?;
|
||||
format_change(f, &self.lock_tables_priv, "lock_tables_priv")?;
|
||||
format_change(f, &self.references_priv, "references_priv")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// This enum encapsulates whether a [`DatabasePrivilegeRow`] was introduced, modified or deleted.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub enum DatabasePrivilegesDiff {
|
||||
New(DatabasePrivilegeRow),
|
||||
Modified(DatabasePrivilegeRowDiff),
|
||||
Deleted(DatabasePrivilegeRow),
|
||||
Noop { db: MySQLDatabase, user: MySQLUser },
|
||||
}
|
||||
|
||||
impl DatabasePrivilegesDiff {
|
||||
pub fn get_database_name(&self) -> &MySQLDatabase {
|
||||
match self {
|
||||
DatabasePrivilegesDiff::New(p) => &p.db,
|
||||
DatabasePrivilegesDiff::Modified(p) => &p.db,
|
||||
DatabasePrivilegesDiff::Deleted(p) => &p.db,
|
||||
DatabasePrivilegesDiff::Noop { db, .. } => db,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_user_name(&self) -> &MySQLUser {
|
||||
match self {
|
||||
DatabasePrivilegesDiff::New(p) => &p.user,
|
||||
DatabasePrivilegesDiff::Modified(p) => &p.user,
|
||||
DatabasePrivilegesDiff::Deleted(p) => &p.user,
|
||||
DatabasePrivilegesDiff::Noop { user, .. } => user,
|
||||
}
|
||||
}
|
||||
|
||||
/// Merges another [`DatabasePrivilegesDiff`] into this one, combining them in a sequential manner.
|
||||
/// For example, if this diff represents a creation and the other represents a modification,
|
||||
/// the result will be a creation with the modifications applied.
|
||||
pub fn mappend(&mut self, other: &DatabasePrivilegesDiff) -> anyhow::Result<()> {
|
||||
debug_assert!(
|
||||
self.get_database_name() == other.get_database_name()
|
||||
&& self.get_user_name() == other.get_user_name()
|
||||
);
|
||||
|
||||
if matches!(self, DatabasePrivilegesDiff::Deleted(_))
|
||||
&& (matches!(other, DatabasePrivilegesDiff::Modified(_)))
|
||||
{
|
||||
anyhow::bail!("Cannot modify a deleted database privilege row");
|
||||
}
|
||||
|
||||
if matches!(self, DatabasePrivilegesDiff::New(_))
|
||||
&& (matches!(other, DatabasePrivilegesDiff::New(_)))
|
||||
{
|
||||
anyhow::bail!("Cannot create an already existing database privilege row");
|
||||
}
|
||||
|
||||
if matches!(self, DatabasePrivilegesDiff::Modified(_))
|
||||
&& (matches!(other, DatabasePrivilegesDiff::New(_)))
|
||||
{
|
||||
anyhow::bail!("Cannot create an already existing database privilege row");
|
||||
}
|
||||
|
||||
if matches!(self, DatabasePrivilegesDiff::Noop { .. }) {
|
||||
*self = other.to_owned();
|
||||
return Ok(());
|
||||
} else if matches!(other, DatabasePrivilegesDiff::Noop { .. }) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match (&self, other) {
|
||||
(DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Modified(modified)) => {
|
||||
let inner_row = match self {
|
||||
DatabasePrivilegesDiff::New(r) => r,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
modified.apply(inner_row);
|
||||
}
|
||||
(DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Modified(modified)) => {
|
||||
let inner_diff = match self {
|
||||
DatabasePrivilegesDiff::Modified(r) => r,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
inner_diff.mappend(modified);
|
||||
|
||||
if inner_diff.is_empty() {
|
||||
let db = inner_diff.db.to_owned();
|
||||
let user = inner_diff.user.to_owned();
|
||||
*self = DatabasePrivilegesDiff::Noop { db, user };
|
||||
}
|
||||
}
|
||||
(DatabasePrivilegesDiff::Modified(_), DatabasePrivilegesDiff::Deleted(deleted)) => {
|
||||
*self = DatabasePrivilegesDiff::Deleted(deleted.to_owned());
|
||||
}
|
||||
(DatabasePrivilegesDiff::New(_), DatabasePrivilegesDiff::Deleted(_)) => {
|
||||
let db = self.get_database_name().to_owned();
|
||||
let user = self.get_user_name().to_owned();
|
||||
*self = DatabasePrivilegesDiff::Noop { db, user };
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub type DatabasePrivilegeState<'a> = &'a [DatabasePrivilegeRow];
|
||||
|
||||
/// This function calculates the differences between two sets of database privileges.
|
||||
/// It returns a set of [`DatabasePrivilegesDiff`] that can be used to display or
|
||||
/// apply a set of privilege modifications to the database.
|
||||
pub fn diff_privileges(
|
||||
from: DatabasePrivilegeState<'_>,
|
||||
to: &[DatabasePrivilegeRow],
|
||||
) -> BTreeSet<DatabasePrivilegesDiff> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
to.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result = BTreeSet::new();
|
||||
|
||||
for p in to {
|
||||
if let Some(old_p) = from_lookup_table.get(&(p.db.to_owned(), p.user.to_owned())) {
|
||||
let diff = DatabasePrivilegeRowDiff::from_rows(old_p, p);
|
||||
if !diff.is_empty() {
|
||||
result.insert(DatabasePrivilegesDiff::Modified(diff));
|
||||
}
|
||||
} else {
|
||||
result.insert(DatabasePrivilegesDiff::New(p.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
for p in from {
|
||||
if !to_lookup_table.contains_key(&(p.db.to_owned(), p.user.to_owned())) {
|
||||
result.insert(DatabasePrivilegesDiff::Deleted(p.to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Converts a set of [`DatabasePrivilegeRowDiff`] into a set of [`DatabasePrivilegesDiff`],
|
||||
/// representing either creating new privilege rows, or modifying the existing ones.
|
||||
///
|
||||
/// This is particularly useful for processing CLI arguments.
|
||||
pub fn create_or_modify_privilege_rows(
|
||||
from: DatabasePrivilegeState<'_>,
|
||||
to: &BTreeSet<DatabasePrivilegeRowDiff>,
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result = BTreeSet::new();
|
||||
|
||||
for diff in to {
|
||||
if let Some(old_p) = from_lookup_table.get(&(diff.db.to_owned(), diff.user.to_owned())) {
|
||||
let mut modified_diff = diff.to_owned();
|
||||
modified_diff.remove_noops(old_p);
|
||||
if !modified_diff.is_empty() {
|
||||
result.insert(DatabasePrivilegesDiff::Modified(modified_diff));
|
||||
}
|
||||
} else {
|
||||
let mut new_row = DatabasePrivilegeRow {
|
||||
db: diff.db.to_owned(),
|
||||
user: diff.user.to_owned(),
|
||||
select_priv: false,
|
||||
insert_priv: false,
|
||||
update_priv: false,
|
||||
delete_priv: false,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
};
|
||||
diff.apply(&mut new_row);
|
||||
result.insert(DatabasePrivilegesDiff::New(new_row));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Reduces a set of [`DatabasePrivilegesDiff`] by removing any modifications that would be no-ops.
|
||||
/// For example, if a privilege is changed from Yes to No, but it was already No, that change
|
||||
/// is removed from the diff.
|
||||
///
|
||||
/// The `from` parameter is used to determine the current state of the privileges.
|
||||
/// The `to` parameter is the set of diffs to be reduced.
|
||||
pub fn reduce_privilege_diffs(
|
||||
from: DatabasePrivilegeState<'_>,
|
||||
to: BTreeSet<DatabasePrivilegesDiff>,
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegesDiff> = from_lookup_table
|
||||
.iter()
|
||||
.map(|((db, user), _)| {
|
||||
(
|
||||
(db.to_owned(), user.to_owned()),
|
||||
DatabasePrivilegesDiff::Noop {
|
||||
db: db.to_owned(),
|
||||
user: user.to_owned(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for diff in to {
|
||||
let entry = result.entry((
|
||||
diff.get_database_name().to_owned(),
|
||||
diff.get_user_name().to_owned(),
|
||||
));
|
||||
match entry {
|
||||
Entry::Occupied(mut occupied_entry) => {
|
||||
let existing_diff = occupied_entry.get_mut();
|
||||
existing_diff.mappend(&diff)?;
|
||||
}
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
vacant_entry.insert(diff.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (key, diff) in result.iter_mut() {
|
||||
if let Some(from_row) = from_lookup_table.get(key)
|
||||
&& let DatabasePrivilegesDiff::Modified(modified_diff) = diff
|
||||
{
|
||||
modified_diff.remove_noops(from_row);
|
||||
if modified_diff.is_empty() {
|
||||
let db = modified_diff.db.to_owned();
|
||||
let user = modified_diff.user.to_owned();
|
||||
*diff = DatabasePrivilegesDiff::Noop { db, user };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.into_values()
|
||||
.filter(|diff| !matches!(diff, DatabasePrivilegesDiff::Noop { .. }))
|
||||
.collect::<BTreeSet<DatabasePrivilegesDiff>>())
|
||||
}
|
||||
|
||||
/// Renders a set of [`DatabasePrivilegesDiff`] into a human-readable formatted table.
|
||||
pub fn display_privilege_diffs(diffs: &BTreeSet<DatabasePrivilegesDiff>) -> String {
|
||||
let mut table = Table::new();
|
||||
table.set_titles(row!["Database", "User", "Privilege diff",]);
|
||||
for row in diffs {
|
||||
match row {
|
||||
DatabasePrivilegesDiff::New(p) => {
|
||||
table.add_row(row![
|
||||
p.db,
|
||||
p.user,
|
||||
"(Previously unprivileged)\n".to_string() + &p.to_string()
|
||||
]);
|
||||
}
|
||||
DatabasePrivilegesDiff::Modified(p) => {
|
||||
table.add_row(row![p.db, p.user, p.to_string(),]);
|
||||
}
|
||||
DatabasePrivilegesDiff::Deleted(p) => {
|
||||
table.add_row(row![p.db, p.user, "Removed".to_string()]);
|
||||
}
|
||||
DatabasePrivilegesDiff::Noop { db, user } => {
|
||||
table.add_row(row![db, user, "No changes".to_string()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
table.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_database_privilege_change_creation() {
|
||||
assert_eq!(
|
||||
DatabasePrivilegeChange::new(true, false),
|
||||
Some(DatabasePrivilegeChange::YesToNo),
|
||||
);
|
||||
assert_eq!(
|
||||
DatabasePrivilegeChange::new(false, true),
|
||||
Some(DatabasePrivilegeChange::NoToYes),
|
||||
);
|
||||
assert_eq!(DatabasePrivilegeChange::new(true, true), None);
|
||||
assert_eq!(DatabasePrivilegeChange::new(false, false), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_database_privilege_row_diff_from_rows() {
|
||||
let row1 = DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
|
||||
select_priv: true,
|
||||
insert_priv: false,
|
||||
update_priv: true,
|
||||
delete_priv: false,
|
||||
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
};
|
||||
let row2 = DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: false,
|
||||
delete_priv: false,
|
||||
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
};
|
||||
|
||||
let diff = DatabasePrivilegeRowDiff::from_rows(&row1, &row2);
|
||||
assert_eq!(
|
||||
diff,
|
||||
DatabasePrivilegeRowDiff {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: None,
|
||||
insert_priv: Some(DatabasePrivilegeChange::NoToYes),
|
||||
update_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
delete_priv: None,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_database_privilege_row_diff_is_empty() {
|
||||
let empty_diff = DatabasePrivilegeRowDiff {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(empty_diff.is_empty());
|
||||
|
||||
let non_empty_diff = DatabasePrivilegeRowDiff {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(!non_empty_diff.is_empty());
|
||||
}
|
||||
|
||||
// TODO: test in isolation:
|
||||
// DatabasePrivilegeRowDiff::mappend
|
||||
// DatabasePrivilegeRowDiff::remove_noops
|
||||
// DatabasePrivilegeRowDiff::apply
|
||||
//
|
||||
// DatabasePrivilegesDiff::mappend
|
||||
//
|
||||
// reduce_privilege_diffs
|
||||
|
||||
#[test]
|
||||
fn test_diff_privileges() {
|
||||
let row_to_be_modified = DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: true,
|
||||
drop_priv: true,
|
||||
alter_priv: true,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: true,
|
||||
references_priv: false,
|
||||
};
|
||||
|
||||
let mut row_to_be_deleted = row_to_be_modified.to_owned();
|
||||
"user2".clone_into(&mut row_to_be_deleted.user);
|
||||
|
||||
let from = vec![row_to_be_modified.to_owned(), row_to_be_deleted.to_owned()];
|
||||
|
||||
let mut modified_row = row_to_be_modified.to_owned();
|
||||
modified_row.select_priv = false;
|
||||
modified_row.insert_priv = false;
|
||||
modified_row.index_priv = true;
|
||||
|
||||
let mut new_row = row_to_be_modified.to_owned();
|
||||
"user3".clone_into(&mut new_row.user);
|
||||
|
||||
let to = vec![modified_row.to_owned(), new_row.to_owned()];
|
||||
|
||||
let diffs = diff_privileges(&from, &to);
|
||||
|
||||
assert_eq!(
|
||||
diffs,
|
||||
BTreeSet::from_iter(vec![
|
||||
DatabasePrivilegesDiff::Deleted(row_to_be_deleted),
|
||||
DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
insert_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
index_priv: Some(DatabasePrivilegeChange::NoToYes),
|
||||
..Default::default()
|
||||
}),
|
||||
DatabasePrivilegesDiff::New(new_row),
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
346
src/core/database_privileges/editor.rs
Normal file
346
src/core/database_privileges/editor.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
//! This module contains serialization and deserialization logic for
|
||||
//! editing database privileges in a text editor.
|
||||
|
||||
use super::base::{
|
||||
DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow, db_priv_field_human_readable_name,
|
||||
};
|
||||
use crate::core::{
|
||||
common::{rev_yn, yn},
|
||||
protocol::MySQLDatabase,
|
||||
};
|
||||
use anyhow::{Context, anyhow};
|
||||
use itertools::Itertools;
|
||||
use std::cmp::max;
|
||||
|
||||
/// Generates a single row of the privileges table for the editor.
|
||||
pub fn format_privileges_line_for_editor(
|
||||
privs: &DatabasePrivilegeRow,
|
||||
username_len: usize,
|
||||
database_name_len: usize,
|
||||
) -> String {
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.map(|field| match field {
|
||||
"Db" => format!("{:width$}", privs.db, width = database_name_len),
|
||||
"User" => format!("{:width$}", privs.user, width = username_len),
|
||||
privilege => format!(
|
||||
"{:width$}",
|
||||
yn(privs.get_privilege_by_name(privilege).unwrap()),
|
||||
width = db_priv_field_human_readable_name(privilege).len()
|
||||
),
|
||||
})
|
||||
.join(" ")
|
||||
.trim()
|
||||
.to_string()
|
||||
}
|
||||
|
||||
const EDITOR_COMMENT: &str = r#"
|
||||
# Welcome to the privilege editor.
|
||||
# Each line defines what privileges a single user has on a single database.
|
||||
# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.
|
||||
# If the user should have a certain privilege, write 'Y', otherwise write 'N'.
|
||||
#
|
||||
# Lines starting with '#' are comments and will be ignored.
|
||||
"#;
|
||||
|
||||
/// Generates the content for the privilege editor.
|
||||
///
|
||||
/// The unix user is used in case there are no privileges to edit,
|
||||
/// so that the user can see an example line based on their username.
|
||||
pub fn generate_editor_content_from_privilege_data(
|
||||
privilege_data: &[DatabasePrivilegeRow],
|
||||
unix_user: &str,
|
||||
database_name: Option<&MySQLDatabase>,
|
||||
) -> String {
|
||||
let example_user = format!("{}_user", unix_user);
|
||||
let example_db = database_name
|
||||
.unwrap_or(&format!("{}_db", unix_user).into())
|
||||
.to_string();
|
||||
|
||||
// NOTE: `.max()`` fails when the iterator is empty.
|
||||
// In this case, we know that the only fields in the
|
||||
// editor will be the example user and example db name.
|
||||
// Hence, it's put as the fallback value, despite not really
|
||||
// being a "fallback" in the normal sense.
|
||||
let longest_username = max(
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|p| p.user.len())
|
||||
.max()
|
||||
.unwrap_or(example_user.len()),
|
||||
"User".len(),
|
||||
);
|
||||
|
||||
let longest_database_name = max(
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|p| p.db.len())
|
||||
.max()
|
||||
.unwrap_or(example_db.len()),
|
||||
"Database".len(),
|
||||
);
|
||||
|
||||
let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
.map(db_priv_field_human_readable_name)
|
||||
.collect();
|
||||
|
||||
// Pad the first two columns with spaces to align the privileges.
|
||||
header[0] = format!("{:width$}", header[0], width = longest_database_name);
|
||||
header[1] = format!("{:width$}", header[1], width = longest_username);
|
||||
|
||||
let example_line = format_privileges_line_for_editor(
|
||||
&DatabasePrivilegeRow {
|
||||
db: example_db.into(),
|
||||
user: example_user.into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
},
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
);
|
||||
|
||||
format!(
|
||||
"{}\n{}\n{}",
|
||||
EDITOR_COMMENT,
|
||||
header.join(" "),
|
||||
if privilege_data.is_empty() {
|
||||
format!("# {}", example_line)
|
||||
} else {
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|privs| {
|
||||
format_privileges_line_for_editor(
|
||||
privs,
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum PrivilegeRowParseResult {
|
||||
PrivilegeRow(DatabasePrivilegeRow),
|
||||
ParserError(anyhow::Error),
|
||||
TooFewFields(usize),
|
||||
TooManyFields(usize),
|
||||
Header,
|
||||
Comment,
|
||||
Empty,
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse_privilege_cell_from_editor(yn: &str, name: &str) -> anyhow::Result<bool> {
|
||||
rev_yn(yn)
|
||||
.ok_or_else(|| anyhow!("Expected Y or N, found {}", yn))
|
||||
.context(format!("Could not parse {} privilege", name))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn editor_row_is_header(row: &str) -> bool {
|
||||
row.split_ascii_whitespace()
|
||||
.zip(DATABASE_PRIVILEGE_FIELDS.iter())
|
||||
.map(|(field, priv_name)| (field, db_priv_field_human_readable_name(priv_name)))
|
||||
.all(|(field, header_field)| field == header_field)
|
||||
}
|
||||
|
||||
/// Parse a single row of the privileges table from the editor.
|
||||
fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
|
||||
if row.starts_with('#') || row.starts_with("//") {
|
||||
return PrivilegeRowParseResult::Comment;
|
||||
}
|
||||
|
||||
if row.trim().is_empty() {
|
||||
return PrivilegeRowParseResult::Empty;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = row.trim().split_ascii_whitespace().collect();
|
||||
|
||||
match parts.len() {
|
||||
n if (n < DATABASE_PRIVILEGE_FIELDS.len()) => {
|
||||
return PrivilegeRowParseResult::TooFewFields(n);
|
||||
}
|
||||
n if (n > DATABASE_PRIVILEGE_FIELDS.len()) => {
|
||||
return PrivilegeRowParseResult::TooManyFields(n);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
if editor_row_is_header(row) {
|
||||
return PrivilegeRowParseResult::Header;
|
||||
}
|
||||
|
||||
let row = DatabasePrivilegeRow {
|
||||
db: (*parts.first().unwrap()).into(),
|
||||
user: (*parts.get(1).unwrap()).into(),
|
||||
select_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(2).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[2],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
insert_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(3).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[3],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
update_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(4).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[4],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
delete_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(5).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[5],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
create_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(6).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[6],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
drop_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(7).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[7],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
alter_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(8).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[8],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
index_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(9).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[9],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
create_tmp_table_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(10).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[10],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
lock_tables_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(11).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[11],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
references_priv: match parse_privilege_cell_from_editor(
|
||||
parts.get(12).unwrap(),
|
||||
DATABASE_PRIVILEGE_FIELDS[12],
|
||||
) {
|
||||
Ok(p) => p,
|
||||
Err(e) => return PrivilegeRowParseResult::ParserError(e),
|
||||
},
|
||||
};
|
||||
|
||||
PrivilegeRowParseResult::PrivilegeRow(row)
|
||||
}
|
||||
|
||||
// TODO: return better errors
|
||||
|
||||
pub fn parse_privilege_data_from_editor_content(
|
||||
content: String,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.map(parse_privilege_row_from_editor)
|
||||
.map(|result| match result {
|
||||
PrivilegeRowParseResult::PrivilegeRow(row) => Ok(Some(row)),
|
||||
PrivilegeRowParseResult::ParserError(e) => Err(e),
|
||||
PrivilegeRowParseResult::TooFewFields(n) => Err(anyhow!(
|
||||
"Too few fields in line. Expected to find {} fields, found {}",
|
||||
DATABASE_PRIVILEGE_FIELDS.len(),
|
||||
n
|
||||
)),
|
||||
PrivilegeRowParseResult::TooManyFields(n) => Err(anyhow!(
|
||||
"Too many fields in line. Expected to find {} fields, found {}",
|
||||
DATABASE_PRIVILEGE_FIELDS.len(),
|
||||
n
|
||||
)),
|
||||
PrivilegeRowParseResult::Header => Ok(None),
|
||||
PrivilegeRowParseResult::Comment => Ok(None),
|
||||
PrivilegeRowParseResult::Empty => Ok(None),
|
||||
})
|
||||
.filter_map(|result| result.transpose())
|
||||
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ensure_generated_and_parsed_editor_content_is_equal() {
|
||||
let permissions = vec![
|
||||
DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: true,
|
||||
insert_priv: true,
|
||||
update_priv: true,
|
||||
delete_priv: true,
|
||||
create_priv: true,
|
||||
drop_priv: true,
|
||||
alter_priv: true,
|
||||
index_priv: true,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: true,
|
||||
references_priv: true,
|
||||
},
|
||||
DatabasePrivilegeRow {
|
||||
db: "db".into(),
|
||||
user: "user".into(),
|
||||
select_priv: false,
|
||||
insert_priv: false,
|
||||
update_priv: false,
|
||||
delete_priv: false,
|
||||
create_priv: false,
|
||||
drop_priv: false,
|
||||
alter_priv: false,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: false,
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
},
|
||||
];
|
||||
|
||||
let content = generate_editor_content_from_privilege_data(&permissions, "user", None);
|
||||
|
||||
let parsed_permissions = parse_privilege_data_from_editor_content(content).unwrap();
|
||||
|
||||
assert_eq!(permissions, parsed_permissions);
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ pub fn create_client_to_server_message_stream(socket: UnixStream) -> ClientToSer
|
||||
tokio_serde::Framed::new(length_delimited, Bincode::default())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
||||
pub struct MySQLUser(String);
|
||||
|
||||
impl FromStr for MySQLUser {
|
||||
@@ -79,7 +79,7 @@ impl From<String> for MySQLUser {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default)]
|
||||
pub struct MySQLDatabase(String);
|
||||
|
||||
impl FromStr for MySQLDatabase {
|
||||
|
||||
@@ -6,11 +6,11 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
core::{common::UnixUser, database_privileges::DatabasePrivilegeRowDiff},
|
||||
server::sql::{
|
||||
database_operations::DatabaseRow, database_privilege_operations::DatabasePrivilegeRow,
|
||||
user_operations::DatabaseUser,
|
||||
core::{
|
||||
common::UnixUser,
|
||||
database_privileges::{DatabasePrivilegeRow, DatabasePrivilegeRowDiff},
|
||||
},
|
||||
server::sql::{database_operations::DatabaseRow, user_operations::DatabaseUser},
|
||||
};
|
||||
|
||||
use super::{MySQLDatabase, MySQLUser};
|
||||
|
||||
@@ -18,13 +18,15 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{MySqlConnection, mysql::MySqlRow, prelude::*};
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
common::{UnixUser, rev_yn, yn},
|
||||
database_privileges::{DatabasePrivilegeChange, DatabasePrivilegesDiff},
|
||||
database_privileges::{
|
||||
DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeChange, DatabasePrivilegeRow,
|
||||
DatabasePrivilegesDiff,
|
||||
},
|
||||
protocol::{
|
||||
DiffDoesNotApplyError, GetAllDatabasesPrivilegeData, GetAllDatabasesPrivilegeDataError,
|
||||
GetDatabasesPrivilegeData, GetDatabasesPrivilegeDataError,
|
||||
@@ -39,65 +41,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
/// This is the list of fields that are used to fetch the db + user + privileges
|
||||
/// from the `db` table in the database. If you need to add or remove privilege
|
||||
/// fields, this is a good place to start.
|
||||
pub const DATABASE_PRIVILEGE_FIELDS: [&str; 13] = [
|
||||
"Db",
|
||||
"User",
|
||||
"select_priv",
|
||||
"insert_priv",
|
||||
"update_priv",
|
||||
"delete_priv",
|
||||
"create_priv",
|
||||
"drop_priv",
|
||||
"alter_priv",
|
||||
"index_priv",
|
||||
"create_tmp_table_priv",
|
||||
"lock_tables_priv",
|
||||
"references_priv",
|
||||
];
|
||||
|
||||
// NOTE: ord is needed for BTreeSet to accept the type, but it
|
||||
// doesn't have any natural implementation semantics.
|
||||
|
||||
/// This struct represents the set of privileges for a single user on a single database.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord)]
|
||||
pub struct DatabasePrivilegeRow {
|
||||
pub db: MySQLDatabase,
|
||||
pub user: MySQLUser,
|
||||
pub select_priv: bool,
|
||||
pub insert_priv: bool,
|
||||
pub update_priv: bool,
|
||||
pub delete_priv: bool,
|
||||
pub create_priv: bool,
|
||||
pub drop_priv: bool,
|
||||
pub alter_priv: bool,
|
||||
pub index_priv: bool,
|
||||
pub create_tmp_table_priv: bool,
|
||||
pub lock_tables_priv: bool,
|
||||
pub references_priv: bool,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeRow {
|
||||
pub fn get_privilege_by_name(&self, name: &str) -> bool {
|
||||
match name {
|
||||
"select_priv" => self.select_priv,
|
||||
"insert_priv" => self.insert_priv,
|
||||
"update_priv" => self.update_priv,
|
||||
"delete_priv" => self.delete_priv,
|
||||
"create_priv" => self.create_priv,
|
||||
"drop_priv" => self.drop_priv,
|
||||
"alter_priv" => self.alter_priv,
|
||||
"index_priv" => self.index_priv,
|
||||
"create_tmp_table_priv" => self.create_tmp_table_priv,
|
||||
"lock_tables_priv" => self.lock_tables_priv,
|
||||
"references_priv" => self.references_priv,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: get by name instead of row tuple position
|
||||
|
||||
#[inline]
|
||||
@@ -304,22 +247,39 @@ async fn unsafe_apply_privilege_diff(
|
||||
.map(|_| ())
|
||||
}
|
||||
DatabasePrivilegesDiff::Modified(p) => {
|
||||
let changes = p
|
||||
.diff
|
||||
let changes = DATABASE_PRIVILEGE_FIELDS
|
||||
.iter()
|
||||
.map(|diff| match diff {
|
||||
DatabasePrivilegeChange::YesToNo(name) => {
|
||||
format!("{} = 'N'", quote_identifier(name))
|
||||
}
|
||||
DatabasePrivilegeChange::NoToYes(name) => {
|
||||
format!("{} = 'Y'", quote_identifier(name))
|
||||
}
|
||||
.skip(2) // Skip Db and User fields
|
||||
.map(|field| {
|
||||
format!(
|
||||
"{} = COALESCE(?, {})",
|
||||
quote_identifier(field),
|
||||
quote_identifier(field)
|
||||
)
|
||||
})
|
||||
.join(",");
|
||||
|
||||
fn change_to_yn(change: DatabasePrivilegeChange) -> &'static str {
|
||||
match change {
|
||||
DatabasePrivilegeChange::YesToNo => "N",
|
||||
DatabasePrivilegeChange::NoToYes => "Y",
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
format!("UPDATE `db` SET {} WHERE `Db` = ? AND `User` = ?", changes).as_str(),
|
||||
)
|
||||
.bind(p.select_priv.map(change_to_yn))
|
||||
.bind(p.insert_priv.map(change_to_yn))
|
||||
.bind(p.update_priv.map(change_to_yn))
|
||||
.bind(p.delete_priv.map(change_to_yn))
|
||||
.bind(p.create_priv.map(change_to_yn))
|
||||
.bind(p.drop_priv.map(change_to_yn))
|
||||
.bind(p.alter_priv.map(change_to_yn))
|
||||
.bind(p.index_priv.map(change_to_yn))
|
||||
.bind(p.create_tmp_table_priv.map(change_to_yn))
|
||||
.bind(p.lock_tables_priv.map(change_to_yn))
|
||||
.bind(p.references_priv.map(change_to_yn))
|
||||
.bind(p.db.to_string())
|
||||
.bind(p.user.to_string())
|
||||
.execute(connection)
|
||||
@@ -334,6 +294,7 @@ async fn unsafe_apply_privilege_diff(
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
DatabasePrivilegesDiff::Noop { .. } => Ok(()),
|
||||
};
|
||||
|
||||
if let Err(e) = &result {
|
||||
@@ -359,7 +320,7 @@ async fn validate_diff(
|
||||
Err(e) => return Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
|
||||
};
|
||||
|
||||
let result = match diff {
|
||||
match diff {
|
||||
DatabasePrivilegesDiff::New(_) => {
|
||||
if privilege_row.is_some() {
|
||||
Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
|
||||
@@ -383,10 +344,20 @@ async fn validate_diff(
|
||||
DatabasePrivilegesDiff::Modified(row_diff) => {
|
||||
let row = privilege_row.unwrap();
|
||||
|
||||
let error_exists = row_diff.diff.iter().any(|change| match change {
|
||||
DatabasePrivilegeChange::YesToNo(name) => !row.get_privilege_by_name(name),
|
||||
DatabasePrivilegeChange::NoToYes(name) => row.get_privilege_by_name(name),
|
||||
});
|
||||
let error_exists = DATABASE_PRIVILEGE_FIELDS
|
||||
.iter()
|
||||
.skip(2) // Skip Db and User fields
|
||||
.any(
|
||||
|field| match row_diff.get_privilege_change_by_name(field).unwrap() {
|
||||
Some(DatabasePrivilegeChange::YesToNo) => {
|
||||
!row.get_privilege_by_name(field).unwrap()
|
||||
}
|
||||
Some(DatabasePrivilegeChange::NoToYes) => {
|
||||
row.get_privilege_by_name(field).unwrap()
|
||||
}
|
||||
None => false,
|
||||
},
|
||||
);
|
||||
|
||||
if error_exists {
|
||||
Err(ModifyDatabasePrivilegesError::DiffDoesNotApply(
|
||||
@@ -408,9 +379,13 @@ async fn validate_diff(
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
result
|
||||
DatabasePrivilegesDiff::Noop { .. } => {
|
||||
log::warn!(
|
||||
"Server got sent a noop database privilege diff to validate, is the client buggy?"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the result of [`diff_privileges`] to modify privileges in the database.
|
||||
|
||||
@@ -10,6 +10,7 @@ use sqlx::prelude::*;
|
||||
use crate::{
|
||||
core::{
|
||||
common::UnixUser,
|
||||
database_privileges::DATABASE_PRIVILEGE_FIELDS,
|
||||
protocol::{
|
||||
CreateUserError, CreateUsersOutput, DropUserError, DropUsersOutput, ListAllUsersError,
|
||||
ListAllUsersOutput, ListUsersError, ListUsersOutput, LockUserError, LockUsersOutput,
|
||||
@@ -22,8 +23,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
use super::database_privilege_operations::DATABASE_PRIVILEGE_FIELDS;
|
||||
|
||||
// NOTE: this function is unsafe because it does no input validation.
|
||||
async fn unsafe_user_exists(
|
||||
db_user: &str,
|
||||
|
||||
Reference in New Issue
Block a user