core/protocol: split commands into separate files
All checks were successful
Build and test / check (push) Successful in 2m8s
Build and test / build (push) Successful in 3m3s
Build and test / check-license (push) Successful in 4m58s
Build and test / test (push) Successful in 3m47s
Build and test / docs (push) Successful in 5m47s

This commit is contained in:
2025-11-26 02:41:28 +09:00
parent c9815fe7de
commit 805c2d11ff
28 changed files with 1058 additions and 898 deletions

View File

@@ -70,7 +70,7 @@ pub async fn create_users(
.interact()? .interact()?
{ {
let password = read_password_from_stdin_with_double_check(username)?; 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 { if let Err(err) = server_connection.send(message).await {
server_connection.close().await.ok(); server_connection.close().await.ok();
@@ -78,7 +78,7 @@ pub async fn create_users(
} }
match server_connection.next().await { match server_connection.next().await {
Some(Ok(Response::PasswdUser(result))) => { Some(Ok(Response::SetUserPassword(result))) => {
print_set_password_output_status(&result, username) print_set_password_output_status(&result, username)
} }
response => return erroneous_server_response(response), response => return erroneous_server_response(response),

View File

@@ -73,7 +73,7 @@ pub async fn passwd_user(
read_password_from_stdin_with_double_check(&args.username)? 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 { if let Err(err) = server_connection.send(message).await {
server_connection.close().await.ok(); server_connection.close().await.ok();
@@ -81,7 +81,7 @@ pub async fn passwd_user(
} }
let result = match server_connection.next().await { 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), response => return erroneous_server_response(response),
}; };

View File

@@ -1,6 +1,6 @@
use crate::core::protocol::{ use crate::core::protocol::{
CreateDatabaseError, CreateUserError, DbOrUser, DropDatabaseError, DropUserError, CreateDatabaseError, CreateUserError, DropDatabaseError, DropUserError,
GetDatabasesPrivilegeDataError, ListUsersError, GetDatabasesPrivilegeDataError, ListUsersError, request_validation::DbOrUser,
}; };
pub fn name_validation_error_to_error_message(name: &str, db_or_user: DbOrUser) -> String { pub fn name_validation_error_to_error_message(name: &str, db_or_user: DbOrUser) -> String {

View File

@@ -235,10 +235,10 @@ async fn passwd_users(
for user in users { for user in users {
let password = read_password_from_stdin_with_double_check(&user.user)?; 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?; server_connection.send(message).await?;
match server_connection.next().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), Ok(()) => println!("Password updated for user '{}'.", &user.user),
Err(_) => eprintln!( Err(_) => eprintln!(
"{}: Failed to update password for user '{}'.", "{}: Failed to update password for user '{}'.",

View File

@@ -1,5 +1,4 @@
pub mod request_response; mod commands;
pub mod server_responses; pub mod request_validation;
pub use request_response::*; pub use commands::*;
pub use server_responses::*;

View File

@@ -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<UnixStream, LengthDelimitedCodec>,
Request,
Response,
Bincode<Request, Response>,
>;
pub type ClientToServerMessageStream = SerdeFramed<
Framed<UnixStream, LengthDelimitedCodec>,
Response,
Request,
Bincode<Response, Request>,
>;
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),
}

View File

@@ -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<MySQLDatabase>;
pub type CreateDatabasesResponse = BTreeMap<MySQLDatabase, Result<(), CreateDatabaseError>>;
#[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::<serde_json::Map<_, _>>();
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)
}
}
}
}

View File

@@ -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<MySQLUser>;
pub type CreateUsersResponse = BTreeMap<MySQLUser, Result<(), CreateUserError>>;
#[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::<serde_json::Map<_, _>>();
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)
}
}
}
}

View File

@@ -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<MySQLDatabase>;
pub type DropDatabasesResponse = BTreeMap<MySQLDatabase, Result<(), DropDatabaseError>>;
#[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::<serde_json::Map<_, _>>();
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)
}
}
}
}

View File

@@ -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<MySQLUser>;
pub type DropUsersResponse = BTreeMap<MySQLUser, Result<(), DropUserError>>;
#[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::<serde_json::Map<_, _>>();
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)
}
}
}
}

View File

@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use crate::server::sql::database_operations::DatabaseRow;
pub type ListAllDatabasesResponse = Result<Vec<DatabaseRow>, 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),
}
}
}

View File

