Rewrite entire codebase to split into client and server
This commit was merged in pull request #55.
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use sqlx::prelude::*;
|
||||
use sqlx::MySqlConnection;
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
common::UnixUser,
|
||||
protocol::{
|
||||
CreateUserError, CreateUsersOutput, DropUserError, DropUsersOutput, ListAllUsersError,
|
||||
ListAllUsersOutput, ListUsersError, ListUsersOutput, LockUserError, LockUsersOutput,
|
||||
SetPasswordError, SetPasswordOutput, UnlockUserError, UnlockUsersOutput,
|
||||
},
|
||||
},
|
||||
server::{
|
||||
common::create_user_group_matching_regex,
|
||||
input_sanitization::{quote_literal, validate_name, validate_ownership_by_unix_user},
|
||||
},
|
||||
};
|
||||
|
||||
// NOTE: this function is unsafe because it does no input validation.
|
||||
async fn unsafe_user_exists(
|
||||
db_user: &str,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM `mysql`.`user`
|
||||
WHERE `User` = ?
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(db_user)
|
||||
.fetch_one(connection)
|
||||
.await
|
||||
.map(|row| row.get::<bool, _>(0))
|
||||
}
|
||||
|
||||
pub async fn create_database_users(
|
||||
db_users: Vec<String>,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> CreateUsersOutput {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) = validate_name(&db_user) {
|
||||
results.insert(db_user, Err(CreateUserError::SanitizationError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
|
||||
results.insert(db_user, Err(CreateUserError::OwnershipError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_user_exists(&db_user, &mut *connection).await {
|
||||
Ok(true) => {
|
||||
results.insert(db_user, Err(CreateUserError::UserAlreadyExists));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(db_user, Err(CreateUserError::MySqlError(err.to_string())));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let result = sqlx::query(format!("CREATE USER {}@'%'", quote_literal(&db_user),).as_str())
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| CreateUserError::MySqlError(err.to_string()));
|
||||
|
||||
results.insert(db_user, result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn drop_database_users(
|
||||
db_users: Vec<String>,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> DropUsersOutput {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) = validate_name(&db_user) {
|
||||
results.insert(db_user, Err(DropUserError::SanitizationError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
|
||||
results.insert(db_user, Err(DropUserError::OwnershipError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_user_exists(&db_user, &mut *connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(db_user, Err(DropUserError::UserDoesNotExist));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(db_user, Err(DropUserError::MySqlError(err.to_string())));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let result = sqlx::query(format!("DROP USER {}@'%'", quote_literal(&db_user),).as_str())
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| DropUserError::MySqlError(err.to_string()));
|
||||
|
||||
results.insert(db_user, result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn set_password_for_database_user(
|
||||
db_user: &str,
|
||||
password: &str,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> SetPasswordOutput {
|
||||
if let Err(err) = validate_name(db_user) {
|
||||
return Err(SetPasswordError::SanitizationError(err));
|
||||
}
|
||||
|
||||
if let Err(err) = validate_ownership_by_unix_user(db_user, unix_user) {
|
||||
return Err(SetPasswordError::OwnershipError(err));
|
||||
}
|
||||
|
||||
match unsafe_user_exists(db_user, &mut *connection).await {
|
||||
Ok(false) => return Err(SetPasswordError::UserDoesNotExist),
|
||||
Err(err) => return Err(SetPasswordError::MySqlError(err.to_string())),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
sqlx::query(
|
||||
format!(
|
||||
"ALTER USER {}@'%' IDENTIFIED BY {}",
|
||||
quote_literal(db_user),
|
||||
quote_literal(password).as_str()
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()))
|
||||
}
|
||||
|
||||
// NOTE: this function is unsafe because it does no input validation.
|
||||
async fn database_user_is_locked_unsafe(
|
||||
db_user: &str,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query(
|
||||
r#"
|
||||
SELECT COALESCE(
|
||||
JSON_EXTRACT(`mysql`.`global_priv`.`priv`, "$.account_locked"),
|
||||
'false'
|
||||
) != 'false'
|
||||
FROM `mysql`.`global_priv`
|
||||
WHERE `User` = ?
|
||||
AND `Host` = '%'
|
||||
"#,
|
||||
)
|
||||
.bind(db_user)
|
||||
.fetch_one(connection)
|
||||
.await
|
||||
.map(|row| row.get::<bool, _>(0))
|
||||
}
|
||||
|
||||
pub async fn lock_database_users(
|
||||
db_users: Vec<String>,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> LockUsersOutput {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) = validate_name(&db_user) {
|
||||
results.insert(db_user, Err(LockUserError::SanitizationError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
|
||||
results.insert(db_user, Err(LockUserError::OwnershipError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_user_exists(&db_user, &mut *connection).await {
|
||||
Ok(true) => {}
|
||||
Ok(false) => {
|
||||
results.insert(db_user, Err(LockUserError::UserDoesNotExist));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(db_user, Err(LockUserError::MySqlError(err.to_string())));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match database_user_is_locked_unsafe(&db_user, &mut *connection).await {
|
||||
Ok(false) => {}
|
||||
Ok(true) => {
|
||||
results.insert(db_user, Err(LockUserError::UserIsAlreadyLocked));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(db_user, Err(LockUserError::MySqlError(err.to_string())));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let result = sqlx::query(
|
||||
format!("ALTER USER {}@'%' ACCOUNT LOCK", quote_literal(&db_user),).as_str(),
|
||||
)
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| LockUserError::MySqlError(err.to_string()));
|
||||
|
||||
results.insert(db_user, result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn unlock_database_users(
|
||||
db_users: Vec<String>,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> UnlockUsersOutput {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) = validate_name(&db_user) {
|
||||
results.insert(db_user, Err(UnlockUserError::SanitizationError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
|
||||
results.insert(db_user, Err(UnlockUserError::OwnershipError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_user_exists(&db_user, &mut *connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(db_user, Err(UnlockUserError::UserDoesNotExist));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(db_user, Err(UnlockUserError::MySqlError(err.to_string())));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match database_user_is_locked_unsafe(&db_user, &mut *connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(db_user, Err(UnlockUserError::UserIsAlreadyUnlocked));
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(db_user, Err(UnlockUserError::MySqlError(err.to_string())));
|
||||
continue;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let result = sqlx::query(
|
||||
format!("ALTER USER {}@'%' ACCOUNT UNLOCK", quote_literal(&db_user),).as_str(),
|
||||
)
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| UnlockUserError::MySqlError(err.to_string()));
|
||||
|
||||
results.insert(db_user, result);
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// This struct contains information about a database user.
|
||||
/// This can be extended if we need more information in the future.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, FromRow)]
|
||||
pub struct DatabaseUser {
|
||||
#[sqlx(rename = "User")]
|
||||
pub user: String,
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[serde(skip)]
|
||||
#[sqlx(rename = "Host")]
|
||||
pub host: String,
|
||||
|
||||
#[sqlx(rename = "has_password")]
|
||||
pub has_password: bool,
|
||||
|
||||
#[sqlx(rename = "is_locked")]
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
const DB_USER_SELECT_STATEMENT: &str = r#"
|
||||
SELECT
|
||||
`mysql`.`user`.`User`,
|
||||
`mysql`.`user`.`Host`,
|
||||
`mysql`.`user`.`Password` != '' OR `mysql`.`user`.`authentication_string` != '' AS `has_password`,
|
||||
COALESCE(
|
||||
JSON_EXTRACT(`mysql`.`global_priv`.`priv`, "$.account_locked"),
|
||||
'false'
|
||||
) != 'false' AS `is_locked`
|
||||
FROM `mysql`.`user`
|
||||
JOIN `mysql`.`global_priv` ON
|
||||
`mysql`.`user`.`User` = `mysql`.`global_priv`.`User`
|
||||
AND `mysql`.`user`.`Host` = `mysql`.`global_priv`.`Host`
|
||||
"#;
|
||||
|
||||
pub async fn list_database_users(
|
||||
db_users: Vec<String>,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> ListUsersOutput {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) = validate_name(&db_user) {
|
||||
results.insert(db_user, Err(ListUsersError::SanitizationError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_ownership_by_unix_user(&db_user, unix_user) {
|
||||
results.insert(db_user, Err(ListUsersError::OwnershipError(err)));
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = sqlx::query_as::<_, DatabaseUser>(
|
||||
&(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` = ?"),
|
||||
)
|
||||
.bind(&db_user)
|
||||
.fetch_optional(&mut *connection)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(Some(user)) => results.insert(db_user, Ok(user)),
|
||||
Ok(None) => results.insert(db_user, Err(ListUsersError::UserDoesNotExist)),
|
||||
Err(err) => results.insert(db_user, Err(ListUsersError::MySqlError(err.to_string()))),
|
||||
};
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub async fn list_all_database_users_for_unix_user(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> ListAllUsersOutput {
|
||||
sqlx::query_as::<_, DatabaseUser>(
|
||||
&(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` REGEXP ?"),
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.fetch_all(connection)
|
||||
.await
|
||||
.map_err(|err| ListAllUsersError::MySqlError(err.to_string()))
|
||||
}
|
||||
Reference in New Issue
Block a user