use anyhow::{anyhow, Context}; use itertools::Itertools; use prettytable::Table; use serde::{Deserialize, Serialize}; use std::{ cmp::max, collections::{BTreeSet, HashMap}, }; use super::common::{rev_yn, yn}; use crate::server::sql::database_privilege_operations::{ DatabasePrivilegeRow, DATABASE_PRIVILEGE_FIELDS, }; 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.clone(), user: row1.user.clone(), 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 { let parts: Vec<&str> = arg.split(':').collect(); if parts.len() != 3 { anyhow::bail!("Invalid argument format. See `edit-db-privs --help` for more information."); } let db = parts[0].to_string(); let user = parts[1].to_string(); 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<&str>, ) -> String { let example_user = format!("{}_user", unix_user); let example_db = database_name .unwrap_or(&format!("{}_db", unix_user)) .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, user: example_user, 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 { 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> { 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::>>() } /*****************************/ /* 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: String, pub user: String, pub diff: BTreeSet, } /// 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 { 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) -> &str { match self { DatabasePrivilegesDiff::New(p) => &p.db, DatabasePrivilegesDiff::Modified(p) => &p.db, DatabasePrivilegesDiff::Deleted(p) => &p.db, } } pub fn get_user_name(&self) -> &str { 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 { let from_lookup_table: HashMap<(String, String), DatabasePrivilegeRow> = HashMap::from_iter( from.iter() .cloned() .map(|p| ((p.db.clone(), p.user.clone()), p)), ); let to_lookup_table: HashMap<(String, String), DatabasePrivilegeRow> = HashMap::from_iter( to.iter() .cloned() .map(|p| ((p.db.clone(), p.user.clone()), p)), ); let mut result = BTreeSet::new(); for p in to { if let Some(old_p) = from_lookup_table.get(&(p.db.clone(), p.user.clone())) { let diff = diff(old_p, p); if !diff.diff.is_empty() { result.insert(DatabasePrivilegesDiff::Modified(diff)); } } else { result.insert(DatabasePrivilegesDiff::New(p.clone())); } } for p in from { if !to_lookup_table.contains_key(&(p.db.clone(), p.user.clone())) { result.insert(DatabasePrivilegesDiff::Deleted(p.clone())); } } 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) -> 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_diff_privileges() { let row_to_be_modified = DatabasePrivilegeRow { db: "db".to_owned(), user: "user".to_owned(), 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.clone(); "user2".clone_into(&mut row_to_be_deleted.user); let from = vec![row_to_be_modified.clone(), row_to_be_deleted.clone()]; let mut modified_row = row_to_be_modified.clone(); modified_row.select_priv = false; modified_row.insert_priv = false; modified_row.index_priv = true; let mut new_row = row_to_be_modified.clone(); "user3".clone_into(&mut new_row.user); let to = vec![modified_row.clone(), new_row.clone()]; let diffs = diff_privileges(&from, &to); assert_eq!( diffs, BTreeSet::from_iter(vec![ DatabasePrivilegesDiff::Deleted(row_to_be_deleted), DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff { db: "db".to_owned(), user: "user".to_owned(), 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".to_owned(), user: "user".to_owned(), 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: "db2".to_owned(), user: "user2".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, }, ]; 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); } }