Files
mysqladm-rs/src/core/user_operations.rs
2024-08-07 17:22:23 +02:00

167 lines
4.6 KiB
Rust

use anyhow::Context;
use nix::unistd::User;
use serde::{Deserialize, Serialize};
use sqlx::{prelude::*, MySqlConnection};
use crate::core::common::quote_literal;
use super::common::{
create_user_group_matching_regex, get_current_unix_user, validate_name_token,
validate_ownership_by_user_prefix,
};
pub async fn user_exists(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<bool> {
let unix_user = get_current_unix_user()?;
validate_user_name(db_user, &unix_user)?;
let user_exists = sqlx::query(
r#"
SELECT EXISTS(
SELECT 1
FROM `mysql`.`user`
WHERE `User` = ?
)
"#,
)
.bind(db_user)
.fetch_one(conn)
.await?
.get::<bool, _>(0);
Ok(user_exists)
}
pub async fn create_database_user(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
let unix_user = get_current_unix_user()?;
validate_user_name(db_user, &unix_user)?;
if user_exists(db_user, conn).await? {
anyhow::bail!("User '{}' already exists", db_user);
}
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
sqlx::query(format!("CREATE USER {}@'%'", quote_literal(db_user),).as_str())
.execute(conn)
.await?;
Ok(())
}
pub async fn delete_database_user(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
let unix_user = get_current_unix_user()?;
validate_user_name(db_user, &unix_user)?;
if !user_exists(db_user, conn).await? {
anyhow::bail!("User '{}' does not exist", db_user);
}
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
sqlx::query(format!("DROP USER {}@'%'", quote_literal(db_user),).as_str())
.execute(conn)
.await?;
Ok(())
}
pub async fn set_password_for_database_user(
db_user: &str,
password: &str,
conn: &mut MySqlConnection,
) -> anyhow::Result<()> {
let unix_user = crate::core::common::get_current_unix_user()?;
validate_user_name(db_user, &unix_user)?;
if !user_exists(db_user, conn).await? {
anyhow::bail!("User '{}' does not exist", db_user);
}
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
sqlx::query(
format!(
"ALTER USER {}@'%' IDENTIFIED BY {}",
quote_literal(db_user),
quote_literal(password).as_str()
)
.as_str(),
)
.execute(conn)
.await?;
Ok(())
}
/// This struct contains information about a database user.
/// This can be extended if we need more information in the future.
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct DatabaseUser {
#[sqlx(rename = "User")]
pub user: String,
#[serde(skip)]
#[sqlx(rename = "Host")]
pub host: String,
#[sqlx(rename = "`Password` != '' OR `authentication_string` != ''")]
pub has_password: bool,
}
/// This function fetches all database users that have a prefix matching the
/// unix username and group names of the given unix user.
pub async fn get_all_database_users_for_unix_user(
unix_user: &User,
conn: &mut MySqlConnection,
) -> anyhow::Result<Vec<DatabaseUser>> {
let users = sqlx::query_as::<_, DatabaseUser>(
r#"
SELECT
`User`,
`Host`,
`Password` != '' OR `authentication_string` != ''
FROM `mysql`.`user`
WHERE `User` REGEXP ?
"#,
)
.bind(create_user_group_matching_regex(unix_user))
.fetch_all(conn)
.await?;
Ok(users)
}
/// This function fetches a database user if it exists.
pub async fn get_database_user_for_user(
username: &str,
conn: &mut MySqlConnection,
) -> anyhow::Result<Option<DatabaseUser>> {
let user = sqlx::query_as::<_, DatabaseUser>(
r#"
SELECT
`User`,
`Host`,
`Password` != '' OR `authentication_string` != ''
FROM `mysql`.`user`
WHERE `User` = ?
"#,
)
.bind(username)
.fetch_optional(conn)
.await?;
Ok(user)
}
/// NOTE: It is very critical that this function validates the database name
/// properly. MySQL does not seem to allow for prepared statements, binding
/// the database name as a parameter to the query. This means that we have
/// to validate the database name ourselves to prevent SQL injection.
pub fn validate_user_name(name: &str, user: &User) -> anyhow::Result<()> {
validate_name_token(name).context(format!("Invalid username: '{}'", name))?;
validate_ownership_by_user_prefix(name, user)
.context(format!("Invalid username: '{}'", name))?;
Ok(())
}