From 805c2d11ff95fc96f43f56eaea4d1a2634d0d74d Mon Sep 17 00:00:00 2001 From: h7x4 Date: Wed, 26 Nov 2025 02:41:28 +0900 Subject: [PATCH] core/protocol: split commands into separate files --- src/client/commands/create_user.rs | 4 +- src/client/commands/passwd_user.rs | 4 +- .../error_messages.rs | 4 +- .../mysql_useradm.rs | 4 +- src/core/protocol.rs | 7 +- src/core/protocol/commands.rs | 105 +++ .../protocol/commands/create_databases.rs | 76 ++ src/core/protocol/commands/create_users.rs | 74 ++ src/core/protocol/commands/drop_databases.rs | 79 ++ src/core/protocol/commands/drop_users.rs | 72 ++ .../protocol/commands/list_all_databases.rs | 18 + .../protocol/commands/list_all_privileges.rs | 19 + src/core/protocol/commands/list_all_users.rs | 18 + src/core/protocol/commands/list_databases.rs | 42 + src/core/protocol/commands/list_privileges.rs | 45 + src/core/protocol/commands/list_users.rs | 40 + src/core/protocol/commands/lock_users.rs | 76 ++ .../protocol/commands/modify_privileges.rs | 107 +++ src/core/protocol/commands/passwd_user.rs | 47 ++ src/core/protocol/commands/unlock_users.rs | 78 ++ src/core/protocol/request_response.rs | 83 -- src/core/protocol/request_validation.rs | 117 +++ src/core/protocol/server_responses.rs | 773 ------------------ src/server/input_sanitization.rs | 2 +- src/server/server_loop.rs | 16 +- src/server/sql/database_operations.rs | 13 +- .../sql/database_privilege_operations.rs | 12 +- src/server/sql/user_operations.rs | 21 +- 28 files changed, 1058 insertions(+), 898 deletions(-) create mode 100644 src/core/protocol/commands.rs create mode 100644 src/core/protocol/commands/create_databases.rs create mode 100644 src/core/protocol/commands/create_users.rs create mode 100644 src/core/protocol/commands/drop_databases.rs create mode 100644 src/core/protocol/commands/drop_users.rs create mode 100644 src/core/protocol/commands/list_all_databases.rs create mode 100644 src/core/protocol/commands/list_all_privileges.rs create mode 100644 src/core/protocol/commands/list_all_users.rs create mode 100644 src/core/protocol/commands/list_databases.rs create mode 100644 src/core/protocol/commands/list_privileges.rs create mode 100644 src/core/protocol/commands/list_users.rs create mode 100644 src/core/protocol/commands/lock_users.rs create mode 100644 src/core/protocol/commands/modify_privileges.rs create mode 100644 src/core/protocol/commands/passwd_user.rs create mode 100644 src/core/protocol/commands/unlock_users.rs delete mode 100644 src/core/protocol/request_response.rs create mode 100644 src/core/protocol/request_validation.rs delete mode 100644 src/core/protocol/server_responses.rs diff --git a/src/client/commands/create_user.rs b/src/client/commands/create_user.rs index 84d7a38..5aa3aaf 100644 --- a/src/client/commands/create_user.rs +++ b/src/client/commands/create_user.rs @@ -70,7 +70,7 @@ pub async fn create_users( .interact()? { let password = read_password_from_stdin_with_double_check(username)?; - let message = Request::PasswdUser(username.to_owned(), password); + let message = Request::PasswdUser((username.to_owned(), password)); if let Err(err) = server_connection.send(message).await { server_connection.close().await.ok(); @@ -78,7 +78,7 @@ pub async fn create_users( } match server_connection.next().await { - Some(Ok(Response::PasswdUser(result))) => { + Some(Ok(Response::SetUserPassword(result))) => { print_set_password_output_status(&result, username) } response => return erroneous_server_response(response), diff --git a/src/client/commands/passwd_user.rs b/src/client/commands/passwd_user.rs index 45a532b..be99df2 100644 --- a/src/client/commands/passwd_user.rs +++ b/src/client/commands/passwd_user.rs @@ -73,7 +73,7 @@ pub async fn passwd_user( read_password_from_stdin_with_double_check(&args.username)? }; - let message = Request::PasswdUser(args.username.to_owned(), password); + let message = Request::PasswdUser((args.username.to_owned(), password)); if let Err(err) = server_connection.send(message).await { server_connection.close().await.ok(); @@ -81,7 +81,7 @@ pub async fn passwd_user( } let result = match server_connection.next().await { - Some(Ok(Response::PasswdUser(result))) => result, + Some(Ok(Response::SetUserPassword(result))) => result, response => return erroneous_server_response(response), }; diff --git a/src/client/mysql_admutils_compatibility/error_messages.rs b/src/client/mysql_admutils_compatibility/error_messages.rs index 76fdc44..f5da3a6 100644 --- a/src/client/mysql_admutils_compatibility/error_messages.rs +++ b/src/client/mysql_admutils_compatibility/error_messages.rs @@ -1,6 +1,6 @@ use crate::core::protocol::{ - CreateDatabaseError, CreateUserError, DbOrUser, DropDatabaseError, DropUserError, - GetDatabasesPrivilegeDataError, ListUsersError, + CreateDatabaseError, CreateUserError, DropDatabaseError, DropUserError, + GetDatabasesPrivilegeDataError, ListUsersError, request_validation::DbOrUser, }; pub fn name_validation_error_to_error_message(name: &str, db_or_user: DbOrUser) -> String { diff --git a/src/client/mysql_admutils_compatibility/mysql_useradm.rs b/src/client/mysql_admutils_compatibility/mysql_useradm.rs index ac19fe3..c17dbc3 100644 --- a/src/client/mysql_admutils_compatibility/mysql_useradm.rs +++ b/src/client/mysql_admutils_compatibility/mysql_useradm.rs @@ -235,10 +235,10 @@ async fn passwd_users( for user in users { let password = read_password_from_stdin_with_double_check(&user.user)?; - let message = Request::PasswdUser(user.user.to_owned(), password); + let message = Request::PasswdUser((user.user.to_owned(), password)); server_connection.send(message).await?; match server_connection.next().await { - Some(Ok(Response::PasswdUser(result))) => match result { + Some(Ok(Response::SetUserPassword(result))) => match result { Ok(()) => println!("Password updated for user '{}'.", &user.user), Err(_) => eprintln!( "{}: Failed to update password for user '{}'.", diff --git a/src/core/protocol.rs b/src/core/protocol.rs index 1048569..2b0e5bb 100644 --- a/src/core/protocol.rs +++ b/src/core/protocol.rs @@ -1,5 +1,4 @@ -pub mod request_response; -pub mod server_responses; +mod commands; +pub mod request_validation; -pub use request_response::*; -pub use server_responses::*; +pub use commands::*; diff --git a/src/core/protocol/commands.rs b/src/core/protocol/commands.rs new file mode 100644 index 0000000..880938e --- /dev/null +++ b/src/core/protocol/commands.rs @@ -0,0 +1,105 @@ +mod create_databases; +mod create_users; +mod drop_databases; +mod drop_users; +mod list_all_databases; +mod list_all_privileges; +mod list_all_users; +mod list_databases; +mod list_privileges; +mod list_users; +mod lock_users; +mod modify_privileges; +mod passwd_user; +mod unlock_users; + +pub use create_databases::*; +pub use create_users::*; +pub use drop_databases::*; +pub use drop_users::*; +pub use list_all_databases::*; +pub use list_all_privileges::*; +pub use list_all_users::*; +pub use list_databases::*; +pub use list_privileges::*; +pub use list_users::*; +pub use lock_users::*; +pub use modify_privileges::*; +pub use passwd_user::*; +pub use unlock_users::*; + +use serde::{Deserialize, Serialize}; +use tokio::net::UnixStream; +use tokio_serde::{Framed as SerdeFramed, formats::Bincode}; +use tokio_util::codec::{Framed, LengthDelimitedCodec}; + +pub type ServerToClientMessageStream = SerdeFramed< + Framed, + Request, + Response, + Bincode, +>; + +pub type ClientToServerMessageStream = SerdeFramed< + Framed, + Response, + Request, + Bincode, +>; + +pub fn create_server_to_client_message_stream(socket: UnixStream) -> ServerToClientMessageStream { + let length_delimited = Framed::new(socket, LengthDelimitedCodec::new()); + tokio_serde::Framed::new(length_delimited, Bincode::default()) +} + +pub fn create_client_to_server_message_stream(socket: UnixStream) -> ClientToServerMessageStream { + let length_delimited = Framed::new(socket, LengthDelimitedCodec::new()); + tokio_serde::Framed::new(length_delimited, Bincode::default()) +} + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Request { + CreateDatabases(CreateDatabasesRequest), + DropDatabases(DropDatabasesRequest), + ListDatabases(ListDatabasesRequest), + ListPrivileges(ListPrivilegesRequest), + ModifyPrivileges(ModifyPrivilegesRequest), + + CreateUsers(CreateUsersRequest), + DropUsers(DropUsersRequest), + PasswdUser(SetUserPasswordRequest), + ListUsers(ListUsersRequest), + LockUsers(LockUsersRequest), + UnlockUsers(UnlockUsersRequest), + + // Commit, + Exit, +} + +// TODO: include a generic "message" that will display a message to the user? + +#[non_exhaustive] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum Response { + // Specific data for specific commands + CreateDatabases(CreateDatabasesResponse), + DropDatabases(DropDatabasesResponse), + ListDatabases(ListDatabasesResponse), + ListAllDatabases(ListAllDatabasesResponse), + ListPrivileges(ListPrivilegesResponse), + ListAllPrivileges(ListAllPrivilegesResponse), + ModifyPrivileges(ModifyPrivilegesResponse), + + CreateUsers(CreateUsersResponse), + DropUsers(DropUsersResponse), + SetUserPassword(SetUserPasswordResponse), + ListUsers(ListUsersResponse), + ListAllUsers(ListAllUsersResponse), + LockUsers(LockUsersResponse), + UnlockUsers(UnlockUsersResponse), + + // Generic responses + Ready, + Error(String), +} diff --git a/src/core/protocol/commands/create_databases.rs b/src/core/protocol/commands/create_databases.rs new file mode 100644 index 0000000..27f6f03 --- /dev/null +++ b/src/core/protocol/commands/create_databases.rs @@ -0,0 +1,76 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLDatabase, +}; + +pub type CreateDatabasesRequest = Vec; + +pub type CreateDatabasesResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CreateDatabaseError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + DatabaseAlreadyExists, + MySqlError(String), +} + +pub fn print_create_databases_output_status(output: &CreateDatabasesResponse) { + for (database_name, result) in output { + match result { + Ok(()) => { + println!("Database '{}' created successfully.", database_name); + } + Err(err) => { + println!("{}", err.to_error_message(database_name)); + println!("Skipping..."); + } + } + println!(); + } +} + +pub fn print_create_databases_output_status_json(output: &CreateDatabasesResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(()) => (name.to_string(), json!({ "status": "success" })), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + +impl CreateDatabaseError { + pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { + match self { + CreateDatabaseError::SanitizationError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + CreateDatabaseError::OwnershipError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + CreateDatabaseError::DatabaseAlreadyExists => { + format!("Database {} already exists.", database_name) + } + CreateDatabaseError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/create_users.rs b/src/core/protocol/commands/create_users.rs new file mode 100644 index 0000000..392dccd --- /dev/null +++ b/src/core/protocol/commands/create_users.rs @@ -0,0 +1,74 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLUser, +}; + +pub type CreateUsersRequest = Vec; + +pub type CreateUsersResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum CreateUserError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + UserAlreadyExists, + MySqlError(String), +} + +pub fn print_create_users_output_status(output: &CreateUsersResponse) { + for (username, result) in output { + match result { + Ok(()) => { + println!("User '{}' created successfully.", username); + } + Err(err) => { + println!("{}", err.to_error_message(username)); + println!("Skipping..."); + } + } + println!(); + } +} + +pub fn print_create_users_output_status_json(output: &CreateUsersResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(()) => (name.to_string(), json!({ "status": "success" })), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + +impl CreateUserError { + pub fn to_error_message(&self, username: &MySQLUser) -> String { + match self { + CreateUserError::SanitizationError(err) => { + err.to_error_message(username, DbOrUser::User) + } + CreateUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), + CreateUserError::UserAlreadyExists => { + format!("User '{}' already exists.", username) + } + CreateUserError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/drop_databases.rs b/src/core/protocol/commands/drop_databases.rs new file mode 100644 index 0000000..daba6bf --- /dev/null +++ b/src/core/protocol/commands/drop_databases.rs @@ -0,0 +1,79 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLDatabase, +}; + +pub type DropDatabasesRequest = Vec; + +pub type DropDatabasesResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DropDatabaseError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + DatabaseDoesNotExist, + MySqlError(String), +} + +pub fn print_drop_databases_output_status(output: &DropDatabasesResponse) { + for (database_name, result) in output { + match result { + Ok(()) => { + println!( + "Database '{}' dropped successfully.", + database_name.as_str() + ); + } + Err(err) => { + println!("{}", err.to_error_message(database_name)); + println!("Skipping..."); + } + } + println!(); + } +} + +pub fn print_drop_databases_output_status_json(output: &DropDatabasesResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(()) => (name.to_string(), json!({ "status": "success" })), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + +impl DropDatabaseError { + pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { + match self { + DropDatabaseError::SanitizationError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + DropDatabaseError::OwnershipError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + DropDatabaseError::DatabaseDoesNotExist => { + format!("Database {} does not exist.", database_name) + } + DropDatabaseError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/drop_users.rs b/src/core/protocol/commands/drop_users.rs new file mode 100644 index 0000000..83cdaf0 --- /dev/null +++ b/src/core/protocol/commands/drop_users.rs @@ -0,0 +1,72 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLUser, +}; + +pub type DropUsersRequest = Vec; + +pub type DropUsersResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DropUserError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + UserDoesNotExist, + MySqlError(String), +} + +pub fn print_drop_users_output_status(output: &DropUsersResponse) { + for (username, result) in output { + match result { + Ok(()) => { + println!("User '{}' dropped successfully.", username); + } + Err(err) => { + println!("{}", err.to_error_message(username)); + println!("Skipping..."); + } + } + println!(); + } +} + +pub fn print_drop_users_output_status_json(output: &DropUsersResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(()) => (name.to_string(), json!({ "status": "success" })), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + +impl DropUserError { + pub fn to_error_message(&self, username: &MySQLUser) -> String { + match self { + DropUserError::SanitizationError(err) => err.to_error_message(username, DbOrUser::User), + DropUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), + DropUserError::UserDoesNotExist => { + format!("User '{}' does not exist.", username) + } + DropUserError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/list_all_databases.rs b/src/core/protocol/commands/list_all_databases.rs new file mode 100644 index 0000000..89108f0 --- /dev/null +++ b/src/core/protocol/commands/list_all_databases.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +use crate::server::sql::database_operations::DatabaseRow; + +pub type ListAllDatabasesResponse = Result, ListAllDatabasesError>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ListAllDatabasesError { + MySqlError(String), +} + +impl ListAllDatabasesError { + pub fn to_error_message(&self) -> String { + match self { + ListAllDatabasesError::MySqlError(err) => format!("MySQL error: {}", err), + } + } +} diff --git a/src/core/protocol/commands/list_all_privileges.rs b/src/core/protocol/commands/list_all_privileges.rs new file mode 100644 index 0000000..ef30456 --- /dev/null +++ b/src/core/protocol/commands/list_all_privileges.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +use crate::core::database_privileges::DatabasePrivilegeRow; + +pub type ListAllPrivilegesResponse = + Result, GetAllDatabasesPrivilegeDataError>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum GetAllDatabasesPrivilegeDataError { + MySqlError(String), +} + +impl GetAllDatabasesPrivilegeDataError { + pub fn to_error_message(&self) -> String { + match self { + GetAllDatabasesPrivilegeDataError::MySqlError(err) => format!("MySQL error: {}", err), + } + } +} diff --git a/src/core/protocol/commands/list_all_users.rs b/src/core/protocol/commands/list_all_users.rs new file mode 100644 index 0000000..19d303c --- /dev/null +++ b/src/core/protocol/commands/list_all_users.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +use crate::server::sql::user_operations::DatabaseUser; + +pub type ListAllUsersResponse = Result, ListAllUsersError>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ListAllUsersError { + MySqlError(String), +} + +impl ListAllUsersError { + pub fn to_error_message(&self) -> String { + match self { + ListAllUsersError::MySqlError(err) => format!("MySQL error: {}", err), + } + } +} diff --git a/src/core/protocol/commands/list_databases.rs b/src/core/protocol/commands/list_databases.rs new file mode 100644 index 0000000..470a381 --- /dev/null +++ b/src/core/protocol/commands/list_databases.rs @@ -0,0 +1,42 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLDatabase, + }, + server::sql::database_operations::DatabaseRow, +}; + +pub type ListDatabasesRequest = Option>; + +pub type ListDatabasesResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ListDatabasesError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + DatabaseDoesNotExist, + MySqlError(String), +} + +impl ListDatabasesError { + pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { + match self { + ListDatabasesError::SanitizationError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + ListDatabasesError::OwnershipError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + ListDatabasesError::DatabaseDoesNotExist => { + format!("Database '{}' does not exist.", database_name) + } + ListDatabasesError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/list_privileges.rs b/src/core/protocol/commands/list_privileges.rs new file mode 100644 index 0000000..8208f29 --- /dev/null +++ b/src/core/protocol/commands/list_privileges.rs @@ -0,0 +1,45 @@ +// TODO: merge all rows into a single collection. +// they already contain which database they belong to. +// no need to index by database name. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::core::{ + database_privileges::DatabasePrivilegeRow, + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLDatabase, +}; + +pub type ListPrivilegesRequest = Option>; + +pub type ListPrivilegesResponse = + BTreeMap, GetDatabasesPrivilegeDataError>>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum GetDatabasesPrivilegeDataError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + DatabaseDoesNotExist, + MySqlError(String), +} + +impl GetDatabasesPrivilegeDataError { + pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { + match self { + GetDatabasesPrivilegeDataError::SanitizationError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + GetDatabasesPrivilegeDataError::OwnershipError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + GetDatabasesPrivilegeDataError::DatabaseDoesNotExist => { + format!("Database '{}' does not exist.", database_name) + } + GetDatabasesPrivilegeDataError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/list_users.rs b/src/core/protocol/commands/list_users.rs new file mode 100644 index 0000000..9d12d83 --- /dev/null +++ b/src/core/protocol/commands/list_users.rs @@ -0,0 +1,40 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; + +use crate::{ + core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLUser, + }, + server::sql::user_operations::DatabaseUser, +}; + +pub type ListUsersRequest = Option>; + +pub type ListUsersResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ListUsersError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + UserDoesNotExist, + MySqlError(String), +} + +impl ListUsersError { + pub fn to_error_message(&self, username: &MySQLUser) -> String { + match self { + ListUsersError::SanitizationError(err) => { + err.to_error_message(username, DbOrUser::User) + } + ListUsersError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), + ListUsersError::UserDoesNotExist => { + format!("User '{}' does not exist.", username) + } + ListUsersError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/lock_users.rs b/src/core/protocol/commands/lock_users.rs new file mode 100644 index 0000000..7c79670 --- /dev/null +++ b/src/core/protocol/commands/lock_users.rs @@ -0,0 +1,76 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLUser, +}; + +pub type LockUsersRequest = Vec; + +pub type LockUsersResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum LockUserError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + UserDoesNotExist, + UserIsAlreadyLocked, + MySqlError(String), +} + +pub fn print_lock_users_output_status(output: &LockUsersResponse) { + for (username, result) in output { + match result { + Ok(()) => { + println!("User '{}' locked successfully.", username); + } + Err(err) => { + println!("{}", err.to_error_message(username)); + println!("Skipping..."); + } + } + println!(); + } +} + +pub fn print_lock_users_output_status_json(output: &LockUsersResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(()) => (name.to_string(), json!({ "status": "success" })), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + +impl LockUserError { + pub fn to_error_message(&self, username: &MySQLUser) -> String { + match self { + LockUserError::SanitizationError(err) => err.to_error_message(username, DbOrUser::User), + LockUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), + LockUserError::UserDoesNotExist => { + format!("User '{}' does not exist.", username) + } + LockUserError::UserIsAlreadyLocked => { + format!("User '{}' is already locked.", username) + } + LockUserError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/modify_privileges.rs b/src/core/protocol/commands/modify_privileges.rs new file mode 100644 index 0000000..df70f72 --- /dev/null +++ b/src/core/protocol/commands/modify_privileges.rs @@ -0,0 +1,107 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use serde::{Deserialize, Serialize}; + +use crate::core::{ + database_privileges::{DatabasePrivilegeRow, DatabasePrivilegeRowDiff, DatabasePrivilegesDiff}, + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::{MySQLDatabase, MySQLUser}, +}; + +pub type ModifyPrivilegesRequest = BTreeSet; + +pub type ModifyPrivilegesResponse = + BTreeMap<(MySQLDatabase, MySQLUser), Result<(), ModifyDatabasePrivilegesError>>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ModifyDatabasePrivilegesError { + DatabaseSanitizationError(NameValidationError), + DatabaseOwnershipError(OwnerValidationError), + UserSanitizationError(NameValidationError), + UserOwnershipError(OwnerValidationError), + DatabaseDoesNotExist, + DiffDoesNotApply(DiffDoesNotApplyError), + MySqlError(String), +} + +#[allow(clippy::enum_variant_names)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum DiffDoesNotApplyError { + RowAlreadyExists(MySQLDatabase, MySQLUser), + RowDoesNotExist(MySQLDatabase, MySQLUser), + RowPrivilegeChangeDoesNotApply(DatabasePrivilegeRowDiff, DatabasePrivilegeRow), +} + +pub fn print_modify_database_privileges_output_status(output: &ModifyPrivilegesResponse) { + for ((database_name, username), result) in output { + match result { + Ok(()) => { + println!( + "Privileges for user '{}' on database '{}' modified successfully.", + username, database_name + ); + } + Err(err) => { + println!("{}", err.to_error_message(database_name, username)); + println!("Skipping..."); + } + } + println!(); + } +} + +impl ModifyDatabasePrivilegesError { + pub fn to_error_message(&self, database_name: &MySQLDatabase, username: &MySQLUser) -> String { + match self { + ModifyDatabasePrivilegesError::DatabaseSanitizationError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + ModifyDatabasePrivilegesError::DatabaseOwnershipError(err) => { + err.to_error_message(database_name, DbOrUser::Database) + } + ModifyDatabasePrivilegesError::UserSanitizationError(err) => { + err.to_error_message(username, DbOrUser::User) + } + ModifyDatabasePrivilegesError::UserOwnershipError(err) => { + err.to_error_message(username, DbOrUser::User) + } + ModifyDatabasePrivilegesError::DatabaseDoesNotExist => { + format!("Database '{}' does not exist.", database_name) + } + ModifyDatabasePrivilegesError::DiffDoesNotApply(diff) => { + format!( + "Could not apply privilege change:\n{}", + diff.to_error_message() + ) + } + ModifyDatabasePrivilegesError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} + +impl DiffDoesNotApplyError { + pub fn to_error_message(&self) -> String { + match self { + DiffDoesNotApplyError::RowAlreadyExists(database_name, username) => { + format!( + "Privileges for user '{}' on database '{}' already exist.", + username, database_name + ) + } + DiffDoesNotApplyError::RowDoesNotExist(database_name, username) => { + format!( + "Privileges for user '{}' on database '{}' do not exist.", + username, database_name + ) + } + DiffDoesNotApplyError::RowPrivilegeChangeDoesNotApply(diff, row) => { + format!( + "Could not apply privilege change {:?} to row {:?}", + diff, row + ) + } + } + } +} diff --git a/src/core/protocol/commands/passwd_user.rs b/src/core/protocol/commands/passwd_user.rs new file mode 100644 index 0000000..1efb703 --- /dev/null +++ b/src/core/protocol/commands/passwd_user.rs @@ -0,0 +1,47 @@ +use serde::{Deserialize, Serialize}; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLUser, +}; + +pub type SetUserPasswordRequest = (MySQLUser, String); + +pub type SetUserPasswordResponse = Result<(), SetPasswordError>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum SetPasswordError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + UserDoesNotExist, + MySqlError(String), +} + +pub fn print_set_password_output_status(output: &SetUserPasswordResponse, username: &MySQLUser) { + match output { + Ok(()) => { + println!("Password for user '{}' set successfully.", username); + } + Err(err) => { + println!("{}", err.to_error_message(username)); + println!("Skipping..."); + } + } +} + +impl SetPasswordError { + pub fn to_error_message(&self, username: &MySQLUser) -> String { + match self { + SetPasswordError::SanitizationError(err) => { + err.to_error_message(username, DbOrUser::User) + } + SetPasswordError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), + SetPasswordError::UserDoesNotExist => { + format!("User '{}' does not exist.", username) + } + SetPasswordError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/commands/unlock_users.rs b/src/core/protocol/commands/unlock_users.rs new file mode 100644 index 0000000..5356972 --- /dev/null +++ b/src/core/protocol/commands/unlock_users.rs @@ -0,0 +1,78 @@ +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use crate::core::{ + protocol::request_validation::{DbOrUser, NameValidationError, OwnerValidationError}, + types::MySQLUser, +}; + +pub type UnlockUsersRequest = Vec; + +pub type UnlockUsersResponse = BTreeMap>; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum UnlockUserError { + SanitizationError(NameValidationError), + OwnershipError(OwnerValidationError), + UserDoesNotExist, + UserIsAlreadyUnlocked, + MySqlError(String), +} + +pub fn print_unlock_users_output_status(output: &UnlockUsersResponse) { + for (username, result) in output { + match result { + Ok(()) => { + println!("User '{}' unlocked successfully.", username); + } + Err(err) => { + println!("{}", err.to_error_message(username)); + println!("Skipping..."); + } + } + println!(); + } +} + +pub fn print_unlock_users_output_status_json(output: &UnlockUsersResponse) { + let value = output + .iter() + .map(|(name, result)| match result { + Ok(()) => (name.to_string(), json!({ "status": "success" })), + Err(err) => ( + name.to_string(), + json!({ + "status": "error", + "error": err.to_error_message(name), + }), + ), + }) + .collect::>(); + println!( + "{}", + serde_json::to_string_pretty(&value) + .unwrap_or("Failed to serialize result to JSON".to_string()) + ); +} + +impl UnlockUserError { + pub fn to_error_message(&self, username: &MySQLUser) -> String { + match self { + UnlockUserError::SanitizationError(err) => { + err.to_error_message(username, DbOrUser::User) + } + UnlockUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), + UnlockUserError::UserDoesNotExist => { + format!("User '{}' does not exist.", username) + } + UnlockUserError::UserIsAlreadyUnlocked => { + format!("User '{}' is already unlocked.", username) + } + UnlockUserError::MySqlError(err) => { + format!("MySQL error: {}", err) + } + } + } +} diff --git a/src/core/protocol/request_response.rs b/src/core/protocol/request_response.rs deleted file mode 100644 index a0e831e..0000000 --- a/src/core/protocol/request_response.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::collections::BTreeSet; - -use serde::{Deserialize, Serialize}; -use tokio::net::UnixStream; -use tokio_serde::{Framed as SerdeFramed, formats::Bincode}; -use tokio_util::codec::{Framed, LengthDelimitedCodec}; - -use crate::core::{ - database_privileges::DatabasePrivilegesDiff, - protocol::*, - types::{MySQLDatabase, MySQLUser}, -}; - -pub type ServerToClientMessageStream = SerdeFramed< - Framed, - Request, - Response, - Bincode, ->; - -pub type ClientToServerMessageStream = SerdeFramed< - Framed, - Response, - Request, - Bincode, ->; - -pub fn create_server_to_client_message_stream(socket: UnixStream) -> ServerToClientMessageStream { - let length_delimited = Framed::new(socket, LengthDelimitedCodec::new()); - tokio_serde::Framed::new(length_delimited, Bincode::default()) -} - -pub fn create_client_to_server_message_stream(socket: UnixStream) -> ClientToServerMessageStream { - let length_delimited = Framed::new(socket, LengthDelimitedCodec::new()); - tokio_serde::Framed::new(length_delimited, Bincode::default()) -} - -#[non_exhaustive] -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Request { - CreateDatabases(Vec), - DropDatabases(Vec), - ListDatabases(Option>), - ListPrivileges(Option>), - ModifyPrivileges(BTreeSet), - - CreateUsers(Vec), - DropUsers(Vec), - PasswdUser(MySQLUser, String), - ListUsers(Option>), - LockUsers(Vec), - UnlockUsers(Vec), - - // Commit, - Exit, -} - -// TODO: include a generic "message" that will display a message to the user? - -#[non_exhaustive] -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum Response { - // Specific data for specific commands - CreateDatabases(CreateDatabasesOutput), - DropDatabases(DropDatabasesOutput), - ListDatabases(ListDatabasesOutput), - ListAllDatabases(ListAllDatabasesOutput), - ListPrivileges(GetDatabasesPrivilegeData), - ListAllPrivileges(GetAllDatabasesPrivilegeData), - ModifyPrivileges(ModifyDatabasePrivilegesOutput), - - CreateUsers(CreateUsersOutput), - DropUsers(DropUsersOutput), - PasswdUser(SetPasswordOutput), - ListUsers(ListUsersOutput), - ListAllUsers(ListAllUsersOutput), - LockUsers(LockUsersOutput), - UnlockUsers(UnlockUsersOutput), - - // Generic responses - Ready, - Error(String), -} diff --git a/src/core/protocol/request_validation.rs b/src/core/protocol/request_validation.rs new file mode 100644 index 0000000..d93c34b --- /dev/null +++ b/src/core/protocol/request_validation.rs @@ -0,0 +1,117 @@ +use indoc::indoc; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +use crate::core::common::UnixUser; + +/// This enum is used to differentiate between database and user operations. +/// Their output are very similar, but there are slight differences in the words used. +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum DbOrUser { + Database, + User, +} + +impl DbOrUser { + pub fn lowercased(&self) -> &'static str { + match self { + DbOrUser::Database => "database", + DbOrUser::User => "user", + } + } + + pub fn capitalized(&self) -> &'static str { + match self { + DbOrUser::Database => "Database", + DbOrUser::User => "User", + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub enum NameValidationError { + EmptyString, + InvalidCharacters, + TooLong, +} + +impl NameValidationError { + pub fn to_error_message(self, name: &str, db_or_user: DbOrUser) -> String { + match self { + NameValidationError::EmptyString => { + format!("{} name cannot be empty.", db_or_user.capitalized()).to_owned() + } + NameValidationError::TooLong => format!( + "{} is too long. Maximum length is 64 characters.", + db_or_user.capitalized() + ) + .to_owned(), + NameValidationError::InvalidCharacters => format!( + indoc! {r#" + Invalid characters in {} name: '{}' + + Only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted. + "#}, + db_or_user.lowercased(), + name + ) + .to_owned(), + } + } +} + +impl OwnerValidationError { + pub fn to_error_message(self, name: &str, db_or_user: DbOrUser) -> String { + let user = UnixUser::from_enviroment(); + + let UnixUser { + username, + mut groups, + } = user.unwrap_or(UnixUser { + username: "???".to_string(), + groups: vec![], + }); + + groups.sort(); + + match self { + OwnerValidationError::NoMatch => format!( + indoc! {r#" + Invalid {} name prefix: '{}' does not match your username or any of your groups. + Are you sure you are allowed to create {} names with this prefix? + The format should be: _<{} name> + + Allowed prefixes: + - {} + {} + "#}, + db_or_user.lowercased(), + name, + db_or_user.lowercased(), + db_or_user.lowercased(), + username, + groups + .into_iter() + .filter(|g| g != &username) + .map(|g| format!(" - {}", g)) + .join("\n"), + ) + .to_owned(), + OwnerValidationError::StringEmpty => format!( + "'{}' is not a valid {} name.", + name, + db_or_user.lowercased() + ) + .to_string(), + } + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] +pub enum OwnerValidationError { + // The name is valid, but none of the given prefixes matched the name + NoMatch, + + // The name is empty, which is invalid + StringEmpty, +} diff --git a/src/core/protocol/server_responses.rs b/src/core/protocol/server_responses.rs deleted file mode 100644 index 553a097..0000000 --- a/src/core/protocol/server_responses.rs +++ /dev/null @@ -1,773 +0,0 @@ -use std::collections::BTreeMap; - -use indoc::indoc; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use serde_json::json; - -use crate::{ - core::{ - common::UnixUser, - database_privileges::{DatabasePrivilegeRow, DatabasePrivilegeRowDiff}, - types::{MySQLDatabase, MySQLUser}, - }, - server::sql::{database_operations::DatabaseRow, user_operations::DatabaseUser}, -}; - -/// This enum is used to differentiate between database and user operations. -/// Their output are very similar, but there are slight differences in the words used. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum DbOrUser { - Database, - User, -} - -impl DbOrUser { - pub fn lowercased(&self) -> &'static str { - match self { - DbOrUser::Database => "database", - DbOrUser::User => "user", - } - } - - pub fn capitalized(&self) -> &'static str { - match self { - DbOrUser::Database => "Database", - DbOrUser::User => "User", - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub enum NameValidationError { - EmptyString, - InvalidCharacters, - TooLong, -} - -impl NameValidationError { - pub fn to_error_message(self, name: &str, db_or_user: DbOrUser) -> String { - match self { - NameValidationError::EmptyString => { - format!("{} name cannot be empty.", db_or_user.capitalized()).to_owned() - } - NameValidationError::TooLong => format!( - "{} is too long. Maximum length is 64 characters.", - db_or_user.capitalized() - ) - .to_owned(), - NameValidationError::InvalidCharacters => format!( - indoc! {r#" - Invalid characters in {} name: '{}' - - Only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted. - "#}, - db_or_user.lowercased(), - name - ) - .to_owned(), - } - } -} - -impl OwnerValidationError { - pub fn to_error_message(self, name: &str, db_or_user: DbOrUser) -> String { - let user = UnixUser::from_enviroment(); - - let UnixUser { - username, - mut groups, - } = user.unwrap_or(UnixUser { - username: "???".to_string(), - groups: vec![], - }); - - groups.sort(); - - match self { - OwnerValidationError::NoMatch => format!( - indoc! {r#" - Invalid {} name prefix: '{}' does not match your username or any of your groups. - Are you sure you are allowed to create {} names with this prefix? - The format should be: _<{} name> - - Allowed prefixes: - - {} - {} - "#}, - db_or_user.lowercased(), - name, - db_or_user.lowercased(), - db_or_user.lowercased(), - username, - groups - .into_iter() - .filter(|g| g != &username) - .map(|g| format!(" - {}", g)) - .join("\n"), - ) - .to_owned(), - OwnerValidationError::StringEmpty => format!( - "'{}' is not a valid {} name.", - name, - db_or_user.lowercased() - ) - .to_string(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] -pub enum OwnerValidationError { - // The name is valid, but none of the given prefixes matched the name - NoMatch, - - // The name is empty, which is invalid - StringEmpty, -} - -pub type CreateDatabasesOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum CreateDatabaseError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - DatabaseAlreadyExists, - MySqlError(String), -} - -pub fn print_create_databases_output_status(output: &CreateDatabasesOutput) { - for (database_name, result) in output { - match result { - Ok(()) => { - println!("Database '{}' created successfully.", database_name); - } - Err(err) => { - println!("{}", err.to_error_message(database_name)); - println!("Skipping..."); - } - } - println!(); - } -} - -pub fn print_create_databases_output_status_json(output: &CreateDatabasesOutput) { - let value = output - .iter() - .map(|(name, result)| match result { - Ok(()) => (name.to_string(), json!({ "status": "success" })), - Err(err) => ( - name.to_string(), - json!({ - "status": "error", - "error": err.to_error_message(name), - }), - ), - }) - .collect::>(); - println!( - "{}", - serde_json::to_string_pretty(&value) - .unwrap_or("Failed to serialize result to JSON".to_string()) - ); -} - -impl CreateDatabaseError { - pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { - match self { - CreateDatabaseError::SanitizationError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - CreateDatabaseError::OwnershipError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - CreateDatabaseError::DatabaseAlreadyExists => { - format!("Database {} already exists.", database_name) - } - CreateDatabaseError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type DropDatabasesOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum DropDatabaseError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - DatabaseDoesNotExist, - MySqlError(String), -} - -pub fn print_drop_databases_output_status(output: &DropDatabasesOutput) { - for (database_name, result) in output { - match result { - Ok(()) => { - println!( - "Database '{}' dropped successfully.", - database_name.as_str() - ); - } - Err(err) => { - println!("{}", err.to_error_message(database_name)); - println!("Skipping..."); - } - } - println!(); - } -} - -pub fn print_drop_databases_output_status_json(output: &DropDatabasesOutput) { - let value = output - .iter() - .map(|(name, result)| match result { - Ok(()) => (name.to_string(), json!({ "status": "success" })), - Err(err) => ( - name.to_string(), - json!({ - "status": "error", - "error": err.to_error_message(name), - }), - ), - }) - .collect::>(); - println!( - "{}", - serde_json::to_string_pretty(&value) - .unwrap_or("Failed to serialize result to JSON".to_string()) - ); -} - -impl DropDatabaseError { - pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { - match self { - DropDatabaseError::SanitizationError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - DropDatabaseError::OwnershipError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - DropDatabaseError::DatabaseDoesNotExist => { - format!("Database {} does not exist.", database_name) - } - DropDatabaseError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type ListDatabasesOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ListDatabasesError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - DatabaseDoesNotExist, - MySqlError(String), -} - -impl ListDatabasesError { - pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { - match self { - ListDatabasesError::SanitizationError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - ListDatabasesError::OwnershipError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - ListDatabasesError::DatabaseDoesNotExist => { - format!("Database '{}' does not exist.", database_name) - } - ListDatabasesError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type ListAllDatabasesOutput = Result, ListAllDatabasesError>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ListAllDatabasesError { - MySqlError(String), -} - -impl ListAllDatabasesError { - pub fn to_error_message(&self) -> String { - match self { - ListAllDatabasesError::MySqlError(err) => format!("MySQL error: {}", err), - } - } -} - -// TODO: merge all rows into a single collection. -// they already contain which database they belong to. -// no need to index by database name. - -pub type GetDatabasesPrivilegeData = - BTreeMap, GetDatabasesPrivilegeDataError>>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum GetDatabasesPrivilegeDataError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - DatabaseDoesNotExist, - MySqlError(String), -} - -impl GetDatabasesPrivilegeDataError { - pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String { - match self { - GetDatabasesPrivilegeDataError::SanitizationError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - GetDatabasesPrivilegeDataError::OwnershipError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - GetDatabasesPrivilegeDataError::DatabaseDoesNotExist => { - format!("Database '{}' does not exist.", database_name) - } - GetDatabasesPrivilegeDataError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type GetAllDatabasesPrivilegeData = - Result, GetAllDatabasesPrivilegeDataError>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum GetAllDatabasesPrivilegeDataError { - MySqlError(String), -} - -impl GetAllDatabasesPrivilegeDataError { - pub fn to_error_message(&self) -> String { - match self { - GetAllDatabasesPrivilegeDataError::MySqlError(err) => format!("MySQL error: {}", err), - } - } -} - -pub type ModifyDatabasePrivilegesOutput = - BTreeMap<(MySQLDatabase, MySQLUser), Result<(), ModifyDatabasePrivilegesError>>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ModifyDatabasePrivilegesError { - DatabaseSanitizationError(NameValidationError), - DatabaseOwnershipError(OwnerValidationError), - UserSanitizationError(NameValidationError), - UserOwnershipError(OwnerValidationError), - DatabaseDoesNotExist, - DiffDoesNotApply(DiffDoesNotApplyError), - MySqlError(String), -} - -#[allow(clippy::enum_variant_names)] -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum DiffDoesNotApplyError { - RowAlreadyExists(MySQLDatabase, MySQLUser), - RowDoesNotExist(MySQLDatabase, MySQLUser), - RowPrivilegeChangeDoesNotApply(DatabasePrivilegeRowDiff, DatabasePrivilegeRow), -} - -pub fn print_modify_database_privileges_output_status(output: &ModifyDatabasePrivilegesOutput) { - for ((database_name, username), result) in output { - match result { - Ok(()) => { - println!( - "Privileges for user '{}' on database '{}' modified successfully.", - username, database_name - ); - } - Err(err) => { - println!("{}", err.to_error_message(database_name, username)); - println!("Skipping..."); - } - } - println!(); - } -} - -impl ModifyDatabasePrivilegesError { - pub fn to_error_message(&self, database_name: &MySQLDatabase, username: &MySQLUser) -> String { - match self { - ModifyDatabasePrivilegesError::DatabaseSanitizationError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - ModifyDatabasePrivilegesError::DatabaseOwnershipError(err) => { - err.to_error_message(database_name, DbOrUser::Database) - } - ModifyDatabasePrivilegesError::UserSanitizationError(err) => { - err.to_error_message(username, DbOrUser::User) - } - ModifyDatabasePrivilegesError::UserOwnershipError(err) => { - err.to_error_message(username, DbOrUser::User) - } - ModifyDatabasePrivilegesError::DatabaseDoesNotExist => { - format!("Database '{}' does not exist.", database_name) - } - ModifyDatabasePrivilegesError::DiffDoesNotApply(diff) => { - format!( - "Could not apply privilege change:\n{}", - diff.to_error_message() - ) - } - ModifyDatabasePrivilegesError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -impl DiffDoesNotApplyError { - pub fn to_error_message(&self) -> String { - match self { - DiffDoesNotApplyError::RowAlreadyExists(database_name, username) => { - format!( - "Privileges for user '{}' on database '{}' already exist.", - username, database_name - ) - } - DiffDoesNotApplyError::RowDoesNotExist(database_name, username) => { - format!( - "Privileges for user '{}' on database '{}' do not exist.", - username, database_name - ) - } - DiffDoesNotApplyError::RowPrivilegeChangeDoesNotApply(diff, row) => { - format!( - "Could not apply privilege change {:?} to row {:?}", - diff, row - ) - } - } - } -} - -pub type CreateUsersOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum CreateUserError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - UserAlreadyExists, - MySqlError(String), -} - -pub fn print_create_users_output_status(output: &CreateUsersOutput) { - for (username, result) in output { - match result { - Ok(()) => { - println!("User '{}' created successfully.", username); - } - Err(err) => { - println!("{}", err.to_error_message(username)); - println!("Skipping..."); - } - } - println!(); - } -} - -pub fn print_create_users_output_status_json(output: &CreateUsersOutput) { - let value = output - .iter() - .map(|(name, result)| match result { - Ok(()) => (name.to_string(), json!({ "status": "success" })), - Err(err) => ( - name.to_string(), - json!({ - "status": "error", - "error": err.to_error_message(name), - }), - ), - }) - .collect::>(); - println!( - "{}", - serde_json::to_string_pretty(&value) - .unwrap_or("Failed to serialize result to JSON".to_string()) - ); -} - -impl CreateUserError { - pub fn to_error_message(&self, username: &MySQLUser) -> String { - match self { - CreateUserError::SanitizationError(err) => { - err.to_error_message(username, DbOrUser::User) - } - CreateUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), - CreateUserError::UserAlreadyExists => { - format!("User '{}' already exists.", username) - } - CreateUserError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type DropUsersOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum DropUserError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - UserDoesNotExist, - MySqlError(String), -} - -pub fn print_drop_users_output_status(output: &DropUsersOutput) { - for (username, result) in output { - match result { - Ok(()) => { - println!("User '{}' dropped successfully.", username); - } - Err(err) => { - println!("{}", err.to_error_message(username)); - println!("Skipping..."); - } - } - println!(); - } -} - -pub fn print_drop_users_output_status_json(output: &DropUsersOutput) { - let value = output - .iter() - .map(|(name, result)| match result { - Ok(()) => (name.to_string(), json!({ "status": "success" })), - Err(err) => ( - name.to_string(), - json!({ - "status": "error", - "error": err.to_error_message(name), - }), - ), - }) - .collect::>(); - println!( - "{}", - serde_json::to_string_pretty(&value) - .unwrap_or("Failed to serialize result to JSON".to_string()) - ); -} - -impl DropUserError { - pub fn to_error_message(&self, username: &MySQLUser) -> String { - match self { - DropUserError::SanitizationError(err) => err.to_error_message(username, DbOrUser::User), - DropUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), - DropUserError::UserDoesNotExist => { - format!("User '{}' does not exist.", username) - } - DropUserError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type SetPasswordOutput = Result<(), SetPasswordError>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum SetPasswordError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - UserDoesNotExist, - MySqlError(String), -} - -pub fn print_set_password_output_status(output: &SetPasswordOutput, username: &MySQLUser) { - match output { - Ok(()) => { - println!("Password for user '{}' set successfully.", username); - } - Err(err) => { - println!("{}", err.to_error_message(username)); - println!("Skipping..."); - } - } -} - -impl SetPasswordError { - pub fn to_error_message(&self, username: &MySQLUser) -> String { - match self { - SetPasswordError::SanitizationError(err) => { - err.to_error_message(username, DbOrUser::User) - } - SetPasswordError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), - SetPasswordError::UserDoesNotExist => { - format!("User '{}' does not exist.", username) - } - SetPasswordError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type LockUsersOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum LockUserError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - UserDoesNotExist, - UserIsAlreadyLocked, - MySqlError(String), -} - -pub fn print_lock_users_output_status(output: &LockUsersOutput) { - for (username, result) in output { - match result { - Ok(()) => { - println!("User '{}' locked successfully.", username); - } - Err(err) => { - println!("{}", err.to_error_message(username)); - println!("Skipping..."); - } - } - println!(); - } -} - -pub fn print_lock_users_output_status_json(output: &LockUsersOutput) { - let value = output - .iter() - .map(|(name, result)| match result { - Ok(()) => (name.to_string(), json!({ "status": "success" })), - Err(err) => ( - name.to_string(), - json!({ - "status": "error", - "error": err.to_error_message(name), - }), - ), - }) - .collect::>(); - println!( - "{}", - serde_json::to_string_pretty(&value) - .unwrap_or("Failed to serialize result to JSON".to_string()) - ); -} - -impl LockUserError { - pub fn to_error_message(&self, username: &MySQLUser) -> String { - match self { - LockUserError::SanitizationError(err) => err.to_error_message(username, DbOrUser::User), - LockUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), - LockUserError::UserDoesNotExist => { - format!("User '{}' does not exist.", username) - } - LockUserError::UserIsAlreadyLocked => { - format!("User '{}' is already locked.", username) - } - LockUserError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type UnlockUsersOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum UnlockUserError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - UserDoesNotExist, - UserIsAlreadyUnlocked, - MySqlError(String), -} - -pub fn print_unlock_users_output_status(output: &UnlockUsersOutput) { - for (username, result) in output { - match result { - Ok(()) => { - println!("User '{}' unlocked successfully.", username); - } - Err(err) => { - println!("{}", err.to_error_message(username)); - println!("Skipping..."); - } - } - println!(); - } -} - -pub fn print_unlock_users_output_status_json(output: &UnlockUsersOutput) { - let value = output - .iter() - .map(|(name, result)| match result { - Ok(()) => (name.to_string(), json!({ "status": "success" })), - Err(err) => ( - name.to_string(), - json!({ - "status": "error", - "error": err.to_error_message(name), - }), - ), - }) - .collect::>(); - println!( - "{}", - serde_json::to_string_pretty(&value) - .unwrap_or("Failed to serialize result to JSON".to_string()) - ); -} - -impl UnlockUserError { - pub fn to_error_message(&self, username: &MySQLUser) -> String { - match self { - UnlockUserError::SanitizationError(err) => { - err.to_error_message(username, DbOrUser::User) - } - UnlockUserError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), - UnlockUserError::UserDoesNotExist => { - format!("User '{}' does not exist.", username) - } - UnlockUserError::UserIsAlreadyUnlocked => { - format!("User '{}' is already unlocked.", username) - } - UnlockUserError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type ListUsersOutput = BTreeMap>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ListUsersError { - SanitizationError(NameValidationError), - OwnershipError(OwnerValidationError), - UserDoesNotExist, - MySqlError(String), -} - -impl ListUsersError { - pub fn to_error_message(&self, username: &MySQLUser) -> String { - match self { - ListUsersError::SanitizationError(err) => { - err.to_error_message(username, DbOrUser::User) - } - ListUsersError::OwnershipError(err) => err.to_error_message(username, DbOrUser::User), - ListUsersError::UserDoesNotExist => { - format!("User '{}' does not exist.", username) - } - ListUsersError::MySqlError(err) => { - format!("MySQL error: {}", err) - } - } - } -} - -pub type ListAllUsersOutput = Result, ListAllUsersError>; -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub enum ListAllUsersError { - MySqlError(String), -} - -impl ListAllUsersError { - pub fn to_error_message(&self) -> String { - match self { - ListAllUsersError::MySqlError(err) => format!("MySQL error: {}", err), - } - } -} diff --git a/src/server/input_sanitization.rs b/src/server/input_sanitization.rs index a16f69c..46aa8e9 100644 --- a/src/server/input_sanitization.rs +++ b/src/server/input_sanitization.rs @@ -1,6 +1,6 @@ use crate::core::{ common::UnixUser, - protocol::server_responses::{NameValidationError, OwnerValidationError}, + protocol::request_validation::{NameValidationError, OwnerValidationError}, }; const MAX_NAME_LENGTH: usize = 64; diff --git a/src/server/server_loop.rs b/src/server/server_loop.rs index 5966f30..1f391e0 100644 --- a/src/server/server_loop.rs +++ b/src/server/server_loop.rs @@ -18,7 +18,7 @@ use crate::server::sql::database_operations::list_databases; use crate::{ core::{ common::{DEFAULT_SOCKET_PATH, UnixUser}, - protocol::request_response::{ + protocol::{ Request, Response, ServerToClientMessageStream, create_server_to_client_message_stream, }, }, @@ -233,8 +233,8 @@ async fn handle_requests_for_single_session_with_db_connection( // TODO: don't clone the request let request_to_display = match &request { - Request::PasswdUser(db_user, _) => { - Request::PasswdUser(db_user.to_owned(), "".to_string()) + Request::PasswdUser((db_user, _)) => { + Request::PasswdUser((db_user.to_owned(), "".to_string())) } request => request.to_owned(), }; @@ -289,11 +289,11 @@ async fn handle_requests_for_single_session_with_db_connection( let result = drop_database_users(db_users, unix_user, db_connection).await; Response::DropUsers(result) } - Request::PasswdUser(db_user, password) => { + Request::PasswdUser((db_user, password)) => { let result = set_password_for_database_user(&db_user, &password, unix_user, db_connection) .await; - Response::PasswdUser(result) + Response::SetUserPassword(result) } Request::ListUsers(db_users) => match db_users { Some(db_users) => { @@ -321,8 +321,10 @@ async fn handle_requests_for_single_session_with_db_connection( // TODO: don't clone the response let response_to_display = match &response { - Response::PasswdUser(Err(SetPasswordError::MySqlError(_))) => { - Response::PasswdUser(Err(SetPasswordError::MySqlError("".to_string()))) + Response::SetUserPassword(Err(SetPasswordError::MySqlError(_))) => { + Response::SetUserPassword(Err(SetPasswordError::MySqlError( + "".to_string(), + ))) } response => response.to_owned(), }; diff --git a/src/server/sql/database_operations.rs b/src/server/sql/database_operations.rs index 64da8cc..e1ca273 100644 --- a/src/server/sql/database_operations.rs +++ b/src/server/sql/database_operations.rs @@ -10,8 +10,9 @@ use crate::{ core::{ common::UnixUser, protocol::{ - CreateDatabaseError, CreateDatabasesOutput, DropDatabaseError, DropDatabasesOutput, - ListAllDatabasesError, ListAllDatabasesOutput, ListDatabasesError, ListDatabasesOutput, + CreateDatabaseError, CreateDatabasesResponse, DropDatabaseError, DropDatabasesResponse, + ListAllDatabasesError, ListAllDatabasesResponse, ListDatabasesError, + ListDatabasesResponse, }, }, server::{ @@ -46,7 +47,7 @@ pub async fn create_databases( database_names: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> CreateDatabasesOutput { +) -> CreateDatabasesResponse { let mut results = BTreeMap::new(); for database_name in database_names { @@ -105,7 +106,7 @@ pub async fn drop_databases( database_names: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> DropDatabasesOutput { +) -> DropDatabasesResponse { let mut results = BTreeMap::new(); for database_name in database_names { @@ -177,7 +178,7 @@ pub async fn list_databases( database_names: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> ListDatabasesOutput { +) -> ListDatabasesResponse { let mut results = BTreeMap::new(); for database_name in database_names { @@ -227,7 +228,7 @@ pub async fn list_databases( pub async fn list_all_databases_for_user( unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> ListAllDatabasesOutput { +) -> ListAllDatabasesResponse { let result = sqlx::query_as::<_, DatabaseRow>( r#" SELECT `SCHEMA_NAME` AS `database` diff --git a/src/server/sql/database_privilege_operations.rs b/src/server/sql/database_privilege_operations.rs index 5eecc2e..fcdb3d3 100644 --- a/src/server/sql/database_privilege_operations.rs +++ b/src/server/sql/database_privilege_operations.rs @@ -28,9 +28,9 @@ use crate::{ DatabasePrivilegesDiff, }, protocol::{ - DiffDoesNotApplyError, GetAllDatabasesPrivilegeData, GetAllDatabasesPrivilegeDataError, - GetDatabasesPrivilegeData, GetDatabasesPrivilegeDataError, - ModifyDatabasePrivilegesError, ModifyDatabasePrivilegesOutput, + DiffDoesNotApplyError, GetAllDatabasesPrivilegeDataError, + GetDatabasesPrivilegeDataError, ListAllPrivilegesResponse, ListPrivilegesResponse, + ModifyDatabasePrivilegesError, ModifyPrivilegesResponse, }, types::{MySQLDatabase, MySQLUser}, }, @@ -139,7 +139,7 @@ pub async fn get_databases_privilege_data( database_names: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> GetDatabasesPrivilegeData { +) -> ListPrivilegesResponse { let mut results = BTreeMap::new(); for database_name in database_names.iter() { @@ -186,7 +186,7 @@ pub async fn get_databases_privilege_data( pub async fn get_all_database_privileges( unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> GetAllDatabasesPrivilegeData { +) -> ListAllPrivilegesResponse { let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!( indoc! {r#" SELECT {} FROM `db` WHERE `db` IN @@ -393,7 +393,7 @@ pub async fn apply_privilege_diffs( database_privilege_diffs: BTreeSet, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> ModifyDatabasePrivilegesOutput { +) -> ModifyPrivilegesResponse { let mut results: BTreeMap<(MySQLDatabase, MySQLUser), _> = BTreeMap::new(); for diff in database_privilege_diffs { diff --git a/src/server/sql/user_operations.rs b/src/server/sql/user_operations.rs index bbb71e4..67a517c 100644 --- a/src/server/sql/user_operations.rs +++ b/src/server/sql/user_operations.rs @@ -12,9 +12,10 @@ use crate::{ common::UnixUser, database_privileges::DATABASE_PRIVILEGE_FIELDS, protocol::{ - CreateUserError, CreateUsersOutput, DropUserError, DropUsersOutput, ListAllUsersError, - ListAllUsersOutput, ListUsersError, ListUsersOutput, LockUserError, LockUsersOutput, - SetPasswordError, SetPasswordOutput, UnlockUserError, UnlockUsersOutput, + CreateUserError, CreateUsersResponse, DropUserError, DropUsersResponse, + ListAllUsersError, ListAllUsersResponse, ListUsersError, ListUsersResponse, + LockUserError, LockUsersResponse, SetPasswordError, SetUserPasswordResponse, + UnlockUserError, UnlockUsersResponse, }, types::MySQLUser, }, @@ -54,7 +55,7 @@ pub async fn create_database_users( db_users: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> CreateUsersOutput { +) -> CreateUsersResponse { let mut results = BTreeMap::new(); for db_user in db_users { @@ -100,7 +101,7 @@ pub async fn drop_database_users( db_users: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> DropUsersOutput { +) -> DropUsersResponse { let mut results = BTreeMap::new(); for db_user in db_users { @@ -147,7 +148,7 @@ pub async fn set_password_for_database_user( password: &str, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> SetPasswordOutput { +) -> SetUserPasswordResponse { if let Err(err) = validate_name(db_user) { return Err(SetPasswordError::SanitizationError(err)); } @@ -221,7 +222,7 @@ pub async fn lock_database_users( db_users: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> LockUsersOutput { +) -> LockUsersResponse { let mut results = BTreeMap::new(); for db_user in db_users { @@ -281,7 +282,7 @@ pub async fn unlock_database_users( db_users: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> UnlockUsersOutput { +) -> UnlockUsersResponse { let mut results = BTreeMap::new(); for db_user in db_users { @@ -380,7 +381,7 @@ pub async fn list_database_users( db_users: Vec, unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> ListUsersOutput { +) -> ListUsersResponse { let mut results = BTreeMap::new(); for db_user in db_users { @@ -422,7 +423,7 @@ pub async fn list_database_users( pub async fn list_all_database_users_for_unix_user( unix_user: &UnixUser, connection: &mut MySqlConnection, -) -> ListAllUsersOutput { +) -> ListAllUsersResponse { let mut result = sqlx::query_as::<_, DatabaseUser>( &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `user`.`User` REGEXP ?"), )