Refactor privilege handling
All checks were successful
Build / check (push) Successful in 2m41s
Build / build (push) Successful in 3m5s
Build / docs (push) Successful in 5m37s

This commit is contained in:
2025-11-14 00:49:29 +09:00
parent 7760b001d8
commit 03a761a0ff
12 changed files with 1567 additions and 886 deletions

View File

@@ -1,7 +1,7 @@
[package]
name = "mysqladm-rs"
version = "0.1.0"
edition = "2021"
edition = "2024"
license = "BSD3"
authors = [
"oysteikt@pvv.ntnu.no",

View File

@@ -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(

View File

@@ -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#"

View File

@@ -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::*;

View 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),
}
}

View 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()],
}),
);
}
}

View 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),
])
);
}
}

View 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);
}
}

View File

@@ -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 {

View File

@@ -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};

View File

@@ -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.

View File

@@ -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,