diff --git a/src/cli/database_command.rs b/src/cli/database_command.rs index 1d1eaa4..df05ae0 100644 --- a/src/cli/database_command.rs +++ b/src/cli/database_command.rs @@ -366,7 +366,7 @@ pub async fn edit_database_privileges( let privileges_to_change = if !args.privs.is_empty() { parse_privilege_tables_from_args(&args)? } else { - edit_privileges_with_editor(&privilege_data)? + edit_privileges_with_editor(&privilege_data, args.name.as_deref())? }; let diffs = diff_privileges(&privilege_data, &privileges_to_change); @@ -432,13 +432,14 @@ fn parse_privilege_tables_from_args( fn edit_privileges_with_editor( privilege_data: &[DatabasePrivilegeRow], + database_name: Option<&str>, ) -> anyhow::Result> { let unix_user = User::from_uid(getuid()) .context("Failed to look up your UNIX username") .and_then(|u| u.ok_or(anyhow::anyhow!("Failed to look up your UNIX username")))?; let editor_content = - generate_editor_content_from_privilege_data(privilege_data, &unix_user.name); + generate_editor_content_from_privilege_data(privilege_data, &unix_user.name, database_name); // TODO: handle errors better here let result = Editor::new().extension("tsv").edit(&editor_content)?; diff --git a/src/cli/user_command.rs b/src/cli/user_command.rs index 2b841e8..2a5eaf3 100644 --- a/src/cli/user_command.rs +++ b/src/cli/user_command.rs @@ -290,14 +290,14 @@ async fn show_users( "User", "Password is set", "Locked", - // "Databases where user has privileges" + "Databases where user has privileges" ]); for user in users { table.add_row(row![ user.user, user.has_password, user.is_locked, - // user.databases.join("\n") + user.databases.join("\n") ]); } table.printstd(); diff --git a/src/core/database_privileges.rs b/src/core/database_privileges.rs index f8df3d4..847108f 100644 --- a/src/core/database_privileges.rs +++ b/src/core/database_privileges.rs @@ -157,9 +157,12 @@ const EDITOR_COMMENT: &str = r#" pub fn generate_editor_content_from_privilege_data( privilege_data: &[DatabasePrivilegeRow], unix_user: &str, + database_name: Option<&str>, ) -> String { let example_user = format!("{}_user", unix_user); - let example_db = format!("{}_db", unix_user); + let example_db = database_name + .unwrap_or(&format!("{}_db", unix_user)) + .to_string(); // NOTE: `.max()`` fails when the iterator is empty. // In this case, we know that the only fields in the @@ -539,7 +542,7 @@ pub fn display_privilege_diffs(diffs: &BTreeSet) -> Stri table.add_row(row![ p.db, p.user, - "(New user)\n".to_string() + &display_new_privileges_list(p) + "(Previously unprivileged)\n".to_string() + &display_new_privileges_list(p) ]); } DatabasePrivilegesDiff::Modified(p) => { @@ -664,7 +667,7 @@ mod tests { }, ]; - let content = generate_editor_content_from_privilege_data(&permissions, "user"); + let content = generate_editor_content_from_privilege_data(&permissions, "user", None); let parsed_permissions = parse_privilege_data_from_editor_content(content).unwrap(); diff --git a/src/core/protocol/server_responses.rs b/src/core/protocol/server_responses.rs index 23b89c2..e5f56ec 100644 --- a/src/core/protocol/server_responses.rs +++ b/src/core/protocol/server_responses.rs @@ -95,8 +95,7 @@ impl OwnerValidationError { .join("\n"), ) .to_owned(), - - _ => format!( + OwnerValidationError::StringEmpty => format!( "'{}' is not a valid {} name.", name, db_or_user.lowercased() @@ -113,12 +112,6 @@ pub enum OwnerValidationError { // The name is empty, which is invalid StringEmpty, - - // The name is in the format "_", which is invalid - MissingPrefix, - - // The name is in the format "_", which is invalid - MissingPostfix, } pub type CreateDatabasesOutput = BTreeMap>; diff --git a/src/server/input_sanitization.rs b/src/server/input_sanitization.rs index 6ce5201..3026f05 100644 --- a/src/server/input_sanitization.rs +++ b/src/server/input_sanitization.rs @@ -43,18 +43,14 @@ pub fn validate_ownership_by_prefixes( return Err(OwnerValidationError::StringEmpty); } - if name.starts_with('_') { - return Err(OwnerValidationError::MissingPrefix); - } - - let (prefix, _) = match name.split_once('_') { - Some(pair) => pair, - None => return Err(OwnerValidationError::MissingPostfix), - }; - - if !prefixes.iter().any(|g| g == prefix) { + if prefixes + .iter() + .filter(|p| name.starts_with(*p)) + .collect::>() + .is_empty() + { return Err(OwnerValidationError::NoMatch); - } + }; Ok(()) } @@ -115,24 +111,6 @@ mod tests { Err(OwnerValidationError::StringEmpty) ); - assert_eq!( - validate_ownership_by_prefixes("user", &prefixes), - Err(OwnerValidationError::MissingPostfix) - ); - assert_eq!( - validate_ownership_by_prefixes("something", &prefixes), - Err(OwnerValidationError::MissingPostfix) - ); - assert_eq!( - validate_ownership_by_prefixes("user-testdb", &prefixes), - Err(OwnerValidationError::MissingPostfix) - ); - - assert_eq!( - validate_ownership_by_prefixes("_testdb", &prefixes), - Err(OwnerValidationError::MissingPrefix) - ); - assert_eq!( validate_ownership_by_prefixes("user_testdb", &prefixes), Ok(()) diff --git a/src/server/sql/user_operations.rs b/src/server/sql/user_operations.rs index 4c83692..c73333e 100644 --- a/src/server/sql/user_operations.rs +++ b/src/server/sql/user_operations.rs @@ -1,4 +1,6 @@ +use itertools::Itertools; use std::collections::BTreeMap; +use indoc::formatdoc; use serde::{Deserialize, Serialize}; @@ -20,6 +22,8 @@ use crate::{ }, }; +use super::database_privilege_operations::DATABASE_PRIVILEGE_FIELDS; + // NOTE: this function is unsafe because it does no input validation. async fn unsafe_user_exists( db_user: &str, @@ -309,6 +313,9 @@ pub struct DatabaseUser { #[sqlx(rename = "is_locked")] pub is_locked: bool, + + #[sqlx(skip)] + pub databases: Vec, } const DB_USER_SELECT_STATEMENT: &str = r#" @@ -344,13 +351,17 @@ pub async fn list_database_users( continue; } - let result = sqlx::query_as::<_, DatabaseUser>( + let mut result = sqlx::query_as::<_, DatabaseUser>( &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` = ?"), ) .bind(&db_user) .fetch_optional(&mut *connection) .await; + if let Ok(Some(user)) = result.as_mut() { + append_databases_where_user_has_privileges(user, &mut *connection).await; + } + match result { Ok(Some(user)) => results.insert(db_user, Ok(user)), Ok(None) => results.insert(db_user, Err(ListUsersError::UserDoesNotExist)), @@ -365,11 +376,50 @@ pub async fn list_all_database_users_for_unix_user( unix_user: &UnixUser, connection: &mut MySqlConnection, ) -> ListAllUsersOutput { - sqlx::query_as::<_, DatabaseUser>( + let mut result = sqlx::query_as::<_, DatabaseUser>( &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` REGEXP ?"), ) .bind(create_user_group_matching_regex(unix_user)) - .fetch_all(connection) + .fetch_all(&mut *connection) .await - .map_err(|err| ListAllUsersError::MySqlError(err.to_string())) + .map_err(|err| ListAllUsersError::MySqlError(err.to_string())); + + if let Ok(users) = result.as_mut() { + for user in users { + append_databases_where_user_has_privileges(user, &mut *connection).await; + } + } + + result +} + +pub async fn append_databases_where_user_has_privileges( + database_user: &mut DatabaseUser, + connection: &mut MySqlConnection, +) { + let database_list = 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(database_user.user.clone()) + .fetch_all(&mut *connection) + .await; + + database_user.databases = database_list + .map(|rows| { + rows.into_iter() + .map(|row| row.get::("database")) + .collect() + }) + .unwrap_or_default(); }