@@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
use crate::core::database_privileges::DatabasePrivilegeRow;
pub type ListAllPrivilegesResponse =
Result<Vec<DatabasePrivilegeRow>, 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),
}
}
}

View File

@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};
use crate::server::sql::user_operations::DatabaseUser;
pub type ListAllUsersResponse = Result<Vec<DatabaseUser>, 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),
}
}
}

View File

@@ -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<Vec<MySQLDatabase>>;
pub type ListDatabasesResponse = BTreeMap<MySQLDatabase, Result<DatabaseRow, ListDatabasesError>>;
#[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)
}
}
}
}

View File

@@ -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<Vec<MySQLDatabase>>;
pub type ListPrivilegesResponse =
BTreeMap<MySQLDatabase, Result<Vec<DatabasePrivilegeRow>, 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)
}
}
}
}

View File

@@ -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<Vec<MySQLUser>>;
pub type ListUsersResponse = BTreeMap<MySQLUser, Result<DatabaseUser, ListUsersError>>;
#[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)
}
}
}
}

View File

@@ -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<MySQLUser>;
pub type LockUsersResponse = BTreeMap<MySQLUser, Result<(), LockUserError>>;
#[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::<serde_json::Map<_, _>>();
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)
}
}
}
}

View File

@@ -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<DatabasePrivilegesDiff>;
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
)
}
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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<MySQLUser>;
pub type UnlockUsersResponse = BTreeMap<MySQLUser, Result<(), UnlockUserError>>;
#[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::<serde_json::Map<_, _>>();
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)
}
}
}
}

View File

@@ -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<UnixStream, LengthDelimitedCodec>,
Request,
Response,
Bincode<Request, Response>,
>;
pub type ClientToServerMessageStream = SerdeFramed<
Framed<UnixStream, LengthDelimitedCodec>,
Response,
Request,
Bincode<Response, Request>,
>;
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<MySQLDatabase>),
DropDatabases(Vec<MySQLDatabase>),
ListDatabases(Option<Vec<MySQLDatabase>>),
ListPrivileges(Option<Vec<MySQLDatabase>>),
ModifyPrivileges(BTreeSet<DatabasePrivilegesDiff>),
CreateUsers(Vec<MySQLUser>),
DropUsers(Vec<MySQLUser>),
PasswdUser(MySQLUser, String),
ListUsers(Option<Vec<MySQLUser>>),
LockUsers(Vec<MySQLUser>),
UnlockUsers(Vec<MySQLUser>),
// 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),
}

View File

@@ -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: <prefix>_<{} 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,
}

View File

@@ -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: <prefix>_<{} 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<MySQLDatabase, Result<(), CreateDatabaseError>>;
#[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::<serde_json::Map<_, _>>();
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<MySQLDatabase, Result<(), DropDatabaseError>>;
#[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::<serde_json::Map<_, _>>();
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<MySQLDatabase, Result<DatabaseRow, ListDatabasesError>>;
#[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<Vec<DatabaseRow>, 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<MySQLDatabase, Result<Vec<DatabasePrivilegeRow>, 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<Vec<DatabasePrivilegeRow>, 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<MySQLUser, Result<(), CreateUserError>>;
#[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::<serde_json::Map<_, _>>();
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<MySQLUser, Result<(), DropUserError>>;
#[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::<serde_json::Map<_, _>>();
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<MySQLUser, Result<(), LockUserError>>;
#[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::<serde_json::Map<_, _>>();
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<MySQLUser, Result<(), UnlockUserError>>;
#[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::<serde_json::Map<_, _>>();
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<MySQLUser, Result<DatabaseUser, ListUsersError>>;
#[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<Vec<DatabaseUser>, 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),
}
}
}

View File

@@ -1,6 +1,6 @@
use crate::core::{ use crate::core::{
common::UnixUser, common::UnixUser,
protocol::server_responses::{NameValidationError, OwnerValidationError}, protocol::request_validation::{NameValidationError, OwnerValidationError},
}; };
const MAX_NAME_LENGTH: usize = 64; const MAX_NAME_LENGTH: usize = 64;

View File

