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`.
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();
if parts.len() != 3 {
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 privs = parts[2].to_string();
let mut result = DatabasePrivileges {
let mut result = DatabasePrivilegeRow {
db,
user,
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
.trim()
.split('\n')
@ -357,7 +357,7 @@ fn parse_permission_data_from_editor(content: String) -> anyhow::Result<Vec<Data
anyhow::bail!("")
}
Ok(DatabasePrivileges {
Ok(DatabasePrivilegeRow {
db: (*line_parts.first().unwrap()).to_owned(),
user: (*line_parts.get(1).unwrap()).to_owned(),
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")?,
})
})
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
}
fn format_privileges_line(
privs: &DatabasePrivileges,
privs: &DatabasePrivilegeRow,
username_len: usize,
database_name_len: usize,
) -> String {
@ -430,7 +430,7 @@ pub async fn edit_permissions(
parse_permission_table_cli_arg(&format!("{}:{}", name, &perm))
.context(format!("Failed parsing database permissions: `{}`", &perm))
})
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()?
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
} else {
args.perm
.iter()
@ -438,7 +438,7 @@ pub async fn edit_permissions(
parse_permission_table_cli_arg(perm)
.context(format!("Failed parsing database permissions: `{}`", &perm))
})
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()?
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()?
}
} else {
let comment = indoc! {r#"
@ -476,7 +476,7 @@ pub async fn edit_permissions(
header[1] = format!("{:width$}", header[1], width = longest_username);
let example_line = format_privileges_line(
&DatabasePrivileges {
&DatabasePrivilegeRow {
db: example_db,
user: example_user,
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 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)]
pub struct DatabasePrivileges {
pub struct DatabasePrivilegeRow {
pub db: String,
pub user: String,
pub select_priv: bool,
@ -65,7 +81,7 @@ pub struct DatabasePrivileges {
pub references_priv: bool,
}
impl DatabasePrivileges {
impl DatabasePrivilegeRow {
pub fn get_privilege_by_name(&self, name: &str) -> bool {
match name {
"select_priv" => self.select_priv,
@ -82,17 +98,18 @@ impl DatabasePrivileges {
_ => 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);
DatabasePrivilegeDiffList {
DatabasePrivilegeRowDiff {
db: self.db.clone(),
user: self.user.clone(),
diff: DATABASE_PRIVILEGE_FIELDS
.into_iter()
.skip(2)
.filter_map(|field| {
diff_single_priv(
DatabasePrivilegeChange::new(
self.get_privilege_by_name(field),
other.get_privilege_by_name(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> {
Ok(Self {
db: row.try_get("db")?,
@ -126,11 +143,11 @@ impl FromRow<'_, MySqlRow> for DatabasePrivileges {
pub async fn get_database_privileges(
database_name: &str,
conn: &mut MySqlConnection,
) -> anyhow::Result<Vec<DatabasePrivileges>> {
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
let unix_user = get_current_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` = ?",
DATABASE_PRIVILEGE_FIELDS
.iter()
@ -147,10 +164,10 @@ pub async fn get_database_privileges(
pub async fn get_all_database_privileges(
conn: &mut MySqlConnection,
) -> anyhow::Result<Vec<DatabasePrivileges>> {
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
let unix_user = get_current_unix_user()?;
let result = sqlx::query_as::<_, DatabasePrivileges>(&format!(
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
indoc! {r#"
SELECT {} FROM `db` WHERE `db` IN
(SELECT DISTINCT `SCHEMA_NAME` AS `database`
@ -171,45 +188,56 @@ pub async fn get_all_database_privileges(
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)]
pub struct DatabasePrivilegeDiffList {
pub struct DatabasePrivilegeRowDiff {
pub db: 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)]
pub enum DatabasePrivilegeDiff {
pub enum DatabasePrivilegeChange {
YesToNo(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) {
(true, false) => Some(DatabasePrivilegeDiff::YesToNo(name.to_owned())),
(false, true) => Some(DatabasePrivilegeDiff::NoToYes(name.to_owned())),
(true, false) => Some(DatabasePrivilegeChange::YesToNo(name.to_owned())),
(false, true) => Some(DatabasePrivilegeChange::NoToYes(name.to_owned())),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum DatabasePrivilegesDiff {
New(DatabasePrivileges),
Modified(DatabasePrivilegeDiffList),
Deleted(DatabasePrivileges),
New(DatabasePrivilegeRow),
Modified(DatabasePrivilegeRowDiff),
Deleted(DatabasePrivilegeRow),
}
pub async fn diff_permissions(
from: Vec<DatabasePrivileges>,
to: &[DatabasePrivileges],
from: Vec<DatabasePrivilegeRow>,
to: &[DatabasePrivilegeRow],
) -> 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()
.cloned()
.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()
.cloned()
.map(|p| ((p.db.clone(), p.user.clone()), p)),
@ -277,8 +305,8 @@ pub async fn apply_permission_diffs(
.diff
.iter()
.map(|diff| match diff {
DatabasePrivilegeDiff::YesToNo(name) => format!("`{}` = 'N'", name),
DatabasePrivilegeDiff::NoToYes(name) => format!("`{}` = 'Y'", name),
DatabasePrivilegeChange::YesToNo(name) => format!("`{}` = 'N'", name),
DatabasePrivilegeChange::NoToYes(name) => format!("`{}` = 'Y'", name),
})
.join(",");
@ -307,22 +335,22 @@ mod tests {
use super::*;
#[test]
fn test_diff_single_priv() {
fn test_database_privilege_change_creation() {
assert_eq!(
diff_single_priv(true, false, "test"),
Some(DatabasePrivilegeDiff::YesToNo("test".to_owned()))
DatabasePrivilegeChange::new(true, false, "test"),
Some(DatabasePrivilegeChange::YesToNo("test".to_owned()))
);
assert_eq!(
diff_single_priv(false, true, "test"),
Some(DatabasePrivilegeDiff::NoToYes("test".to_owned()))
DatabasePrivilegeChange::new(false, true, "test"),
Some(DatabasePrivilegeChange::NoToYes("test".to_owned()))
);
assert_eq!(diff_single_priv(true, true, "test"), None);
assert_eq!(diff_single_priv(false, false, "test"), None);
assert_eq!(DatabasePrivilegeChange::new(true, true, "test"), None);
assert_eq!(DatabasePrivilegeChange::new(false, false, "test"), None);
}
#[tokio::test]
async fn test_diff_permissions() {
let from = vec![DatabasePrivileges {
let from = vec![DatabasePrivilegeRow {
db: "db".to_owned(),
user: "user".to_owned(),
select_priv: true,
@ -338,7 +366,7 @@ mod tests {
references_priv: true,
}];
let to = vec![DatabasePrivileges {
let to = vec![DatabasePrivilegeRow {
db: "db".to_owned(),
user: "user".to_owned(),
select_priv: false,
@ -358,13 +386,11 @@ mod tests {
assert_eq!(
diffs,
vec![DatabasePrivilegesDiff::Modified(
DatabasePrivilegeDiffList {
vec![DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
db: "db".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(_)));

View File

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