edit-db-privs: display diffs and ask before commiting

This commit is contained in:
Oystein Kristoffer Tveit 2024-08-08 21:48:17 +02:00
parent 7ee60dacdc
commit e420c1f4d5
Signed by: oysteikt
GPG Key ID: 9F2F7D8250F35146
2 changed files with 262 additions and 156 deletions

View File

@ -1,6 +1,6 @@
use anyhow::Context; use anyhow::Context;
use clap::Parser; use clap::Parser;
use dialoguer::Editor; use dialoguer::{Confirm, Editor};
use prettytable::{Cell, Row, Table}; use prettytable::{Cell, Row, Table};
use sqlx::{Connection, MySqlConnection}; use sqlx::{Connection, MySqlConnection};
@ -330,7 +330,17 @@ pub async fn edit_privileges(
return Ok(CommandStatus::NoModificationsNeeded); return Ok(CommandStatus::NoModificationsNeeded);
} }
// TODO: Add confirmation prompt. println!("The following changes will be made:\n");
println!("{}", display_privilege_diffs(&diffs));
if !args.yes
&& !Confirm::new()
.with_prompt("Do you want to apply these changes?")
.default(false)
.show_default(true)
.interact()?
{
return Ok(CommandStatus::Cancelled);
}
apply_privilege_diffs(diffs, connection).await?; apply_privilege_diffs(diffs, connection).await?;

View File

