diff --git a/Cargo.lock b/Cargo.lock index 60fbe8d..58fa798 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1364,6 +1364,7 @@ version = "1.0.116" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" dependencies = [ + "indexmap", "itoa", "ryu", "serde", diff --git a/Cargo.toml b/Cargo.toml index 216e6c8..9242ae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,7 @@ nix = { version = "0.28.0", features = ["user"] } prettytable = "0.10.0" ratatui = { version = "0.26.2", optional = true } 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"] } tokio = { version = "1.37.0", features = ["rt", "macros"] } toml = "0.8.12" diff --git a/src/cli/user_command.rs b/src/cli/user_command.rs index 3db7e80..7f8ccd4 100644 --- a/src/cli/user_command.rs +++ b/src/cli/user_command.rs @@ -1,11 +1,18 @@ +use std::collections::BTreeMap; use std::vec; use anyhow::Context; use clap::Parser; use dialoguer::{Confirm, Password}; +use prettytable::Table; +use serde_json::json; use sqlx::{Connection, MySqlConnection}; -use crate::core::{common::close_database_connection, user_operations::validate_user_name}; +use crate::core::{ + common::close_database_connection, + database_operations::get_databases_where_user_has_privileges, + user_operations::validate_user_name, +}; #[derive(Parser)] pub struct UserArgs { @@ -196,20 +203,47 @@ async fn show_users(args: UserShowArgs, conn: &mut MySqlConnection) -> anyhow::R result }; + let mut user_databases: BTreeMap> = BTreeMap::new(); + for user in users.iter() { + user_databases.insert( + user.user.clone(), + get_databases_where_user_has_privileges(&user.user, conn).await?, + ); + } + if args.json { - println!("{}", serde_json::to_string_pretty(&users)?); + 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::(); + 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 { - println!( - "User '{}': {}", - &user.user, - if user.has_password { - "password set." - } else { - "no password set." - } - ); + table.add_row(row![ + user.user, + user.has_password, + user_databases.get(&user.user).unwrap_or(&vec![]).join("\n") + ]); } + table.printstd(); } Ok(()) diff --git a/src/core/database_operations.rs b/src/core/database_operations.rs index 744e843..f0084b9 100644 --- a/src/core/database_operations.rs +++ b/src/core/database_operations.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use anyhow::Context; -use indoc::indoc; +use indoc::{formatdoc, indoc}; use itertools::Itertools; use nix::unistd::User; use serde::{Deserialize, Serialize}; @@ -77,6 +77,37 @@ pub async fn get_database_list(conn: &mut MySqlConnection) -> anyhow::Result anyhow::Result> { + let result = sqlx::query( + formatdoc!( + r#" + SELECT `db` AS `database` + FROM `db` + WHERE `user` = ? + AND ({}) + "#, + DATABASE_PRIVILEGE_FIELDS + .iter() + .map(|field| format!("`{}` = 'Y'", field)) + .join(" OR "), + ) + .as_str(), + ) + .bind(username) + .fetch_all(conn) + .await? + .into_iter() + .map(|databases| { + databases.try_get::("database").unwrap() + }) + .collect(); + + Ok(result) +} + pub const DATABASE_PRIVILEGE_FIELDS: [&str; 13] = [ "db", "user",