More spring cleaning for privs, add test
This commit is contained in:
@@ -1,8 +1,6 @@
|
||||
use anyhow::{anyhow, Context};
|
||||
use anyhow::Context;
|
||||
use clap::Parser;
|
||||
use dialoguer::Editor;
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
use prettytable::{Cell, Row, Table};
|
||||
use sqlx::{Connection, MySqlConnection};
|
||||
|
||||
@@ -285,142 +283,6 @@ async fn show_database_privileges(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// See documentation for `DatabaseCommand::EditDbPrivs`.
|
||||
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.");
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fn parse_privilege(yn: &str) -> anyhow::Result<bool> {
|
||||
match yn.to_ascii_lowercase().as_str() {
|
||||
"y" => Ok(true),
|
||||
"n" => Ok(false),
|
||||
_ => Err(anyhow!("Expected Y or N, found {}", yn)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_privilege_data_from_editor(content: String) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
content
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !(line.starts_with('#') || line.starts_with("//") || line == &""))
|
||||
.skip(1)
|
||||
.map(|line| {
|
||||
let line_parts: Vec<&str> = line.trim().split_ascii_whitespace().collect();
|
||||
if line_parts.len() != DATABASE_PRIVILEGE_FIELDS.len() {
|
||||
anyhow::bail!("")
|
||||
}
|
||||
|
||||
Ok(DatabasePrivilegeRow {
|
||||
db: (*line_parts.first().unwrap()).to_owned(),
|
||||
user: (*line_parts.get(1).unwrap()).to_owned(),
|
||||
select_priv: parse_privilege(line_parts.get(2).unwrap())
|
||||
.context("Could not parse SELECT privilege")?,
|
||||
insert_priv: parse_privilege(line_parts.get(3).unwrap())
|
||||
.context("Could not parse INSERT privilege")?,
|
||||
update_priv: parse_privilege(line_parts.get(4).unwrap())
|
||||
.context("Could not parse UPDATE privilege")?,
|
||||
delete_priv: parse_privilege(line_parts.get(5).unwrap())
|
||||
.context("Could not parse DELETE privilege")?,
|
||||
create_priv: parse_privilege(line_parts.get(6).unwrap())
|
||||
.context("Could not parse CREATE privilege")?,
|
||||
drop_priv: parse_privilege(line_parts.get(7).unwrap())
|
||||
.context("Could not parse DROP privilege")?,
|
||||
alter_priv: parse_privilege(line_parts.get(8).unwrap())
|
||||
.context("Could not parse ALTER privilege")?,
|
||||
index_priv: parse_privilege(line_parts.get(9).unwrap())
|
||||
.context("Could not parse INDEX privilege")?,
|
||||
create_tmp_table_priv: parse_privilege(line_parts.get(10).unwrap())
|
||||
.context("Could not parse CREATE TEMPORARY TABLE privilege")?,
|
||||
lock_tables_priv: parse_privilege(line_parts.get(11).unwrap())
|
||||
.context("Could not parse LOCK TABLES privilege")?,
|
||||
references_priv: parse_privilege(line_parts.get(12).unwrap())
|
||||
.context("Could not parse REFERENCES privilege")?,
|
||||
})
|
||||
})
|
||||
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
|
||||
}
|
||||
|
||||
fn format_privileges_line(
|
||||
privs: &DatabasePrivilegeRow,
|
||||
username_len: usize,
|
||||
database_name_len: usize,
|
||||
) -> 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
|
||||
.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()
|
||||
}
|
||||
|
||||
pub async fn edit_privileges(
|
||||
args: DatabaseEditPrivsArgs,
|
||||
connection: &mut MySqlConnection,
|
||||
@@ -431,108 +293,17 @@ pub async fn edit_privileges(
|
||||
get_all_database_privileges(connection).await?
|
||||
};
|
||||
|
||||
// TODO: The data from args should not be absolute.
|
||||
// In the current implementation, the user would need to
|
||||
// provide all privileges for all users on all databases.
|
||||
// The intended effect is to modify the privileges which have
|
||||
// matching users and databases, as well as add any
|
||||
// new db-user pairs. This makes it impossible to remove
|
||||
// privileges, but that is an issue for another day.
|
||||
let privileges_to_change = if !args.privs.is_empty() {
|
||||
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>>>()?
|
||||
}
|
||||
parse_privilege_tables_from_args(&args)?
|
||||
} else {
|
||||
let comment = indoc! {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.
|
||||
"#};
|
||||
|
||||
let unix_user = get_current_unix_user()?;
|
||||
let example_user = format!("{}_user", unix_user.name);
|
||||
let example_db = format!("{}_db", unix_user.name);
|
||||
|
||||
let longest_username = privilege_data
|
||||
.iter()
|
||||
.map(|p| p.user.len())
|
||||
.max()
|
||||
.unwrap_or(example_user.len());
|
||||
|
||||
let longest_database_name = privilege_data
|
||||
.iter()
|
||||
.map(|p| p.db.len())
|
||||
.max()
|
||||
.unwrap_or(example_db.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(
|
||||
&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,
|
||||
);
|
||||
|
||||
// TODO: handle errors better here
|
||||
let result = Editor::new()
|
||||
.extension("tsv")
|
||||
.edit(
|
||||
format!(
|
||||
"{}\n{}\n{}",
|
||||
comment,
|
||||
header.join(" "),
|
||||
if privilege_data.is_empty() {
|
||||
format!("# {}", example_line)
|
||||
} else {
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|privs| {
|
||||
format_privileges_line(
|
||||
privs,
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
)
|
||||
.as_str(),
|
||||
)?
|
||||
.unwrap();
|
||||
|
||||
parse_privilege_data_from_editor(result)
|
||||
.context("Could not parse privilege data from editor")?
|
||||
edit_privileges_with_editor(&privilege_data)?
|
||||
};
|
||||
|
||||
for row in privileges_to_change.iter() {
|
||||
@@ -542,7 +313,7 @@ pub async fn edit_privileges(
|
||||
}
|
||||
}
|
||||
|
||||
let diffs = diff_privileges(privilege_data, &privileges_to_change).await;
|
||||
let diffs = diff_privileges(privilege_data, &privileges_to_change);
|
||||
|
||||
if diffs.is_empty() {
|
||||
println!("No changes to make.");
|
||||
@@ -555,3 +326,45 @@ pub async fn edit_privileges(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn parse_privilege_tables_from_args(
|
||||
args: &DatabaseEditPrivsArgs,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn edit_privileges_with_editor(
|
||||
privilege_data: &[DatabasePrivilegeRow],
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
let unix_user = get_current_unix_user()?;
|
||||
|
||||
let editor_content =
|
||||
generate_editor_content_from_privilege_data(privilege_data, &unix_user.name);
|
||||
|
||||
// TODO: handle errors better here
|
||||
let result = Editor::new()
|
||||
.extension("tsv")
|
||||
.edit(&editor_content)?
|
||||
.unwrap();
|
||||
|
||||
parse_privilege_data_from_editor_content(result)
|
||||
.context("Could not parse privilege data from editor")
|
||||
}
|
||||
|
Reference in New Issue
Block a user