@ -18,6 +18,7 @@ use std::collections::{BTreeSet, HashMap};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use indoc::indoc; use indoc::indoc;
use itertools::Itertools; use itertools::Itertools;
use prettytable::Table;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{mysql::MySqlRow, prelude::*, MySqlConnection}; use sqlx::{mysql::MySqlRow, prelude::*, MySqlConnection};
@ -85,6 +86,24 @@ pub struct DatabasePrivilegeRow {
} }
impl DatabasePrivilegeRow { impl DatabasePrivilegeRow {
pub fn empty(db: &str, user: &str) -> Self {
Self {
db: db.to_owned(),
user: 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,
}
}
pub fn get_privilege_by_name(&self, name: &str) -> bool { pub fn get_privilege_by_name(&self, name: &str) -> bool {
match name { match name {
"select_priv" => self.select_priv, "select_priv" => self.select_priv,
@ -271,163 +290,15 @@ pub fn parse_privilege_table_cli_arg(arg: &str) -> anyhow::Result<DatabasePrivil
} }
/**********************************/ /**********************************/
/* EDITOR CONTENT PARSING/DISPLAY */ /* EDITOR CONTENT DISPLAY/DISPLAY */
/**********************************/ /**********************************/
#[inline]
fn parse_privilege(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))
}
#[derive(Debug)]
enum PrivilegeRowParseResult {
PrivilegeRow(DatabasePrivilegeRow),
ParserError(anyhow::Error),
TooFewFields(usize),
TooManyFields(usize),
Header,
Comment,
Empty,
}
#[inline]
fn 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 row_is_header(row) {
return PrivilegeRowParseResult::Header;
}
let row = DatabasePrivilegeRow {
db: (*parts.first().unwrap()).to_owned(),
user: (*parts.get(1).unwrap()).to_owned(),
select_priv: match parse_privilege(parts.get(2).unwrap(), DATABASE_PRIVILEGE_FIELDS[2]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
insert_priv: match parse_privilege(parts.get(3).unwrap(), DATABASE_PRIVILEGE_FIELDS[3]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
update_priv: match parse_privilege(parts.get(4).unwrap(), DATABASE_PRIVILEGE_FIELDS[4]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
delete_priv: match parse_privilege(parts.get(5).unwrap(), DATABASE_PRIVILEGE_FIELDS[5]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
create_priv: match parse_privilege(parts.get(6).unwrap(), DATABASE_PRIVILEGE_FIELDS[6]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
drop_priv: match parse_privilege(parts.get(7).unwrap(), DATABASE_PRIVILEGE_FIELDS[7]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
alter_priv: match parse_privilege(parts.get(8).unwrap(), DATABASE_PRIVILEGE_FIELDS[8]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
index_priv: match parse_privilege(parts.get(9).unwrap(), DATABASE_PRIVILEGE_FIELDS[9]) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
create_tmp_table_priv: match parse_privilege(
parts.get(10).unwrap(),
DATABASE_PRIVILEGE_FIELDS[10],
) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
lock_tables_priv: match parse_privilege(
parts.get(11).unwrap(),
DATABASE_PRIVILEGE_FIELDS[11],
) {
Ok(p) => p,
Err(e) => return PrivilegeRowParseResult::ParserError(e),
},
references_priv: match parse_privilege(
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>>>()
}
/// Generates a single row of the privileges table for the editor. /// Generates a single row of the privileges table for the editor.
pub fn format_privileges_line( pub fn format_privileges_line_for_editor(
privs: &DatabasePrivilegeRow, privs: &DatabasePrivilegeRow,
username_len: usize, username_len: usize,
database_name_len: usize, database_name_len: usize,
) -> String { ) -> String {
// Format a privileges line by padding each value with spaces
// The first two fields are padded to the length of the longest username and database name
// The remaining fields are padded to the length of the corresponding field name
DATABASE_PRIVILEGE_FIELDS DATABASE_PRIVILEGE_FIELDS
.into_iter() .into_iter()
.map(|field| match field { .map(|field| match field {
@ -490,7 +361,7 @@ pub fn generate_editor_content_from_privilege_data(
header[0] = format!("{:width$}", header[0], width = longest_database_name); header[0] = format!("{:width$}", header[0], width = longest_database_name);
header[1] = format!("{:width$}", header[1], width = longest_username); header[1] = format!("{:width$}", header[1], width = longest_username);
let example_line = format_privileges_line( let example_line = format_privileges_line_for_editor(
&DatabasePrivilegeRow { &DatabasePrivilegeRow {
db: example_db, db: example_db,
user: example_user, user: example_user,
@ -519,12 +390,186 @@ pub fn generate_editor_content_from_privilege_data(
} else { } else {
privilege_data privilege_data
.iter() .iter()
.map(|privs| format_privileges_line(privs, longest_username, longest_database_name)) .map(|privs| {
format_privileges_line_for_editor(
privs,
longest_username,
longest_database_name,
)
})
.join("\n") .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()).to_owned(),
user: (*parts.get(1).unwrap()).to_owned(),
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 */ /* CALCULATE PRIVILEGE DIFFS */
/*****************************/ /*****************************/
@ -565,7 +610,9 @@ pub enum DatabasePrivilegesDiff {
Deleted(DatabasePrivilegeRow), Deleted(DatabasePrivilegeRow),
} }
/// T /// 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( pub fn diff_privileges(
from: &[DatabasePrivilegeRow], from: &[DatabasePrivilegeRow],
to: &[DatabasePrivilegeRow], to: &[DatabasePrivilegeRow],
@ -604,7 +651,7 @@ pub fn diff_privileges(
result result
} }
/// Uses the resulting diffs to make modifications to the database. /// Uses the result of [`diff_privileges`] to modify privileges in the database.
pub async fn apply_privilege_diffs( pub async fn apply_privilege_diffs(
diffs: BTreeSet<DatabasePrivilegesDiff>, diffs: BTreeSet<DatabasePrivilegesDiff>,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
@ -670,6 +717,55 @@ pub async fn apply_privilege_diffs(
Ok(()) Ok(())
} }
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")
}
/// 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,
"(New user)\n".to_string()
+ &display_privilege_cell(
&DatabasePrivilegeRow::empty(&p.db, &p.user).diff(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,
"(All privileges removed)\n".to_string()
+ &display_privilege_cell(
&p.diff(&DatabasePrivilegeRow::empty(&p.db, &p.user))
)
]);
}
}
}
table.to_string()
}
/*********/ /*********/
/* TESTS */ /* TESTS */
/*********/ /*********/