Misc 2 #40
|
@ -1364,6 +1364,7 @@ version = "1.0.116"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -15,7 +15,7 @@ nix = { version = "0.28.0", features = ["user"] }
|
||||||
prettytable = "0.10.0"
|
prettytable = "0.10.0"
|
||||||
ratatui = { version = "0.26.2", optional = true }
|
ratatui = { version = "0.26.2", optional = true }
|
||||||
serde = "1.0.198"
|
serde = "1.0.198"
|
||||||
serde_json = "1.0.116"
|
serde_json = { version = "1.0.116", features = ["preserve_order"] }
|
||||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "mysql", "tls-rustls"] }
|
sqlx = { version = "0.7.4", features = ["runtime-tokio", "mysql", "tls-rustls"] }
|
||||||
tokio = { version = "1.37.0", features = ["rt", "macros"] }
|
tokio = { version = "1.37.0", features = ["rt", "macros"] }
|
||||||
toml = "0.8.12"
|
toml = "0.8.12"
|
||||||
|
|
|
@ -28,7 +28,10 @@
|
||||||
in f system pkgs toolchain);
|
in f system pkgs toolchain);
|
||||||
in {
|
in {
|
||||||
devShell = forAllSystems (system: pkgs: toolchain: pkgs.mkShell {
|
devShell = forAllSystems (system: pkgs: toolchain: pkgs.mkShell {
|
||||||
nativeBuildInputs = [ toolchain ];
|
nativeBuildInputs = [
|
||||||
|
toolchain
|
||||||
|
pkgs.mysql-client
|
||||||
|
];
|
||||||
|
|
||||||
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
|
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
use anyhow::{anyhow, Context};
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dialoguer::Editor;
|
use dialoguer::Editor;
|
||||||
use indoc::indoc;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use prettytable::{Cell, Row, Table};
|
use prettytable::{Cell, Row, Table};
|
||||||
use sqlx::{Connection, MySqlConnection};
|
use sqlx::{Connection, MySqlConnection};
|
||||||
|
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
self,
|
common::{close_database_connection, get_current_unix_user, yn},
|
||||||
common::{close_database_connection, get_current_unix_user},
|
database_operations::*,
|
||||||
database_operations::{
|
database_privilege_operations::*,
|
||||||
apply_permission_diffs, db_priv_field_human_readable_name, diff_permissions, yn,
|
|
||||||
DatabasePrivileges, DATABASE_PRIVILEGE_FIELDS,
|
|
||||||
},
|
|
||||||
user_operations::user_exists,
|
user_operations::user_exists,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -31,29 +26,29 @@ pub enum DatabaseCommand {
|
||||||
#[command()]
|
#[command()]
|
||||||
ListDb(DatabaseListArgs),
|
ListDb(DatabaseListArgs),
|
||||||
|
|
||||||
/// List user permissions for one or more databases
|
/// List user privileges for one or more databases
|
||||||
///
|
///
|
||||||
/// If no database names are provided, it will show permissions for all databases you have access to.
|
/// If no database names are provided, it will show privileges for all databases you have access to.
|
||||||
#[command()]
|
#[command()]
|
||||||
ShowDbPerm(DatabaseShowPermArgs),
|
ShowDbPrivs(DatabaseShowPrivsArgs),
|
||||||
|
|
||||||
/// Change user permissions for one or more databases. See `edit-db-perm --help` for details.
|
/// Change user privileges for one or more databases. See `edit-db-privs --help` for details.
|
||||||
///
|
///
|
||||||
/// This command has two modes of operation:
|
/// This command has two modes of operation:
|
||||||
///
|
///
|
||||||
/// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the permissions using a text editor.
|
/// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the privileges using a text editor.
|
||||||
///
|
///
|
||||||
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
||||||
///
|
///
|
||||||
/// Follow the instructions inside the editor for more information.
|
/// Follow the instructions inside the editor for more information.
|
||||||
///
|
///
|
||||||
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write permissions using arguments.
|
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write privileges using arguments.
|
||||||
///
|
///
|
||||||
/// The permission arguments should be formatted as `<db>:<user>:<privileges>`
|
/// The privilege arguments should be formatted as `<db>:<user>:<privileges>`
|
||||||
/// where the privileges are a string of characters, each representing a single permissions.
|
/// where the privileges are a string of characters, each representing a single privilege.
|
||||||
/// The character `A` is an exception, because it represents all permissions.
|
/// The character `A` is an exception - it represents all privileges.
|
||||||
///
|
///
|
||||||
/// The character to permission mapping is declared as follows:
|
/// The character-to-privilege mapping is defined as follows:
|
||||||
///
|
///
|
||||||
/// - `s` - SELECT
|
/// - `s` - SELECT
|
||||||
/// - `i` - INSERT
|
/// - `i` - INSERT
|
||||||
|
@ -68,24 +63,27 @@ pub enum DatabaseCommand {
|
||||||
/// - `r` - REFERENCES
|
/// - `r` - REFERENCES
|
||||||
/// - `A` - ALL PRIVILEGES
|
/// - `A` - ALL PRIVILEGES
|
||||||
///
|
///
|
||||||
/// If you provide a database name, you can omit it from the permission arguments.
|
/// If you provide a database name, you can omit it from the privilege string,
|
||||||
|
/// e.g. `edit-db-privs my_db -p my_user:siu` is equivalent to `edit-db-privs -p my_db:my_user:siu`.
|
||||||
|
/// While it doesn't make much of a difference for a single edit, it can be useful for editing multiple users
|
||||||
|
/// on the same database at once.
|
||||||
///
|
///
|
||||||
/// Example usage of non-interactive mode:
|
/// Example usage of non-interactive mode:
|
||||||
///
|
///
|
||||||
/// Set permissions `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
|
/// Enable privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
|
||||||
///
|
///
|
||||||
/// mysqladm edit-db-perm -p my_db:my_user:siu
|
/// `mysqladm edit-db-privs -p my_db:my_user:siu`
|
||||||
///
|
///
|
||||||
/// Set all permissions for user `my_other_user` on database `my_other_db`:
|
/// Enable all privileges for user `my_other_user` on database `my_other_db`:
|
||||||
///
|
///
|
||||||
/// mysqladm edit-db-perm -p my_other_db:my_other_user:A
|
/// `mysqladm edit-db-privs -p my_other_db:my_other_user:A`
|
||||||
///
|
///
|
||||||
/// Set miscellaneous permissions for multiple users on database `my_db`:
|
/// Set miscellaneous privileges for multiple users on database `my_db`:
|
||||||
///
|
///
|
||||||
/// mysqladm edit-db-perm my_db -p my_user:siu my_other_user:ct
|
/// `mysqladm edit-db-privs my_db -p my_user:siu my_other_user:ct``
|
||||||
///
|
///
|
||||||
#[command(verbatim_doc_comment)]
|
#[command(verbatim_doc_comment)]
|
||||||
EditDbPerm(DatabaseEditPermArgs),
|
EditDbPrivs(DatabaseEditPrivsArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -110,7 +108,7 @@ pub struct DatabaseListArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct DatabaseShowPermArgs {
|
pub struct DatabaseShowPrivsArgs {
|
||||||
/// The name of the database(s) to show.
|
/// The name of the database(s) to show.
|
||||||
#[arg(num_args = 0..)]
|
#[arg(num_args = 0..)]
|
||||||
name: Vec<String>,
|
name: Vec<String>,
|
||||||
|
@ -121,18 +119,18 @@ pub struct DatabaseShowPermArgs {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct DatabaseEditPermArgs {
|
pub struct DatabaseEditPrivsArgs {
|
||||||
/// The name of the database to edit permissions for.
|
/// The name of the database to edit privileges for.
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
|
|
||||||
#[arg(short, long, value_name = "[DATABASE:]USER:PERMISSIONS", num_args = 0..)]
|
#[arg(short, long, value_name = "[DATABASE:]USER:PRIVILEGES", num_args = 0..)]
|
||||||
pub perm: Vec<String>,
|
pub privs: Vec<String>,
|
||||||
|
|
||||||
/// Whether to output the information in JSON format.
|
/// Whether to output the information in JSON format.
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
|
||||||
/// Specify the text editor to use for editing permissions.
|
/// Specify the text editor to use for editing privileges
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
pub editor: Option<String>,
|
pub editor: Option<String>,
|
||||||
|
|
||||||
|
@ -143,30 +141,30 @@ pub struct DatabaseEditPermArgs {
|
||||||
|
|
||||||
pub async fn handle_command(
|
pub async fn handle_command(
|
||||||
command: DatabaseCommand,
|
command: DatabaseCommand,
|
||||||
mut conn: MySqlConnection,
|
mut connection: MySqlConnection,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let result = conn
|
let result = connection
|
||||||
.transaction(|txn| {
|
.transaction(|txn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match command {
|
match command {
|
||||||
DatabaseCommand::CreateDb(args) => create_databases(args, txn).await,
|
DatabaseCommand::CreateDb(args) => create_databases(args, txn).await,
|
||||||
DatabaseCommand::DropDb(args) => drop_databases(args, txn).await,
|
DatabaseCommand::DropDb(args) => drop_databases(args, txn).await,
|
||||||
DatabaseCommand::ListDb(args) => list_databases(args, txn).await,
|
DatabaseCommand::ListDb(args) => list_databases(args, txn).await,
|
||||||
DatabaseCommand::ShowDbPerm(args) => show_databases(args, txn).await,
|
DatabaseCommand::ShowDbPrivs(args) => show_database_privileges(args, txn).await,
|
||||||
DatabaseCommand::EditDbPerm(args) => edit_permissions(args, txn).await,
|
DatabaseCommand::EditDbPrivs(args) => edit_privileges(args, txn).await,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
close_database_connection(conn).await;
|
close_database_connection(connection).await;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_databases(
|
async fn create_databases(
|
||||||
args: DatabaseCreateArgs,
|
args: DatabaseCreateArgs,
|
||||||
conn: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if args.name.is_empty() {
|
if args.name.is_empty() {
|
||||||
anyhow::bail!("No database names provided");
|
anyhow::bail!("No database names provided");
|
||||||
|
@ -174,7 +172,7 @@ async fn create_databases(
|
||||||
|
|
||||||
for name in args.name {
|
for name in args.name {
|
||||||
// TODO: This can be optimized by fetching all the database privileges in one query.
|
// TODO: This can be optimized by fetching all the database privileges in one query.
|
||||||
if let Err(e) = core::database_operations::create_database(&name, conn).await {
|
if let Err(e) = create_database(&name, connection).await {
|
||||||
eprintln!("Failed to create database '{}': {}", name, e);
|
eprintln!("Failed to create database '{}': {}", name, e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...");
|
||||||
}
|
}
|
||||||
|
@ -183,14 +181,17 @@ async fn create_databases(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn drop_databases(args: DatabaseDropArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn drop_databases(
|
||||||
|
args: DatabaseDropArgs,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
if args.name.is_empty() {
|
if args.name.is_empty() {
|
||||||
anyhow::bail!("No database names provided");
|
anyhow::bail!("No database names provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in args.name {
|
for name in args.name {
|
||||||
// TODO: This can be optimized by fetching all the database privileges in one query.
|
// TODO: This can be optimized by fetching all the database privileges in one query.
|
||||||
if let Err(e) = core::database_operations::drop_database(&name, conn).await {
|
if let Err(e) = drop_database(&name, connection).await {
|
||||||
eprintln!("Failed to drop database '{}': {}", name, e);
|
eprintln!("Failed to drop database '{}': {}", name, e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...");
|
||||||
}
|
}
|
||||||
|
@ -199,8 +200,11 @@ async fn drop_databases(args: DatabaseDropArgs, conn: &mut MySqlConnection) -> a
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn list_databases(args: DatabaseListArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn list_databases(
|
||||||
let databases = core::database_operations::get_database_list(conn).await?;
|
args: DatabaseListArgs,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let databases = get_database_list(connection).await?;
|
||||||
|
|
||||||
if databases.is_empty() {
|
if databases.is_empty() {
|
||||||
println!("No databases to show.");
|
println!("No databases to show.");
|
||||||
|
@ -218,17 +222,17 @@ async fn list_databases(args: DatabaseListArgs, conn: &mut MySqlConnection) -> a
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_databases(
|
async fn show_database_privileges(
|
||||||
args: DatabaseShowPermArgs,
|
args: DatabaseShowPrivsArgs,
|
||||||
conn: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let database_users_to_show = if args.name.is_empty() {
|
let database_users_to_show = if args.name.is_empty() {
|
||||||
core::database_operations::get_all_database_privileges(conn).await?
|
get_all_database_privileges(connection).await?
|
||||||
} else {
|
} else {
|
||||||
// TODO: This can be optimized by fetching all the database privileges in one query.
|
// TODO: This can be optimized by fetching all the database privileges in one query.
|
||||||
let mut result = Vec::with_capacity(args.name.len());
|
let mut result = Vec::with_capacity(args.name.len());
|
||||||
for name in args.name {
|
for name in args.name {
|
||||||
match core::database_operations::get_database_privileges(&name, conn).await {
|
match get_database_privileges(&name, connection).await {
|
||||||
Ok(db) => result.extend(db),
|
Ok(db) => result.extend(db),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to show database '{}': {}", name, e);
|
eprintln!("Failed to show database '{}': {}", name, e);
|
||||||
|
@ -279,264 +283,37 @@ async fn show_databases(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See documentation for `DatabaseCommand::EditPerm`.
|
pub async fn edit_privileges(
|
||||||
fn parse_permission_table_cli_arg(arg: &str) -> anyhow::Result<DatabasePrivileges> {
|
args: DatabaseEditPrivsArgs,
|
||||||
let parts: Vec<&str> = arg.split(':').collect();
|
connection: &mut MySqlConnection,
|
||||||
if parts.len() != 3 {
|
|
||||||
anyhow::bail!("Invalid argument format. See `edit-perm --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 = DatabasePrivileges {
|
|
||||||
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 permission character: {}", char),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_permission(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_permission_data_from_editor(content: String) -> anyhow::Result<Vec<DatabasePrivileges>> {
|
|
||||||
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(DatabasePrivileges {
|
|
||||||
db: (*line_parts.first().unwrap()).to_owned(),
|
|
||||||
user: (*line_parts.get(1).unwrap()).to_owned(),
|
|
||||||
select_priv: parse_permission(line_parts.get(2).unwrap())
|
|
||||||
.context("Could not parse SELECT privilege")?,
|
|
||||||
insert_priv: parse_permission(line_parts.get(3).unwrap())
|
|
||||||
.context("Could not parse INSERT privilege")?,
|
|
||||||
update_priv: parse_permission(line_parts.get(4).unwrap())
|
|
||||||
.context("Could not parse UPDATE privilege")?,
|
|
||||||
delete_priv: parse_permission(line_parts.get(5).unwrap())
|
|
||||||
.context("Could not parse DELETE privilege")?,
|
|
||||||
create_priv: parse_permission(line_parts.get(6).unwrap())
|
|
||||||
.context("Could not parse CREATE privilege")?,
|
|
||||||
drop_priv: parse_permission(line_parts.get(7).unwrap())
|
|
||||||
.context("Could not parse DROP privilege")?,
|
|
||||||
alter_priv: parse_permission(line_parts.get(8).unwrap())
|
|
||||||
.context("Could not parse ALTER privilege")?,
|
|
||||||
index_priv: parse_permission(line_parts.get(9).unwrap())
|
|
||||||
.context("Could not parse INDEX privilege")?,
|
|
||||||
create_tmp_table_priv: parse_permission(line_parts.get(10).unwrap())
|
|
||||||
.context("Could not parse CREATE TEMPORARY TABLE privilege")?,
|
|
||||||
lock_tables_priv: parse_permission(line_parts.get(11).unwrap())
|
|
||||||
.context("Could not parse LOCK TABLES privilege")?,
|
|
||||||
references_priv: parse_permission(line_parts.get(12).unwrap())
|
|
||||||
.context("Could not parse REFERENCES privilege")?,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_privileges_line(
|
|
||||||
privs: &DatabasePrivileges,
|
|
||||||
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_permissions(
|
|
||||||
args: DatabaseEditPermArgs,
|
|
||||||
conn: &mut MySqlConnection,
|
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let permission_data = if let Some(name) = &args.name {
|
let privilege_data = if let Some(name) = &args.name {
|
||||||
core::database_operations::get_database_privileges(name, conn).await?
|
get_database_privileges(name, connection).await?
|
||||||
} else {
|
} else {
|
||||||
core::database_operations::get_all_database_privileges(conn).await?
|
get_all_database_privileges(connection).await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let permissions_to_change = if !args.perm.is_empty() {
|
// TODO: The data from args should not be absolute.
|
||||||
if let Some(name) = args.name {
|
// In the current implementation, the user would need to
|
||||||
args.perm
|
// provide all privileges for all users on all databases.
|
||||||
.iter()
|
// The intended effect is to modify the privileges which have
|
||||||
.map(|perm| {
|
// matching users and databases, as well as add any
|
||||||
parse_permission_table_cli_arg(&format!("{}:{}", name, &perm))
|
// new db-user pairs. This makes it impossible to remove
|
||||||
.context(format!("Failed parsing database permissions: `{}`", &perm))
|
// privileges, but that is an issue for another day.
|
||||||
})
|
let privileges_to_change = if !args.privs.is_empty() {
|
||||||
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()?
|
parse_privilege_tables_from_args(&args)?
|
||||||
} else {
|
} else {
|
||||||
args.perm
|
edit_privileges_with_editor(&privilege_data)?
|
||||||
.iter()
|
|
||||||
.map(|perm| {
|
|
||||||
parse_permission_table_cli_arg(perm)
|
|
||||||
.context(format!("Failed parsing database permissions: `{}`", &perm))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<Vec<DatabasePrivileges>>>()?
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let comment = indoc! {r#"
|
|
||||||
# Welcome to the permission editor.
|
|
||||||
# Each line defines what permissions 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 permissions.
|
|
||||||
# If the user should have a permission, 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 = permission_data
|
|
||||||
.iter()
|
|
||||||
.map(|p| p.user.len())
|
|
||||||
.max()
|
|
||||||
.unwrap_or(example_user.len());
|
|
||||||
|
|
||||||
let longest_database_name = permission_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 permissions.
|
|
||||||
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(
|
|
||||||
&DatabasePrivileges {
|
|
||||||
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 permission_data.is_empty() {
|
|
||||||
format!("# {}", example_line)
|
|
||||||
} else {
|
|
||||||
permission_data
|
|
||||||
.iter()
|
|
||||||
.map(|perm| {
|
|
||||||
format_privileges_line(
|
|
||||||
perm,
|
|
||||||
longest_username,
|
|
||||||
longest_database_name,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.join("\n")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.as_str(),
|
|
||||||
)?
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
parse_permission_data_from_editor(result)
|
|
||||||
.context("Could not parse permission data from editor")?
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for row in permissions_to_change.iter() {
|
for row in privileges_to_change.iter() {
|
||||||
if !user_exists(&row.user, conn).await? {
|
if !user_exists(&row.user, connection).await? {
|
||||||
// TODO: allow user to return and correct their mistake
|
// TODO: allow user to return and correct their mistake
|
||||||
anyhow::bail!("User {} does not exist", row.user);
|
anyhow::bail!("User {} does not exist", row.user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let diffs = diff_permissions(permission_data, &permissions_to_change).await;
|
let diffs = diff_privileges(privilege_data, &privileges_to_change);
|
||||||
|
|
||||||
if diffs.is_empty() {
|
if diffs.is_empty() {
|
||||||
println!("No changes to make.");
|
println!("No changes to make.");
|
||||||
|
@ -545,7 +322,49 @@ pub async fn edit_permissions(
|
||||||
|
|
||||||
// TODO: Add confirmation prompt.
|
// TODO: Add confirmation prompt.
|
||||||
|
|
||||||
apply_permission_diffs(diffs, conn).await?;
|
apply_privilege_diffs(diffs, connection).await?;
|
||||||
|
|
||||||
Ok(())
|
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")
|
||||||
|
}
|
||||||
|
|
|
@ -7,8 +7,10 @@ use crate::{
|
||||||
mysql_admutils_compatibility::common::{filter_db_or_user_names, DbOrUser},
|
mysql_admutils_compatibility::common::{filter_db_or_user_names, DbOrUser},
|
||||||
},
|
},
|
||||||
core::{
|
core::{
|
||||||
|
common::yn,
|
||||||
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
||||||
database_operations::{self, yn},
|
database_operations::{create_database, drop_database, get_database_list},
|
||||||
|
database_privilege_operations,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,6 +50,10 @@ pub struct Args {
|
||||||
pub help_editperm: bool,
|
pub help_editperm: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: mysql-dbadm explicitly calls privileges "permissions".
|
||||||
|
// This is something we're trying to move away from.
|
||||||
|
// See https://git.pvv.ntnu.no/Projects/mysqladm-rs/issues/29
|
||||||
|
|
||||||
/// Create, drop or edit permissions for the DATABASE(s),
|
/// Create, drop or edit permissions for the DATABASE(s),
|
||||||
/// as determined by the COMMAND.
|
/// as determined by the COMMAND.
|
||||||
///
|
///
|
||||||
|
@ -129,20 +135,20 @@ pub async fn main() -> anyhow::Result<()> {
|
||||||
Command::Create(args) => {
|
Command::Create(args) => {
|
||||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
||||||
for name in filtered_names {
|
for name in filtered_names {
|
||||||
database_operations::create_database(&name, &mut connection).await?;
|
create_database(&name, &mut connection).await?;
|
||||||
println!("Database {} created.", name);
|
println!("Database {} created.", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Drop(args) => {
|
Command::Drop(args) => {
|
||||||
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
let filtered_names = filter_db_or_user_names(args.name, DbOrUser::Database)?;
|
||||||
for name in filtered_names {
|
for name in filtered_names {
|
||||||
database_operations::drop_database(&name, &mut connection).await?;
|
drop_database(&name, &mut connection).await?;
|
||||||
println!("Database {} dropped.", name);
|
println!("Database {} dropped.", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Show(args) => {
|
Command::Show(args) => {
|
||||||
let names = if args.name.is_empty() {
|
let names = if args.name.is_empty() {
|
||||||
database_operations::get_database_list(&mut connection).await?
|
get_database_list(&mut connection).await?
|
||||||
} else {
|
} else {
|
||||||
filter_db_or_user_names(args.name, DbOrUser::Database)?
|
filter_db_or_user_names(args.name, DbOrUser::Database)?
|
||||||
};
|
};
|
||||||
|
@ -156,27 +162,27 @@ pub async fn main() -> anyhow::Result<()> {
|
||||||
// Hopefully, not many people rely on this in an automated fashion, as it
|
// Hopefully, not many people rely on this in an automated fashion, as it
|
||||||
// is made to be interactive in nature. However, we should still try to
|
// is made to be interactive in nature. However, we should still try to
|
||||||
// replicate the old behavior as closely as possible.
|
// replicate the old behavior as closely as possible.
|
||||||
let edit_permissions_args = database_command::DatabaseEditPermArgs {
|
let edit_privileges_args = database_command::DatabaseEditPrivsArgs {
|
||||||
name: Some(args.database),
|
name: Some(args.database),
|
||||||
perm: vec![],
|
privs: vec![],
|
||||||
json: false,
|
json: false,
|
||||||
editor: None,
|
editor: None,
|
||||||
yes: false,
|
yes: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
database_command::edit_permissions(edit_permissions_args, &mut connection).await?;
|
database_command::edit_privileges(edit_privileges_args, &mut connection).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_db(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn show_db(name: &str, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
// NOTE: mysql-dbadm show has a quirk where valid database names
|
// NOTE: mysql-dbadm show has a quirk where valid database names
|
||||||
// for non-existent databases will report with no users.
|
// for non-existent databases will report with no users.
|
||||||
// This function should *not* check for db existence, only
|
// This function should *not* check for db existence, only
|
||||||
// validate the names.
|
// validate the names.
|
||||||
let permissions = database_operations::get_database_privileges(name, conn)
|
let privileges = database_privilege_operations::get_database_privileges(name, connection)
|
||||||
.await
|
.await
|
||||||
.unwrap_or(vec![]);
|
.unwrap_or(vec![]);
|
||||||
|
|
||||||
|
@ -188,24 +194,24 @@ async fn show_db(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
),
|
),
|
||||||
name,
|
name,
|
||||||
);
|
);
|
||||||
if permissions.is_empty() {
|
if privileges.is_empty() {
|
||||||
println!("# (no permissions currently granted to any users)");
|
println!("# (no permissions currently granted to any users)");
|
||||||
} else {
|
} else {
|
||||||
for permission in permissions {
|
for privilege in privileges {
|
||||||
println!(
|
println!(
|
||||||
" {:<16} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
|
" {:<16} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {:<7} {}",
|
||||||
permission.user,
|
privilege.user,
|
||||||
yn(permission.select_priv),
|
yn(privilege.select_priv),
|
||||||
yn(permission.insert_priv),
|
yn(privilege.insert_priv),
|
||||||
yn(permission.update_priv),
|
yn(privilege.update_priv),
|
||||||
yn(permission.delete_priv),
|
yn(privilege.delete_priv),
|
||||||
yn(permission.create_priv),
|
yn(privilege.create_priv),
|
||||||
yn(permission.drop_priv),
|
yn(privilege.drop_priv),
|
||||||
yn(permission.alter_priv),
|
yn(privilege.alter_priv),
|
||||||
yn(permission.index_priv),
|
yn(privilege.index_priv),
|
||||||
yn(permission.create_tmp_table_priv),
|
yn(privilege.create_tmp_table_priv),
|
||||||
yn(permission.lock_tables_priv),
|
yn(privilege.lock_tables_priv),
|
||||||
yn(permission.references_priv)
|
yn(privilege.references_priv)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,10 +9,7 @@ use crate::{
|
||||||
core::{
|
core::{
|
||||||
common::{close_database_connection, get_current_unix_user},
|
common::{close_database_connection, get_current_unix_user},
|
||||||
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
config::{get_config, mysql_connection_from_config, GlobalConfigArgs},
|
||||||
user_operations::{
|
user_operations::*,
|
||||||
create_database_user, delete_database_user, get_all_database_users_for_unix_user,
|
|
||||||
get_database_user_for_user, set_password_for_database_user, user_exists,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
|
use std::collections::BTreeMap;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use dialoguer::{Confirm, Password};
|
use dialoguer::{Confirm, Password};
|
||||||
|
use prettytable::Table;
|
||||||
|
use serde_json::json;
|
||||||
use sqlx::{Connection, MySqlConnection};
|
use sqlx::{Connection, MySqlConnection};
|
||||||
|
|
||||||
use crate::core::{common::close_database_connection, user_operations::validate_user_name};
|
use crate::core::{
|
||||||
|
common::{close_database_connection, get_current_unix_user},
|
||||||
|
database_operations::*,
|
||||||
|
user_operations::*,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
pub struct UserArgs {
|
pub struct UserArgs {
|
||||||
|
@ -67,8 +74,11 @@ pub struct UserShowArgs {
|
||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn handle_command(command: UserCommand, mut conn: MySqlConnection) -> anyhow::Result<()> {
|
pub async fn handle_command(
|
||||||
let result = conn
|
command: UserCommand,
|
||||||
|
mut connection: MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let result = connection
|
||||||
.transaction(|txn| {
|
.transaction(|txn| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
match command {
|
match command {
|
||||||
|
@ -81,18 +91,21 @@ pub async fn handle_command(command: UserCommand, mut conn: MySqlConnection) ->
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
close_database_connection(conn).await;
|
close_database_connection(connection).await;
|
||||||
|
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_users(args: UserCreateArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn create_users(
|
||||||
|
args: UserCreateArgs,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
if args.username.is_empty() {
|
if args.username.is_empty() {
|
||||||
anyhow::bail!("No usernames provided");
|
anyhow::bail!("No usernames provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
for username in args.username {
|
for username in args.username {
|
||||||
if let Err(e) = crate::core::user_operations::create_database_user(&username, conn).await {
|
if let Err(e) = create_database_user(&username, connection).await {
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
eprintln!("Skipping...\n");
|
eprintln!("Skipping...\n");
|
||||||
continue;
|
continue;
|
||||||
|
@ -113,7 +126,7 @@ async fn create_users(args: UserCreateArgs, conn: &mut MySqlConnection) -> anyho
|
||||||
username,
|
username,
|
||||||
password_file: None,
|
password_file: None,
|
||||||
},
|
},
|
||||||
conn,
|
connection,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
@ -122,13 +135,13 @@ async fn create_users(args: UserCreateArgs, conn: &mut MySqlConnection) -> anyho
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn drop_users(args: UserDeleteArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn drop_users(args: UserDeleteArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
if args.username.is_empty() {
|
if args.username.is_empty() {
|
||||||
anyhow::bail!("No usernames provided");
|
anyhow::bail!("No usernames provided");
|
||||||
}
|
}
|
||||||
|
|
||||||
for username in args.username {
|
for username in args.username {
|
||||||
if let Err(e) = crate::core::user_operations::delete_database_user(&username, conn).await {
|
if let Err(e) = delete_database_user(&username, connection).await {
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...");
|
||||||
}
|
}
|
||||||
|
@ -149,11 +162,11 @@ pub fn read_password_from_stdin_with_double_check(username: &str) -> anyhow::Res
|
||||||
|
|
||||||
async fn change_password_for_user(
|
async fn change_password_for_user(
|
||||||
args: UserPasswdArgs,
|
args: UserPasswdArgs,
|
||||||
conn: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
// NOTE: although this also is checked in `set_password_for_database_user`, we check it here
|
// NOTE: although this also is checked in `set_password_for_database_user`, we check it here
|
||||||
// to provide a more natural order of error messages.
|
// to provide a more natural order of error messages.
|
||||||
let unix_user = crate::core::common::get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
validate_user_name(&args.username, &unix_user)?;
|
validate_user_name(&args.username, &unix_user)?;
|
||||||
|
|
||||||
let password = if let Some(password_file) = args.password_file {
|
let password = if let Some(password_file) = args.password_file {
|
||||||
|
@ -165,17 +178,16 @@ async fn change_password_for_user(
|
||||||
read_password_from_stdin_with_double_check(&args.username)?
|
read_password_from_stdin_with_double_check(&args.username)?
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::core::user_operations::set_password_for_database_user(&args.username, &password, conn)
|
set_password_for_database_user(&args.username, &password, connection).await?;
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn show_users(args: UserShowArgs, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
async fn show_users(args: UserShowArgs, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
let unix_user = crate::core::common::get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
let users = if args.username.is_empty() {
|
let users = if args.username.is_empty() {
|
||||||
crate::core::user_operations::get_all_database_users_for_unix_user(&unix_user, conn).await?
|
get_all_database_users_for_unix_user(&unix_user, connection).await?
|
||||||
} else {
|
} else {
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
for username in args.username {
|
for username in args.username {
|
||||||
|
@ -185,8 +197,7 @@ async fn show_users(args: UserShowArgs, conn: &mut MySqlConnection) -> anyhow::R
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user =
|
let user = get_database_user_for_user(&username, connection).await?;
|
||||||
crate::core::user_operations::get_database_user_for_user(&username, conn).await?;
|
|
||||||
if let Some(user) = user {
|
if let Some(user) = user {
|
||||||
result.push(user);
|
result.push(user);
|
||||||
} else {
|
} else {
|
||||||
|
@ -196,20 +207,47 @@ async fn show_users(args: UserShowArgs, conn: &mut MySqlConnection) -> anyhow::R
|
||||||
result
|
result
|
||||||
};
|
};
|
||||||
|
|
||||||
if args.json {
|
let mut user_databases: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||||
println!("{}", serde_json::to_string_pretty(&users)?);
|
for user in users.iter() {
|
||||||
} else {
|
user_databases.insert(
|
||||||
for user in users {
|
user.user.clone(),
|
||||||
println!(
|
get_databases_where_user_has_privileges(&user.user, connection).await?,
|
||||||
"User '{}': {}",
|
|
||||||
&user.user,
|
|
||||||
if user.has_password {
|
|
||||||
"password set."
|
|
||||||
} else {
|
|
||||||
"no password set."
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if args.json {
|
||||||
|
let users_json = users
|
||||||
|
.into_iter()
|
||||||
|
.map(|user| {
|
||||||
|
json!({
|
||||||
|
"user": user.user,
|
||||||
|
"has_password": user.has_password,
|
||||||
|
"databases": user_databases.get(&user.user).unwrap_or(&vec![]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<serde_json::Value>();
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string_pretty(&users_json)
|
||||||
|
.context("Failed to serialize users to JSON")?
|
||||||
|
);
|
||||||
|
} else if users.is_empty() {
|
||||||
|
println!("No users found.");
|
||||||
|
} else {
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.add_row(row![
|
||||||
|
"User",
|
||||||
|
"Password is set",
|
||||||
|
"Databases where user has privileges"
|
||||||
|
]);
|
||||||
|
for user in users {
|
||||||
|
table.add_row(row![
|
||||||
|
user.user,
|
||||||
|
user.has_password,
|
||||||
|
user_databases.get(&user.user).unwrap_or(&vec![]).join("\n")
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
table.printstd();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
pub mod common;
|
pub mod common;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod database_operations;
|
pub mod database_operations;
|
||||||
|
pub mod database_privilege_operations;
|
||||||
pub mod user_operations;
|
pub mod user_operations;
|
||||||
|
|
|
@ -141,8 +141,8 @@ pub fn validate_ownership_by_user_prefix<'a>(
|
||||||
Ok(prefix)
|
Ok(prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn close_database_connection(conn: MySqlConnection) {
|
pub async fn close_database_connection(connection: MySqlConnection) {
|
||||||
if let Err(e) = conn
|
if let Err(e) = connection
|
||||||
.close()
|
.close()
|
||||||
.await
|
.await
|
||||||
.context("Failed to close connection properly")
|
.context("Failed to close connection properly")
|
||||||
|
@ -161,3 +161,21 @@ pub fn quote_literal(s: &str) -> String {
|
||||||
pub fn quote_identifier(s: &str) -> String {
|
pub fn quote_identifier(s: &str) -> String {
|
||||||
format!("`{}`", s.replace('`', r"\`"))
|
format!("`{}`", s.replace('`', r"\`"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn yn(b: bool) -> &'static str {
|
||||||
|
if b {
|
||||||
|
"Y"
|
||||||
|
} else {
|
||||||
|
"N"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn rev_yn(s: &str) -> Option<bool> {
|
||||||
|
match s.to_lowercase().as_str() {
|
||||||
|
"y" => Some(true),
|
||||||
|
"n" => Some(false),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -107,7 +107,7 @@ pub async fn mysql_connection_from_config(config: Config) -> anyhow::Result<MySq
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(conn) => conn.context("Failed to connect to MySQL"),
|
Ok(connection) => connection.context("Failed to connect to MySQL"),
|
||||||
Err(_) => Err(anyhow!("Timed out after 2 seconds")).context("Failed to connect to MySQL"),
|
Err(_) => Err(anyhow!("Timed out after 2 seconds")).context("Failed to connect to MySQL"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,25 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use indoc::indoc;
|
use indoc::formatdoc;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use nix::unistd::User;
|
use nix::unistd::User;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{mysql::MySqlRow, prelude::*, MySqlConnection};
|
use sqlx::{prelude::*, MySqlConnection};
|
||||||
|
|
||||||
use super::common::{
|
use crate::core::{
|
||||||
create_user_group_matching_regex, get_current_unix_user, quote_identifier, validate_name_token,
|
common::{
|
||||||
validate_ownership_by_user_prefix,
|
create_user_group_matching_regex, get_current_unix_user, quote_identifier,
|
||||||
|
validate_name_token, validate_ownership_by_user_prefix,
|
||||||
|
},
|
||||||
|
database_privilege_operations::DATABASE_PRIVILEGE_FIELDS,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn create_database(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
pub async fn create_database(name: &str, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
let user = get_current_unix_user()?;
|
let user = get_current_unix_user()?;
|
||||||
validate_database_name(name, &user)?;
|
validate_database_name(name, &user)?;
|
||||||
|
|
||||||
// NOTE: see the note about SQL injections in `validate_owner_of_database_name`
|
// NOTE: see the note about SQL injections in `validate_owner_of_database_name`
|
||||||
sqlx::query(&format!("CREATE DATABASE {}", quote_identifier(name)))
|
sqlx::query(&format!("CREATE DATABASE {}", quote_identifier(name)))
|
||||||
.execute(conn)
|
.execute(connection)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
if e.to_string().contains("database exists") {
|
if e.to_string().contains("database exists") {
|
||||||
|
@ -31,13 +32,13 @@ pub async fn create_database(name: &str, conn: &mut MySqlConnection) -> anyhow::
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn drop_database(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
pub async fn drop_database(name: &str, connection: &mut MySqlConnection) -> anyhow::Result<()> {
|
||||||
let user = get_current_unix_user()?;
|
let user = get_current_unix_user()?;
|
||||||
validate_database_name(name, &user)?;
|
validate_database_name(name, &user)?;
|
||||||
|
|
||||||
// NOTE: see the note about SQL injections in `validate_owner_of_database_name`
|
// NOTE: see the note about SQL injections in `validate_owner_of_database_name`
|
||||||
sqlx::query(&format!("DROP DATABASE {}", quote_identifier(name)))
|
sqlx::query(&format!("DROP DATABASE {}", quote_identifier(name)))
|
||||||
.execute(conn)
|
.execute(connection)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
if e.to_string().contains("doesn't exist") {
|
if e.to_string().contains("doesn't exist") {
|
||||||
|
@ -55,7 +56,7 @@ struct DatabaseName {
|
||||||
database: String,
|
database: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_database_list(conn: &mut MySqlConnection) -> anyhow::Result<Vec<String>> {
|
pub async fn get_database_list(connection: &mut MySqlConnection) -> anyhow::Result<Vec<String>> {
|
||||||
let unix_user = get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
let databases = sqlx::query_as::<_, DatabaseName>(
|
let databases = sqlx::query_as::<_, DatabaseName>(
|
||||||
|
@ -67,7 +68,7 @@ pub async fn get_database_list(conn: &mut MySqlConnection) -> anyhow::Result<Vec
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(create_user_group_matching_regex(&unix_user))
|
.bind(create_user_group_matching_regex(&unix_user))
|
||||||
.fetch_all(conn)
|
.fetch_all(connection)
|
||||||
.await
|
.await
|
||||||
.context(format!(
|
.context(format!(
|
||||||
"Failed to get databases for user '{}'",
|
"Failed to get databases for user '{}'",
|
||||||
|
@ -77,316 +78,35 @@ pub async fn get_database_list(conn: &mut MySqlConnection) -> anyhow::Result<Vec
|
||||||
Ok(databases.into_iter().map(|d| d.database).collect())
|
Ok(databases.into_iter().map(|d| d.database).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DATABASE_PRIVILEGE_FIELDS: [&str; 13] = [
|
pub async fn get_databases_where_user_has_privileges(
|
||||||
"db",
|
username: &str,
|
||||||
"user",
|
connection: &mut MySqlConnection,
|
||||||
"select_priv",
|
) -> anyhow::Result<Vec<String>> {
|
||||||
"insert_priv",
|
let result = sqlx::query(
|
||||||
"update_priv",
|
formatdoc!(
|
||||||
"delete_priv",
|
r#"
|
||||||
"create_priv",
|
SELECT `db` AS `database`
|
||||||
"drop_priv",
|
FROM `db`
|
||||||
"alter_priv",
|
WHERE `user` = ?
|
||||||
"index_priv",
|
AND ({})
|
||||||
"create_tmp_table_priv",
|
"#,
|
||||||
"lock_tables_priv",
|
DATABASE_PRIVILEGE_FIELDS
|
||||||
"references_priv",
|
.iter()
|
||||||
];
|
.map(|field| format!("`{}` = 'Y'", field))
|
||||||
|
.join(" OR "),
|
||||||
pub fn db_priv_field_human_readable_name(name: &str) -> String {
|
)
|
||||||
match name {
|
.as_str(),
|
||||||
"db" => "Database".to_owned(),
|
)
|
||||||
"user" => "User".to_owned(),
|
.bind(username)
|
||||||
"select_priv" => "Select".to_owned(),
|
.fetch_all(connection)
|
||||||
"insert_priv" => "Insert".to_owned(),
|
.await?
|
||||||
"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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct DatabasePrivileges {
|
|
||||||
pub db: String,
|
|
||||||
pub user: String,
|
|
||||||
pub select_priv: bool,
|
|
||||||
pub insert_priv: bool,
|
|
||||||
pub update_priv: bool,
|
|
||||||
pub delete_priv: bool,
|
|
||||||
pub create_priv: bool,
|
|
||||||
pub drop_priv: bool,
|
|
||||||
pub alter_priv: bool,
|
|
||||||
pub index_priv: bool,
|
|
||||||
pub create_tmp_table_priv: bool,
|
|
||||||
pub lock_tables_priv: bool,
|
|
||||||
pub references_priv: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DatabasePrivileges {
|
|
||||||
pub fn get_privilege_by_name(&self, name: &str) -> bool {
|
|
||||||
match name {
|
|
||||||
"select_priv" => self.select_priv,
|
|
||||||
"insert_priv" => self.insert_priv,
|
|
||||||
"update_priv" => self.update_priv,
|
|
||||||
"delete_priv" => self.delete_priv,
|
|
||||||
"create_priv" => self.create_priv,
|
|
||||||
"drop_priv" => self.drop_priv,
|
|
||||||
"alter_priv" => self.alter_priv,
|
|
||||||
"index_priv" => self.index_priv,
|
|
||||||
"create_tmp_table_priv" => self.create_tmp_table_priv,
|
|
||||||
"lock_tables_priv" => self.lock_tables_priv,
|
|
||||||
"references_priv" => self.references_priv,
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn diff(&self, other: &DatabasePrivileges) -> DatabasePrivilegeDiffList {
|
|
||||||
debug_assert!(self.db == other.db && self.user == other.user);
|
|
||||||
|
|
||||||
DatabasePrivilegeDiffList {
|
|
||||||
db: self.db.clone(),
|
|
||||||
user: self.user.clone(),
|
|
||||||
diff: DATABASE_PRIVILEGE_FIELDS
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.skip(2)
|
.map(|databases| databases.try_get::<String, _>("database").unwrap())
|
||||||
.filter_map(|field| {
|
.collect();
|
||||||
diff_single_priv(
|
|
||||||
self.get_privilege_by_name(field),
|
|
||||||
other.get_privilege_by_name(field),
|
|
||||||
field,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn yn(b: bool) -> &'static str {
|
|
||||||
if b {
|
|
||||||
"Y"
|
|
||||||
} else {
|
|
||||||
"N"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn rev_yn(s: &str) -> bool {
|
|
||||||
match s.to_lowercase().as_str() {
|
|
||||||
"y" => true,
|
|
||||||
"n" => false,
|
|
||||||
_ => {
|
|
||||||
log::warn!("Invalid value for privilege: {}", s);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromRow<'_, MySqlRow> for DatabasePrivileges {
|
|
||||||
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
|
|
||||||
Ok(Self {
|
|
||||||
db: row.try_get("db")?,
|
|
||||||
user: row.try_get("user")?,
|
|
||||||
select_priv: row.try_get("select_priv").map(rev_yn)?,
|
|
||||||
insert_priv: row.try_get("insert_priv").map(rev_yn)?,
|
|
||||||
update_priv: row.try_get("update_priv").map(rev_yn)?,
|
|
||||||
delete_priv: row.try_get("delete_priv").map(rev_yn)?,
|
|
||||||
create_priv: row.try_get("create_priv").map(rev_yn)?,
|
|
||||||
drop_priv: row.try_get("drop_priv").map(rev_yn)?,
|
|
||||||
alter_priv: row.try_get("alter_priv").map(rev_yn)?,
|
|
||||||
index_priv: row.try_get("index_priv").map(rev_yn)?,
|
|
||||||
create_tmp_table_priv: row.try_get("create_tmp_table_priv").map(rev_yn)?,
|
|
||||||
lock_tables_priv: row.try_get("lock_tables_priv").map(rev_yn)?,
|
|
||||||
references_priv: row.try_get("references_priv").map(rev_yn)?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_database_privileges(
|
|
||||||
database_name: &str,
|
|
||||||
conn: &mut MySqlConnection,
|
|
||||||
) -> anyhow::Result<Vec<DatabasePrivileges>> {
|
|
||||||
let unix_user = get_current_unix_user()?;
|
|
||||||
validate_database_name(database_name, &unix_user)?;
|
|
||||||
|
|
||||||
let result = sqlx::query_as::<_, DatabasePrivileges>(&format!(
|
|
||||||
"SELECT {} FROM `db` WHERE `db` = ?",
|
|
||||||
DATABASE_PRIVILEGE_FIELDS
|
|
||||||
.iter()
|
|
||||||
.map(|field| quote_identifier(field))
|
|
||||||
.join(","),
|
|
||||||
))
|
|
||||||
.bind(database_name)
|
|
||||||
.fetch_all(conn)
|
|
||||||
.await
|
|
||||||
.context("Failed to show database")?;
|
|
||||||
|
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_database_privileges(
|
|
||||||
conn: &mut MySqlConnection,
|
|
||||||
) -> anyhow::Result<Vec<DatabasePrivileges>> {
|
|
||||||
let unix_user = get_current_unix_user()?;
|
|
||||||
|
|
||||||
let result = sqlx::query_as::<_, DatabasePrivileges>(&format!(
|
|
||||||
indoc! {r#"
|
|
||||||
SELECT {} FROM `db` WHERE `db` IN
|
|
||||||
(SELECT DISTINCT `SCHEMA_NAME` AS `database`
|
|
||||||
FROM `information_schema`.`SCHEMATA`
|
|
||||||
WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
|
||||||
AND `SCHEMA_NAME` REGEXP ?)
|
|
||||||
"#},
|
|
||||||
DATABASE_PRIVILEGE_FIELDS
|
|
||||||
.iter()
|
|
||||||
.map(|field| format!("`{field}`"))
|
|
||||||
.join(","),
|
|
||||||
))
|
|
||||||
.bind(create_user_group_matching_regex(&unix_user))
|
|
||||||
.fetch_all(conn)
|
|
||||||
.await
|
|
||||||
.context("Failed to show databases")?;
|
|
||||||
|
|
||||||
Ok(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct DatabasePrivilegeDiffList {
|
|
||||||
pub db: String,
|
|
||||||
pub user: String,
|
|
||||||
pub diff: Vec<DatabasePrivilegeDiff>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum DatabasePrivilegeDiff {
|
|
||||||
YesToNo(String),
|
|
||||||
NoToYes(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diff_single_priv(p1: bool, p2: bool, name: &str) -> Option<DatabasePrivilegeDiff> {
|
|
||||||
match (p1, p2) {
|
|
||||||
(true, false) => Some(DatabasePrivilegeDiff::YesToNo(name.to_owned())),
|
|
||||||
(false, true) => Some(DatabasePrivilegeDiff::NoToYes(name.to_owned())),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub enum DatabasePrivilegesDiff {
|
|
||||||
New(DatabasePrivileges),
|
|
||||||
Modified(DatabasePrivilegeDiffList),
|
|
||||||
Deleted(DatabasePrivileges),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn diff_permissions(
|
|
||||||
from: Vec<DatabasePrivileges>,
|
|
||||||
to: &[DatabasePrivileges],
|
|
||||||
) -> Vec<DatabasePrivilegesDiff> {
|
|
||||||
let from_lookup_table: HashMap<(String, String), DatabasePrivileges> = 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(
|
|
||||||
to.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|p| ((p.db.clone(), p.user.clone()), p)),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut result = vec![];
|
|
||||||
|
|
||||||
for p in to {
|
|
||||||
if let Some(old_p) = from_lookup_table.get(&(p.db.clone(), p.user.clone())) {
|
|
||||||
let diff = old_p.diff(p);
|
|
||||||
if !diff.diff.is_empty() {
|
|
||||||
result.push(DatabasePrivilegesDiff::Modified(diff));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.push(DatabasePrivilegesDiff::New(p.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for p in from {
|
|
||||||
if !to_lookup_table.contains_key(&(p.db.clone(), p.user.clone())) {
|
|
||||||
result.push(DatabasePrivilegesDiff::Deleted(p));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn apply_permission_diffs(
|
|
||||||
diffs: Vec<DatabasePrivilegesDiff>,
|
|
||||||
conn: &mut MySqlConnection,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
for diff in diffs {
|
|
||||||
match diff {
|
|
||||||
DatabasePrivilegesDiff::New(p) => {
|
|
||||||
let tables = DATABASE_PRIVILEGE_FIELDS
|
|
||||||
.iter()
|
|
||||||
.map(|field| format!("`{field}`"))
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
let question_marks = std::iter::repeat("?")
|
|
||||||
.take(DATABASE_PRIVILEGE_FIELDS.len())
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
sqlx::query(
|
|
||||||
format!("INSERT INTO `db` ({}) VALUES ({})", tables, question_marks).as_str(),
|
|
||||||
)
|
|
||||||
.bind(p.db)
|
|
||||||
.bind(p.user)
|
|
||||||
.bind(yn(p.select_priv))
|
|
||||||
.bind(yn(p.insert_priv))
|
|
||||||
.bind(yn(p.update_priv))
|
|
||||||
.bind(yn(p.delete_priv))
|
|
||||||
.bind(yn(p.create_priv))
|
|
||||||
.bind(yn(p.drop_priv))
|
|
||||||
.bind(yn(p.alter_priv))
|
|
||||||
.bind(yn(p.index_priv))
|
|
||||||
.bind(yn(p.create_tmp_table_priv))
|
|
||||||
.bind(yn(p.lock_tables_priv))
|
|
||||||
.bind(yn(p.references_priv))
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
DatabasePrivilegesDiff::Modified(p) => {
|
|
||||||
let tables = p
|
|
||||||
.diff
|
|
||||||
.iter()
|
|
||||||
.map(|diff| match diff {
|
|
||||||
DatabasePrivilegeDiff::YesToNo(name) => format!("`{}` = 'N'", name),
|
|
||||||
DatabasePrivilegeDiff::NoToYes(name) => format!("`{}` = 'Y'", name),
|
|
||||||
})
|
|
||||||
.join(",");
|
|
||||||
|
|
||||||
sqlx::query(
|
|
||||||
format!("UPDATE `db` SET {} WHERE `db` = ? AND `user` = ?", tables).as_str(),
|
|
||||||
)
|
|
||||||
.bind(p.db)
|
|
||||||
.bind(p.user)
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
DatabasePrivilegesDiff::Deleted(p) => {
|
|
||||||
sqlx::query("DELETE FROM `db` WHERE `db` = ? AND `user` = ?")
|
|
||||||
.bind(p.db)
|
|
||||||
.bind(p.user)
|
|
||||||
.execute(&mut *conn)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// NOTE: It is very critical that this function validates the database name
|
/// NOTE: It is very critical that this function validates the database name
|
||||||
/// properly. MySQL does not seem to allow for prepared statements, binding
|
/// properly. MySQL does not seem to allow for prepared statements, binding
|
||||||
/// the database name as a parameter to the query. This means that we have
|
/// the database name as a parameter to the query. This means that we have
|
||||||
|
@ -397,72 +117,3 @@ pub fn validate_database_name(name: &str, user: &User) -> anyhow::Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_diff_single_priv() {
|
|
||||||
assert_eq!(
|
|
||||||
diff_single_priv(true, false, "test"),
|
|
||||||
Some(DatabasePrivilegeDiff::YesToNo("test".to_owned()))
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
diff_single_priv(false, true, "test"),
|
|
||||||
Some(DatabasePrivilegeDiff::NoToYes("test".to_owned()))
|
|
||||||
);
|
|
||||||
assert_eq!(diff_single_priv(true, true, "test"), None);
|
|
||||||
assert_eq!(diff_single_priv(false, false, "test"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_diff_permissions() {
|
|
||||||
let from = vec![DatabasePrivileges {
|
|
||||||
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,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let to = vec![DatabasePrivileges {
|
|
||||||
db: "db".to_owned(),
|
|
||||||
user: "user".to_owned(),
|
|
||||||
select_priv: false,
|
|
||||||
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,
|
|
||||||
}];
|
|
||||||
|
|
||||||
let diffs = diff_permissions(from, &to).await;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
diffs,
|
|
||||||
vec![DatabasePrivilegesDiff::Modified(
|
|
||||||
DatabasePrivilegeDiffList {
|
|
||||||
db: "db".to_owned(),
|
|
||||||
user: "user".to_owned(),
|
|
||||||
diff: vec![DatabasePrivilegeDiff::YesToNo("select_priv".to_owned())],
|
|
||||||
}
|
|
||||||
)]
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(matches!(&diffs[0], DatabasePrivilegesDiff::Modified(_)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,683 @@
|
||||||
|
//! 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 privilege 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::{anyhow, Context};
|
||||||
|
use indoc::indoc;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{mysql::MySqlRow, prelude::*, MySqlConnection};
|
||||||
|
|
||||||
|
use crate::core::{
|
||||||
|
common::{
|
||||||
|
create_user_group_matching_regex, get_current_unix_user, quote_identifier, rev_yn, yn,
|
||||||
|
},
|
||||||
|
database_operations::validate_database_name,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// This is the list of fields that are used to fetch the db + user + privileges
|
||||||
|
/// from the `db` table in the database. If you need to add or remove privilege
|
||||||
|
/// fields, this is a good place to start.
|
||||||
|
pub const DATABASE_PRIVILEGE_FIELDS: [&str; 13] = [
|
||||||
|
"db",
|
||||||
|
"user",
|
||||||
|
"select_priv",
|
||||||
|
"insert_priv",
|
||||||
|
"update_priv",
|
||||||
|
"delete_priv",
|
||||||
|
"create_priv",
|
||||||
|
"drop_priv",
|
||||||
|
"alter_priv",
|
||||||
|
"index_priv",
|
||||||
|
"create_tmp_table_priv",
|
||||||
|
"lock_tables_priv",
|
||||||
|
"references_priv",
|
||||||
|
];
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This struct represents the set of privileges for a single user on a single database.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct DatabasePrivilegeRow {
|
||||||
|
pub db: String,
|
||||||
|
pub user: String,
|
||||||
|
pub select_priv: bool,
|
||||||
|
pub insert_priv: bool,
|
||||||
|
pub update_priv: bool,
|
||||||
|
pub delete_priv: bool,
|
||||||
|
pub create_priv: bool,
|
||||||
|
pub drop_priv: bool,
|
||||||
|
pub alter_priv: bool,
|
||||||
|
pub index_priv: bool,
|
||||||
|
pub create_tmp_table_priv: bool,
|
||||||
|
pub lock_tables_priv: bool,
|
||||||
|
pub references_priv: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabasePrivilegeRow {
|
||||||
|
pub fn get_privilege_by_name(&self, name: &str) -> bool {
|
||||||
|
match name {
|
||||||
|
"select_priv" => self.select_priv,
|
||||||
|
"insert_priv" => self.insert_priv,
|
||||||
|
"update_priv" => self.update_priv,
|
||||||
|
"delete_priv" => self.delete_priv,
|
||||||
|
"create_priv" => self.create_priv,
|
||||||
|
"drop_priv" => self.drop_priv,
|
||||||
|
"alter_priv" => self.alter_priv,
|
||||||
|
"index_priv" => self.index_priv,
|
||||||
|
"create_tmp_table_priv" => self.create_tmp_table_priv,
|
||||||
|
"lock_tables_priv" => self.lock_tables_priv,
|
||||||
|
"references_priv" => self.references_priv,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diff(&self, other: &DatabasePrivilegeRow) -> DatabasePrivilegeRowDiff {
|
||||||
|
debug_assert!(self.db == other.db && self.user == other.user);
|
||||||
|
|
||||||
|
DatabasePrivilegeRowDiff {
|
||||||
|
db: self.db.clone(),
|
||||||
|
user: self.user.clone(),
|
||||||
|
diff: DATABASE_PRIVILEGE_FIELDS
|
||||||
|
.into_iter()
|
||||||
|
.skip(2)
|
||||||
|
.filter_map(|field| {
|
||||||
|
DatabasePrivilegeChange::new(
|
||||||
|
self.get_privilege_by_name(field),
|
||||||
|
other.get_privilege_by_name(field),
|
||||||
|
field,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_row_priv_field(row: &MySqlRow, field: &str) -> Result<bool, sqlx::Error> {
|
||||||
|
match rev_yn(row.try_get(field)?) {
|
||||||
|
Some(val) => Ok(val),
|
||||||
|
_ => {
|
||||||
|
log::warn!("Invalid value for privilege: {}", field);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromRow<'_, MySqlRow> for DatabasePrivilegeRow {
|
||||||
|
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
db: row.try_get("db")?,
|
||||||
|
user: row.try_get("user")?,
|
||||||
|
select_priv: get_row_priv_field(row, "select_priv")?,
|
||||||
|
insert_priv: get_row_priv_field(row, "insert_priv")?,
|
||||||
|
update_priv: get_row_priv_field(row, "update_priv")?,
|
||||||
|
delete_priv: get_row_priv_field(row, "delete_priv")?,
|
||||||
|
create_priv: get_row_priv_field(row, "create_priv")?,
|
||||||
|
drop_priv: get_row_priv_field(row, "drop_priv")?,
|
||||||
|
alter_priv: get_row_priv_field(row, "alter_priv")?,
|
||||||
|
index_priv: get_row_priv_field(row, "index_priv")?,
|
||||||
|
create_tmp_table_priv: get_row_priv_field(row, "create_tmp_table_priv")?,
|
||||||
|
lock_tables_priv: get_row_priv_field(row, "lock_tables_priv")?,
|
||||||
|
references_priv: get_row_priv_field(row, "references_priv")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_database_privileges(
|
||||||
|
database_name: &str,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||||
|
let unix_user = get_current_unix_user()?;
|
||||||
|
validate_database_name(database_name, &unix_user)?;
|
||||||
|
|
||||||
|
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
|
||||||
|
"SELECT {} FROM `db` WHERE `db` = ?",
|
||||||
|
DATABASE_PRIVILEGE_FIELDS
|
||||||
|
.iter()
|
||||||
|
.map(|field| quote_identifier(field))
|
||||||
|
.join(","),
|
||||||
|
))
|
||||||
|
.bind(database_name)
|
||||||
|
.fetch_all(connection)
|
||||||
|
.await
|
||||||
|
.context("Failed to show database")?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_database_privileges(
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||||
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
|
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
|
||||||
|
indoc! {r#"
|
||||||
|
SELECT {} FROM `db` WHERE `db` IN
|
||||||
|
(SELECT DISTINCT `SCHEMA_NAME` AS `database`
|
||||||
|
FROM `information_schema`.`SCHEMATA`
|
||||||
|
WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
||||||
|
AND `SCHEMA_NAME` REGEXP ?)
|
||||||
|
"#},
|
||||||
|
DATABASE_PRIVILEGE_FIELDS
|
||||||
|
.iter()
|
||||||
|
.map(|field| format!("`{field}`"))
|
||||||
|
.join(","),
|
||||||
|
))
|
||||||
|
.bind(create_user_group_matching_regex(&unix_user))
|
||||||
|
.fetch_all(connection)
|
||||||
|
.await
|
||||||
|
.context("Failed to show databases")?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*************************/
|
||||||
|
/* CLI INTERFACE PARSING */
|
||||||
|
/*************************/
|
||||||
|
|
||||||
|
/// See documentation for `DatabaseCommand::EditDbPrivs`.
|
||||||
|
pub 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************/
|
||||||
|
/* EDITOR CONTENT PARSING/DISPLAY */
|
||||||
|
/**********************************/
|
||||||
|
|
||||||
|
// TODO: merge with `rev_yn` in `common.rs`
|
||||||
|
|
||||||
|
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)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_privilege_data_from_editor_content(
|
||||||
|
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>>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates a single row of the privileges table for the editor.
|
||||||
|
pub 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
) -> String {
|
||||||
|
let example_user = format!("{}_user", unix_user);
|
||||||
|
let example_db = format!("{}_db", unix_user);
|
||||||
|
|
||||||
|
// 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 = 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,
|
||||||
|
);
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"{}\n{}\n{}",
|
||||||
|
EDITOR_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")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*****************************/
|
||||||
|
/* 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, Serialize, Deserialize)]
|
||||||
|
pub struct DatabasePrivilegeRowDiff {
|
||||||
|
pub db: String,
|
||||||
|
pub user: String,
|
||||||
|
pub diff: Vec<DatabasePrivilegeChange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This enum represents a change for a single privilege.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum DatabasePrivilegeChange {
|
||||||
|
YesToNo(String),
|
||||||
|
NoToYes(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DatabasePrivilegeChange {
|
||||||
|
pub fn new(p1: bool, p2: bool, name: &str) -> Option<DatabasePrivilegeChange> {
|
||||||
|
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, Serialize, Deserialize)]
|
||||||
|
pub enum DatabasePrivilegesDiff {
|
||||||
|
New(DatabasePrivilegeRow),
|
||||||
|
Modified(DatabasePrivilegeRowDiff),
|
||||||
|
Deleted(DatabasePrivilegeRow),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn diff_privileges(
|
||||||
|
from: Vec<DatabasePrivilegeRow>,
|
||||||
|
to: &[DatabasePrivilegeRow],
|
||||||
|
) -> Vec<DatabasePrivilegesDiff> {
|
||||||
|
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 = vec![];
|
||||||
|
|
||||||
|
for p in to {
|
||||||
|
if let Some(old_p) = from_lookup_table.get(&(p.db.clone(), p.user.clone())) {
|
||||||
|
let diff = old_p.diff(p);
|
||||||
|
if !diff.diff.is_empty() {
|
||||||
|
result.push(DatabasePrivilegesDiff::Modified(diff));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push(DatabasePrivilegesDiff::New(p.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in from {
|
||||||
|
if !to_lookup_table.contains_key(&(p.db.clone(), p.user.clone())) {
|
||||||
|
result.push(DatabasePrivilegesDiff::Deleted(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uses the resulting diffs to make modifications to the database.
|
||||||
|
pub async fn apply_privilege_diffs(
|
||||||
|
diffs: Vec<DatabasePrivilegesDiff>,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for diff in diffs {
|
||||||
|
match diff {
|
||||||
|
DatabasePrivilegesDiff::New(p) => {
|
||||||
|
let tables = DATABASE_PRIVILEGE_FIELDS
|
||||||
|
.iter()
|
||||||
|
.map(|field| format!("`{field}`"))
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
let question_marks = std::iter::repeat("?")
|
||||||
|
.take(DATABASE_PRIVILEGE_FIELDS.len())
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
format!("INSERT INTO `db` ({}) VALUES ({})", tables, question_marks).as_str(),
|
||||||
|
)
|
||||||
|
.bind(p.db)
|
||||||
|
.bind(p.user)
|
||||||
|
.bind(yn(p.select_priv))
|
||||||
|
.bind(yn(p.insert_priv))
|
||||||
|
.bind(yn(p.update_priv))
|
||||||
|
.bind(yn(p.delete_priv))
|
||||||
|
.bind(yn(p.create_priv))
|
||||||
|
.bind(yn(p.drop_priv))
|
||||||
|
.bind(yn(p.alter_priv))
|
||||||
|
.bind(yn(p.index_priv))
|
||||||
|
.bind(yn(p.create_tmp_table_priv))
|
||||||
|
.bind(yn(p.lock_tables_priv))
|
||||||
|
.bind(yn(p.references_priv))
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
DatabasePrivilegesDiff::Modified(p) => {
|
||||||
|
let tables = p
|
||||||
|
.diff
|
||||||
|
.iter()
|
||||||
|
.map(|diff| match diff {
|
||||||
|
DatabasePrivilegeChange::YesToNo(name) => format!("`{}` = 'N'", name),
|
||||||
|
DatabasePrivilegeChange::NoToYes(name) => format!("`{}` = 'Y'", name),
|
||||||
|
})
|
||||||
|
.join(",");
|
||||||
|
|
||||||
|
sqlx::query(
|
||||||
|
format!("UPDATE `db` SET {} WHERE `db` = ? AND `user` = ?", tables).as_str(),
|
||||||
|
)
|
||||||
|
.bind(p.db)
|
||||||
|
.bind(p.user)
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
DatabasePrivilegesDiff::Deleted(p) => {
|
||||||
|
sqlx::query("DELETE FROM `db` WHERE `db` = ? AND `user` = ?")
|
||||||
|
.bind(p.db)
|
||||||
|
.bind(p.user)
|
||||||
|
.execute(&mut *connection)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/*********/
|
||||||
|
/* 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 from = 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: false,
|
||||||
|
create_tmp_table_priv: true,
|
||||||
|
lock_tables_priv: true,
|
||||||
|
references_priv: false,
|
||||||
|
}];
|
||||||
|
|
||||||
|
let mut to = from.clone();
|
||||||
|
to[0].select_priv = false;
|
||||||
|
to[0].insert_priv = false;
|
||||||
|
to[0].index_priv = true;
|
||||||
|
|
||||||
|
let diffs = diff_privileges(from, &to);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
diffs,
|
||||||
|
vec![DatabasePrivilegesDiff::Modified(DatabasePrivilegeRowDiff {
|
||||||
|
db: "db".to_owned(),
|
||||||
|
user: "user".to_owned(),
|
||||||
|
diff: vec![
|
||||||
|
DatabasePrivilegeChange::YesToNo("select_priv".to_owned()),
|
||||||
|
DatabasePrivilegeChange::YesToNo("insert_priv".to_owned()),
|
||||||
|
DatabasePrivilegeChange::NoToYes("index_priv".to_owned()),
|
||||||
|
],
|
||||||
|
})]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(matches!(&diffs[0], DatabasePrivilegesDiff::Modified(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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");
|
||||||
|
|
||||||
|
let parsed_permissions = parse_privilege_data_from_editor_content(content).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(permissions, parsed_permissions);
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,7 +10,7 @@ use super::common::{
|
||||||
validate_ownership_by_user_prefix,
|
validate_ownership_by_user_prefix,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn user_exists(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<bool> {
|
pub async fn user_exists(db_user: &str, connection: &mut MySqlConnection) -> anyhow::Result<bool> {
|
||||||
let unix_user = get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
validate_user_name(db_user, &unix_user)?;
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
@ -25,42 +25,48 @@ pub async fn user_exists(db_user: &str, conn: &mut MySqlConnection) -> anyhow::R
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(db_user)
|
.bind(db_user)
|
||||||
.fetch_one(conn)
|
.fetch_one(connection)
|
||||||
.await?
|
.await?
|
||||||
.get::<bool, _>(0);
|
.get::<bool, _>(0);
|
||||||
|
|
||||||
Ok(user_exists)
|
Ok(user_exists)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_database_user(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
pub async fn create_database_user(
|
||||||
|
db_user: &str,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let unix_user = get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
validate_user_name(db_user, &unix_user)?;
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
|
||||||
if user_exists(db_user, conn).await? {
|
if user_exists(db_user, connection).await? {
|
||||||
anyhow::bail!("User '{}' already exists", db_user);
|
anyhow::bail!("User '{}' already exists", db_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
|
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
|
||||||
sqlx::query(format!("CREATE USER {}@'%'", quote_literal(db_user),).as_str())
|
sqlx::query(format!("CREATE USER {}@'%'", quote_literal(db_user),).as_str())
|
||||||
.execute(conn)
|
.execute(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_database_user(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
|
pub async fn delete_database_user(
|
||||||
|
db_user: &str,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
let unix_user = get_current_unix_user()?;
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
validate_user_name(db_user, &unix_user)?;
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
|
||||||
if !user_exists(db_user, conn).await? {
|
if !user_exists(db_user, connection).await? {
|
||||||
anyhow::bail!("User '{}' does not exist", db_user);
|
anyhow::bail!("User '{}' does not exist", db_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
|
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
|
||||||
sqlx::query(format!("DROP USER {}@'%'", quote_literal(db_user),).as_str())
|
sqlx::query(format!("DROP USER {}@'%'", quote_literal(db_user),).as_str())
|
||||||
.execute(conn)
|
.execute(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -69,12 +75,12 @@ pub async fn delete_database_user(db_user: &str, conn: &mut MySqlConnection) ->
|
||||||
pub async fn set_password_for_database_user(
|
pub async fn set_password_for_database_user(
|
||||||
db_user: &str,
|
db_user: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
conn: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let unix_user = crate::core::common::get_current_unix_user()?;
|
let unix_user = crate::core::common::get_current_unix_user()?;
|
||||||
validate_user_name(db_user, &unix_user)?;
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
|
||||||
if !user_exists(db_user, conn).await? {
|
if !user_exists(db_user, connection).await? {
|
||||||
anyhow::bail!("User '{}' does not exist", db_user);
|
anyhow::bail!("User '{}' does not exist", db_user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +93,7 @@ pub async fn set_password_for_database_user(
|
||||||
)
|
)
|
||||||
.as_str(),
|
.as_str(),
|
||||||
)
|
)
|
||||||
.execute(conn)
|
.execute(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -100,6 +106,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,
|
||||||
|
@ -112,7 +119,7 @@ pub struct DatabaseUser {
|
||||||
/// unix username and group names of the given unix user.
|
/// unix username and group names of the given unix user.
|
||||||
pub async fn get_all_database_users_for_unix_user(
|
pub async fn get_all_database_users_for_unix_user(
|
||||||
unix_user: &User,
|
unix_user: &User,
|
||||||
conn: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<Vec<DatabaseUser>> {
|
) -> anyhow::Result<Vec<DatabaseUser>> {
|
||||||
let users = sqlx::query_as::<_, DatabaseUser>(
|
let users = sqlx::query_as::<_, DatabaseUser>(
|
||||||
r#"
|
r#"
|
||||||
|
@ -125,7 +132,7 @@ pub async fn get_all_database_users_for_unix_user(
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(create_user_group_matching_regex(unix_user))
|
.bind(create_user_group_matching_regex(unix_user))
|
||||||
.fetch_all(conn)
|
.fetch_all(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(users)
|
Ok(users)
|
||||||
|
@ -134,7 +141,7 @@ pub async fn get_all_database_users_for_unix_user(
|
||||||
/// This function fetches a database user if it exists.
|
/// This function fetches a database user if it exists.
|
||||||
pub async fn get_database_user_for_user(
|
pub async fn get_database_user_for_user(
|
||||||
username: &str,
|
username: &str,
|
||||||
conn: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<Option<DatabaseUser>> {
|
) -> anyhow::Result<Option<DatabaseUser>> {
|
||||||
let user = sqlx::query_as::<_, DatabaseUser>(
|
let user = sqlx::query_as::<_, DatabaseUser>(
|
||||||
r#"
|
r#"
|
||||||
|
@ -147,7 +154,7 @@ pub async fn get_database_user_for_user(
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(username)
|
.bind(username)
|
||||||
.fetch_optional(conn)
|
.fetch_optional(connection)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
|
|
Loading…
Reference in New Issue