Compare commits

..

2 Commits

3 changed files with 79 additions and 52 deletions

View File

@ -277,7 +277,7 @@ async fn show_databases(
} }
/// See documentation for `DatabaseCommand::EditPerm`. /// See documentation for `DatabaseCommand::EditPerm`.
fn parse_permission_table_cli_arg(arg: &str) -> anyhow::Result<DatabasePrivileges> { fn parse_permission_table_cli_arg(arg: &str) -> anyhow::Result<DatabasePrivilegeRow> {
let parts: Vec<&str> = arg.split(':').collect(); let parts: Vec<&str> = arg.split(':').collect();
if parts.len() != 3 { if parts.len() != 3 {
anyhow::bail!("Invalid argument format. See `edit-perm --help` for more information."); anyhow::bail!("Invalid argument format. See `edit-perm --help` for more information.");
@ -287,7 +287,7 @@ fn parse_permission_table_cli_arg(arg: &str) -> anyhow::Result<DatabasePrivilege
let user = parts[1].to_string(); let user = parts[1].to_string();
let privs = parts[2].to_string(); let privs = parts[2].to_string();
let mut result = DatabasePrivileges { let mut result = DatabasePrivilegeRow {
db, db,
user, user,
select_priv: false, select_priv: false,
@ -344,7 +344,7 @@ fn parse_permission(yn: &str) -> anyhow::Result<bool> {
} }
} }
fn parse_permission_data_from_editor(content: String) -> anyhow::Result<Vec<DatabasePrivileges>> { fn parse_permission_data_from_editor(content: String) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
content content
.trim() .trim()
.split('\n') .split('\n')
@ -357,7 +357,7 @@ fn parse_permission_data_from_editor(content: String) -> anyhow::Result<Vec<Data
anyhow::bail!("") anyhow::bail!("")
} }
Ok(DatabasePrivileges { Ok(DatabasePrivilegeRow {
db: (*line_parts.first().unwrap()).to_owned(), db: (*line_parts.first().unwrap()).to_owned(),
user: (*line_parts.get(1).unwrap()).to_owned(), user: (*line_parts.get(1).unwrap()).to_owned(),
select_priv: parse_permission(line_parts.get(2).unwrap()) select_priv: parse_permission(line_parts.get(2).unwrap())
@ -384,11 +384,11 @@ fn parse_permission_data_from_editor(content: String) -> anyhow::Result<Vec<Data
.context("Could not parse REFERENCES privilege")?, .context("Could not parse REFERENCES privilege")?,
}) })
}) })
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>() .collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
} }
fn format_privileges_line( fn format_privileges_line(
privs: &DatabasePrivileges, privs: &DatabasePrivilegeRow,
username_len: usize, username_len: usize,
database_name_len: usize, database_name_len: usize,
) -> String { ) -> String {
@ -430,7 +430,7 @@ pub async fn edit_permissions(
parse_permission_table_cli_arg(&format!("{}:{}", name, &perm)) parse_permission_table_cli_arg(&format!("{}:{}", name, &perm))
.context(format!("Failed parsing database permissions: `{}`", &perm)) .context(format!("Failed parsing database permissions: `{}`", &perm))
}) })
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()? .collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
} else { } else {
args.perm args.perm
.iter() .iter()
@ -438,7 +438,7 @@ pub async fn edit_permissions(
parse_permission_table_cli_arg(perm) parse_permission_table_cli_arg(perm)
.context(format!("Failed parsing database permissions: `{}`", &perm)) .context(format!("Failed parsing database permissions: `{}`", &perm))
}) })
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()? .collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
} }
} else { } else {
let comment = indoc! {r#" let comment = indoc! {r#"
@ -476,7 +476,7 @@ pub async fn edit_permissions(
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(
&DatabasePrivileges { &DatabasePrivilegeRow {
db: example_db, db: example_db,
user: example_user, user: example_user,
select_priv: true, select_priv: true,

View File

@ -1,3 +1,18 @@
//! Database privilege operations
//!
//! This module contains functions for querying, modifying,
//! displaying and comparing database privileges.
//!
//! A lot of the complexity comes from two core components:
//!
//! - The permission editor that needs to be able to print
//! an editable table of privileges and reparse the content
//! after the user has made manual changes.
//!
//! - The comparison functionality that tells the user what
//! changes will be made when applying a set of changes
//! to the list of database privileges.
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::Context; use anyhow::Context;
@ -48,8 +63,9 @@ pub fn db_priv_field_human_readable_name(name: &str) -> String {
} }
} }
/// This struct represents the set of privileges for a single user on a single database.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatabasePrivileges { pub struct DatabasePrivilegeRow {
pub db: String, pub db: String,
pub user: String, pub user: String,
pub select_priv: bool, pub select_priv: bool,
@ -65,7 +81,7 @@ pub struct DatabasePrivileges {
pub references_priv: bool, pub references_priv: bool,
} }
impl DatabasePrivileges { impl DatabasePrivilegeRow {
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,
@ -82,17 +98,18 @@ impl DatabasePrivileges {
_ => false, _ => false,
} }
} }
pub fn diff(&self, other: &DatabasePrivileges) -> DatabasePrivilegeDiffList {
pub fn diff(&self, other: &DatabasePrivilegeRow) -> DatabasePrivilegeRowDiff {
debug_assert!(self.db == other.db && self.user == other.user); debug_assert!(self.db == other.db && self.user == other.user);
DatabasePrivilegeDiffList { DatabasePrivilegeRowDiff {
db: self.db.clone(), db: self.db.clone(),
user: self.user.clone(), user: self.user.clone(),
diff: DATABASE_PRIVILEGE_FIELDS diff: DATABASE_PRIVILEGE_FIELDS
.into_iter() .into_iter()
.skip(2) .skip(2)
.filter_map(|field| { .filter_map(|field| {
diff_single_priv( DatabasePrivilegeChange::new(
self.get_privilege_by_name(field), self.get_privilege_by_name(field),
other.get_privilege_by_name(field), other.get_privilege_by_name(field),
field, field,
@ -103,7 +120,7 @@ impl DatabasePrivileges {
} }
} }
impl FromRow<'_, MySqlRow> for DatabasePrivileges { impl FromRow<'_, MySqlRow> for DatabasePrivilegeRow {
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> { fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
Ok(Self { Ok(Self {
db: row.try_get("db")?, db: row.try_get("db")?,
@ -126,11 +143,11 @@ impl FromRow<'_, MySqlRow> for DatabasePrivileges {
pub async fn get_database_privileges( pub async fn get_database_privileges(
database_name: &str, database_name: &str,
conn: &mut MySqlConnection, conn: &mut MySqlConnection,
) -> anyhow::Result<Vec<DatabasePrivileges>> { ) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
let unix_user = get_current_unix_user()?; let unix_user = get_current_unix_user()?;
validate_database_name(database_name, &unix_user)?; validate_database_name(database_name, &unix_user)?;
let result = sqlx::query_as::<_, DatabasePrivileges>(&format!( let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
"SELECT {} FROM `db` WHERE `db` = ?", "SELECT {} FROM `db` WHERE `db` = ?",
DATABASE_PRIVILEGE_FIELDS DATABASE_PRIVILEGE_FIELDS
.iter() .iter()
@ -147,10 +164,10 @@ pub async fn get_database_privileges(
pub async fn get_all_database_privileges( pub async fn get_all_database_privileges(
conn: &mut MySqlConnection, conn: &mut MySqlConnection,
) -> anyhow::Result<Vec<DatabasePrivileges>> { ) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
let unix_user = get_current_unix_user()?; let unix_user = get_current_unix_user()?;
let result = sqlx::query_as::<_, DatabasePrivileges>(&format!( let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
indoc! {r#" indoc! {r#"
SELECT {} FROM `db` WHERE `db` IN SELECT {} FROM `db` WHERE `db` IN
(SELECT DISTINCT `SCHEMA_NAME` AS `database` (SELECT DISTINCT `SCHEMA_NAME` AS `database`
@ -171,45 +188,56 @@ pub async fn get_all_database_privileges(
Ok(result) Ok(result)
} }
/*******************/
/* 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, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DatabasePrivilegeDiffList { pub struct DatabasePrivilegeRowDiff {
pub db: String, pub db: String,
pub user: String, pub user: String,
pub diff: Vec<DatabasePrivilegeDiff>, pub diff: Vec<DatabasePrivilegeChange>,
} }
/// This enum represents a change in a single privilege.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DatabasePrivilegeDiff { pub enum DatabasePrivilegeChange {
YesToNo(String), YesToNo(String),
NoToYes(String), NoToYes(String),
} }
fn diff_single_priv(p1: bool, p2: bool, name: &str) -> Option<DatabasePrivilegeDiff> { impl DatabasePrivilegeChange {
pub fn new(p1: bool, p2: bool, name: &str) -> Option<DatabasePrivilegeChange> {
match (p1, p2) { match (p1, p2) {
(true, false) => Some(DatabasePrivilegeDiff::YesToNo(name.to_owned())), (true, false) => Some(DatabasePrivilegeChange::YesToNo(name.to_owned())),
(false, true) => Some(DatabasePrivilegeDiff::NoToYes(name.to_owned())), (false, true) => Some(DatabasePrivilegeChange::NoToYes(name.to_owned())),
_ => None, _ => None,
} }
}
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DatabasePrivilegesDiff { pub enum DatabasePrivilegesDiff {
New(DatabasePrivileges), New(DatabasePrivilegeRow),
Modified(DatabasePrivilegeDiffList), Modified(DatabasePrivilegeRowDiff),
Deleted(DatabasePrivileges), Deleted(DatabasePrivilegeRow),
} }
pub async fn diff_permissions( pub async fn diff_permissions(
from: Vec<DatabasePrivileges>, from: Vec<DatabasePrivilegeRow>,
to: &[DatabasePrivileges], to: &[DatabasePrivilegeRow],
) -> Vec<DatabasePrivilegesDiff> { ) -> Vec<DatabasePrivilegesDiff> {
let from_lookup_table: HashMap<(String, String), DatabasePrivileges> = HashMap::from_iter( let from_lookup_table: HashMap<(String, String), DatabasePrivilegeRow> = HashMap::from_iter(
from.iter() from.iter()
.cloned() .cloned()
.map(|p| ((p.db.clone(), p.user.clone()), p)), .map(|p| ((p.db.clone(), p.user.clone()), p)),
); );
let to_lookup_table: HashMap<(String, String), DatabasePrivileges> = HashMap::from_iter( let to_lookup_table: HashMap<(String, String), DatabasePrivilegeRow> = HashMap::from_iter(
to.iter() to.iter()
.cloned() .cloned()
.map(|p| ((p.db.clone(), p.user.clone()), p)), .map(|p| ((p.db.clone(), p.user.clone()), p)),
@ -277,8 +305,8 @@ pub async fn apply_permission_diffs(
.diff .diff
.iter() .iter()
.map(|diff| match diff { .map(|diff| match diff {
DatabasePrivilegeDiff::YesToNo(name) => format!("`{}` = 'N'", name), DatabasePrivilegeChange::YesToNo(name) => format!("`{}` = 'N'", name),
DatabasePrivilegeDiff::NoToYes(name) => format!("`{}` = 'Y'", name), DatabasePrivilegeChange::NoToYes(name) => format!("`{}` = 'Y'", name),
}) })
.join(","); .join(",");
@ -307,22 +335,22 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_diff_single_priv() { fn test_database_privilege_change_creation() {
assert_eq!( assert_eq!(
diff_single_priv(true, false, "test"), DatabasePrivilegeChange::new(true, false, "test"),
Some(DatabasePrivilegeDiff::YesToNo("test".to_owned())) Some(DatabasePrivilegeChange::YesToNo("test".to_owned()))
); );
assert_eq!( assert_eq!(
diff_single_priv(false, true, "test"), DatabasePrivilegeChange::new(false, true, "test"),
Some(DatabasePrivilegeDiff::NoToYes("test".to_owned())) Some(DatabasePrivilegeChange::NoToYes("test".to_owned()))
); );
assert_eq!(diff_single_priv(true, true, "test"), None); assert_eq!(DatabasePrivilegeChange::new(true, true, "test"), None);
assert_eq!(diff_single_priv(false, false, "test"), None); assert_eq!(DatabasePrivilegeChange::new(false, false, "test"), None);
} }
#[tokio::test] #[tokio::test]
async fn test_diff_permissions() { async fn test_diff_permissions() {
let from = vec![DatabasePrivileges { let from = vec![DatabasePrivilegeRow {
db: "db".to_owned(), db: "db".to_owned(),
user: "user".to_owned(), user: "user".to_owned(),
select_priv: true, select_priv: true,
@ -338,7 +366,7 @@ mod tests {
references_priv: true, references_priv: true,
}]; }];
let to = vec![DatabasePrivileges { let to = vec![DatabasePrivilegeRow {
db: "db".to_owned(), db: "db".to_owned(),
user: "user".to_owned(), user: "user".to_owned(),
select_priv: false, select_priv: false,
@ -358,13 +386,11 @@ mod tests {
assert_eq!( assert_eq!(
diffs, diffs,
vec![DatabasePrivilegesDiff::Modified( vec![DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
DatabasePrivilegeDiffList {
db: "db".to_owned(), db: "db".to_owned(),
user: "user".to_owned(), user: "user".to_owned(),
diff: vec![DatabasePrivilegeDiff::YesToNo("select_priv".to_owned())], diff: vec![DatabasePrivilegeChange::YesToNo("select_priv".to_owned())],
} })]
)]
); );
assert!(matches!(&diffs[0], DatabasePrivilegesDiff::Modified(_))); assert!(matches!(&diffs[0], DatabasePrivilegesDiff::Modified(_)));

View File

@ -100,6 +100,7 @@ pub struct DatabaseUser {
#[sqlx(rename = "User")] #[sqlx(rename = "User")]
pub user: String, pub user: String,
#[allow(dead_code)]
#[serde(skip)] #[serde(skip)]
#[sqlx(rename = "Host")] #[sqlx(rename = "Host")]
pub host: String, pub host: String,