@@ -18,7 +18,7 @@ use crate::server::sql::database_operations::list_databases;
use crate::{ use crate::{
core::{ core::{
common::{DEFAULT_SOCKET_PATH, UnixUser}, common::{DEFAULT_SOCKET_PATH, UnixUser},
protocol::request_response::{ protocol::{
Request, Response, ServerToClientMessageStream, create_server_to_client_message_stream, 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 // TODO: don't clone the request
let request_to_display = match &request { let request_to_display = match &request {
Request::PasswdUser(db_user, _) => { Request::PasswdUser((db_user, _)) => {
Request::PasswdUser(db_user.to_owned(), "<REDACTED>".to_string()) Request::PasswdUser((db_user.to_owned(), "<REDACTED>".to_string()))
} }
request => request.to_owned(), 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; let result = drop_database_users(db_users, unix_user, db_connection).await;
Response::DropUsers(result) Response::DropUsers(result)
} }
Request::PasswdUser(db_user, password) => { Request::PasswdUser((db_user, password)) => {
let result = let result =
set_password_for_database_user(&db_user, &password, unix_user, db_connection) set_password_for_database_user(&db_user, &password, unix_user, db_connection)
.await; .await;
Response::PasswdUser(result) Response::SetUserPassword(result)
} }
Request::ListUsers(db_users) => match db_users { Request::ListUsers(db_users) => match db_users {
Some(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 // TODO: don't clone the response
let response_to_display = match &response { let response_to_display = match &response {
Response::PasswdUser(Err(SetPasswordError::MySqlError(_))) => { Response::SetUserPassword(Err(SetPasswordError::MySqlError(_))) => {
Response::PasswdUser(Err(SetPasswordError::MySqlError("<REDACTED>".to_string()))) Response::SetUserPassword(Err(SetPasswordError::MySqlError(
"<REDACTED>".to_string(),
)))
} }
response => response.to_owned(), response => response.to_owned(),
}; };

View File

@@ -10,8 +10,9 @@ use crate::{
core::{ core::{
common::UnixUser, common::UnixUser,
protocol::{ protocol::{
CreateDatabaseError, CreateDatabasesOutput, DropDatabaseError, DropDatabasesOutput, CreateDatabaseError, CreateDatabasesResponse, DropDatabaseError, DropDatabasesResponse,
ListAllDatabasesError, ListAllDatabasesOutput, ListDatabasesError, ListDatabasesOutput, ListAllDatabasesError, ListAllDatabasesResponse, ListDatabasesError,
ListDatabasesResponse,
}, },
}, },
server::{ server::{
@@ -46,7 +47,7 @@ pub async fn create_databases(
database_names: Vec<MySQLDatabase>, database_names: Vec<MySQLDatabase>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> CreateDatabasesOutput { ) -> CreateDatabasesResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for database_name in database_names { for database_name in database_names {
@@ -105,7 +106,7 @@ pub async fn drop_databases(
database_names: Vec<MySQLDatabase>, database_names: Vec<MySQLDatabase>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> DropDatabasesOutput { ) -> DropDatabasesResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for database_name in database_names { for database_name in database_names {
@@ -177,7 +178,7 @@ pub async fn list_databases(
database_names: Vec<MySQLDatabase>, database_names: Vec<MySQLDatabase>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> ListDatabasesOutput { ) -> ListDatabasesResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for database_name in database_names { for database_name in database_names {
@@ -227,7 +228,7 @@ pub async fn list_databases(
pub async fn list_all_databases_for_user( pub async fn list_all_databases_for_user(
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> ListAllDatabasesOutput { ) -> ListAllDatabasesResponse {
let result = sqlx::query_as::<_, DatabaseRow>( let result = sqlx::query_as::<_, DatabaseRow>(
r#" r#"
SELECT `SCHEMA_NAME` AS `database` SELECT `SCHEMA_NAME` AS `database`

View File

@@ -28,9 +28,9 @@ use crate::{
DatabasePrivilegesDiff, DatabasePrivilegesDiff,
}, },
protocol::{ protocol::{
DiffDoesNotApplyError, GetAllDatabasesPrivilegeData, GetAllDatabasesPrivilegeDataError, DiffDoesNotApplyError, GetAllDatabasesPrivilegeDataError,
GetDatabasesPrivilegeData, GetDatabasesPrivilegeDataError, GetDatabasesPrivilegeDataError, ListAllPrivilegesResponse, ListPrivilegesResponse,
ModifyDatabasePrivilegesError, ModifyDatabasePrivilegesOutput, ModifyDatabasePrivilegesError, ModifyPrivilegesResponse,
}, },
types::{MySQLDatabase, MySQLUser}, types::{MySQLDatabase, MySQLUser},
}, },
@@ -139,7 +139,7 @@ pub async fn get_databases_privilege_data(
database_names: Vec<MySQLDatabase>, database_names: Vec<MySQLDatabase>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> GetDatabasesPrivilegeData { ) -> ListPrivilegesResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for database_name in database_names.iter() { 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( pub async fn get_all_database_privileges(
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> GetAllDatabasesPrivilegeData { ) -> ListAllPrivilegesResponse {
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!( let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
indoc! {r#" indoc! {r#"
SELECT {} FROM `db` WHERE `db` IN SELECT {} FROM `db` WHERE `db` IN
@@ -393,7 +393,7 @@ pub async fn apply_privilege_diffs(
database_privilege_diffs: BTreeSet<DatabasePrivilegesDiff>, database_privilege_diffs: BTreeSet<DatabasePrivilegesDiff>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> ModifyDatabasePrivilegesOutput { ) -> ModifyPrivilegesResponse {
let mut results: BTreeMap<(MySQLDatabase, MySQLUser), _> = BTreeMap::new(); let mut results: BTreeMap<(MySQLDatabase, MySQLUser), _> = BTreeMap::new();
for diff in database_privilege_diffs { for diff in database_privilege_diffs {

View File

@@ -12,9 +12,10 @@ use crate::{
common::UnixUser, common::UnixUser,
database_privileges::DATABASE_PRIVILEGE_FIELDS, database_privileges::DATABASE_PRIVILEGE_FIELDS,
protocol::{ protocol::{
CreateUserError, CreateUsersOutput, DropUserError, DropUsersOutput, ListAllUsersError, CreateUserError, CreateUsersResponse, DropUserError, DropUsersResponse,
ListAllUsersOutput, ListUsersError, ListUsersOutput, LockUserError, LockUsersOutput, ListAllUsersError, ListAllUsersResponse, ListUsersError, ListUsersResponse,
SetPasswordError, SetPasswordOutput, UnlockUserError, UnlockUsersOutput, LockUserError, LockUsersResponse, SetPasswordError, SetUserPasswordResponse,
UnlockUserError, UnlockUsersResponse,
}, },
types::MySQLUser, types::MySQLUser,
}, },
@@ -54,7 +55,7 @@ pub async fn create_database_users(
db_users: Vec<MySQLUser>, db_users: Vec<MySQLUser>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> CreateUsersOutput { ) -> CreateUsersResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for db_user in db_users { for db_user in db_users {
@@ -100,7 +101,7 @@ pub async fn drop_database_users(
db_users: Vec<MySQLUser>, db_users: Vec<MySQLUser>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> DropUsersOutput { ) -> DropUsersResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for db_user in db_users { for db_user in db_users {
@@ -147,7 +148,7 @@ pub async fn set_password_for_database_user(
password: &str, password: &str,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> SetPasswordOutput { ) -> SetUserPasswordResponse {
if let Err(err) = validate_name(db_user) { if let Err(err) = validate_name(db_user) {
return Err(SetPasswordError::SanitizationError(err)); return Err(SetPasswordError::SanitizationError(err));
} }
@@ -221,7 +222,7 @@ pub async fn lock_database_users(
db_users: Vec<MySQLUser>, db_users: Vec<MySQLUser>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> LockUsersOutput { ) -> LockUsersResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for db_user in db_users { for db_user in db_users {
@@ -281,7 +282,7 @@ pub async fn unlock_database_users(
db_users: Vec<MySQLUser>, db_users: Vec<MySQLUser>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> UnlockUsersOutput { ) -> UnlockUsersResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for db_user in db_users { for db_user in db_users {
@@ -380,7 +381,7 @@ pub async fn list_database_users(
db_users: Vec<MySQLUser>, db_users: Vec<MySQLUser>,
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> ListUsersOutput { ) -> ListUsersResponse {
let mut results = BTreeMap::new(); let mut results = BTreeMap::new();
for db_user in db_users { 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( pub async fn list_all_database_users_for_unix_user(
unix_user: &UnixUser, unix_user: &UnixUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> ListAllUsersOutput { ) -> ListAllUsersResponse {
let mut result = sqlx::query_as::<_, DatabaseUser>( let mut result = sqlx::query_as::<_, DatabaseUser>(
&(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `user`.`User` REGEXP ?"), &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `user`.`User` REGEXP ?"),
) )