create compatibility layer for mysql-admutils commands

This commit was merged in pull request #32.
This commit is contained in:
2024-08-05 22:37:23 +02:00
parent c473a4823e
commit 4353689a03
14 changed files with 688 additions and 79 deletions

View File

@@ -62,7 +62,30 @@ pub fn create_user_group_matching_regex(user: &User) -> String {
}
}
pub fn validate_prefix_for_user<'a>(name: &'a str, user: &User) -> anyhow::Result<&'a str> {
pub fn validate_name_token(name: &str) -> anyhow::Result<()> {
if name.is_empty() {
anyhow::bail!("Database name cannot be empty.");
}
if name.len() > 64 {
anyhow::bail!("Database name is too long. Maximum length is 64 characters.");
}
if !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-') {
anyhow::bail!(
indoc! {r#"
Invalid characters in name: '{}'
Only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted.
"#},
name
);
}
Ok(())
}
pub fn validate_ownership_by_user_prefix<'a>(name: &'a str, user: &User) -> anyhow::Result<&'a str> {
let user_groups = get_unix_groups(user)?;
let mut split_name = name.split('_');

View File

@@ -8,13 +8,12 @@ use serde::{Deserialize, Serialize};
use sqlx::{mysql::MySqlRow, prelude::*, MySqlConnection};
use super::common::{
create_user_group_matching_regex, get_current_unix_user, quote_identifier,
validate_prefix_for_user,
create_user_group_matching_regex, get_current_unix_user, quote_identifier, validate_name_token, validate_ownership_by_user_prefix
};
pub async fn create_database(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
let user = get_current_unix_user()?;
validate_ownership_of_database_name(name, &user)?;
validate_database_name(name, &user)?;
// NOTE: see the note about SQL injections in `validate_owner_of_database_name`
sqlx::query(&format!("CREATE DATABASE {}", quote_identifier(name)))
@@ -33,7 +32,7 @@ pub async fn create_database(name: &str, conn: &mut MySqlConnection) -> anyhow::
pub async fn drop_database(name: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
let user = get_current_unix_user()?;
validate_ownership_of_database_name(name, &user)?;
validate_database_name(name, &user)?;
// NOTE: see the note about SQL injections in `validate_owner_of_database_name`
sqlx::query(&format!("DROP DATABASE {}", quote_identifier(name)))
@@ -213,7 +212,7 @@ pub async fn get_database_privileges(
conn: &mut MySqlConnection,
) -> anyhow::Result<Vec<DatabasePrivileges>> {
let unix_user = get_current_unix_user()?;
validate_ownership_of_database_name(database_name, &unix_user)?;
validate_database_name(database_name, &unix_user)?;
let result = sqlx::query_as::<_, DatabasePrivileges>(&format!(
"SELECT {} FROM `db` WHERE `db` = ?",
@@ -391,28 +390,9 @@ pub async fn apply_permission_diffs(
/// 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_ownership_of_database_name(name: &str, user: &User) -> anyhow::Result<()> {
if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') {
anyhow::bail!(
indoc! {r#"
Database name '{}' contains invalid characters.
Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted.
"#},
name
);
}
if name.len() > 64 {
anyhow::bail!(
indoc! {r#"
Database name '{}' is too long.
Maximum length is 64 characters.
"#},
name
);
}
validate_prefix_for_user(name, user).context("Invalid database name")?;
pub fn validate_database_name(name: &str, user: &User) -> anyhow::Result<()> {
validate_name_token(name).context("Invalid database name")?;
validate_ownership_by_user_prefix(name, user).context("Invalid database name")?;
Ok(())
}

View File

@@ -1,19 +1,44 @@
use anyhow::Context;
use indoc::indoc;
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_prefix_for_user};
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_ownership_of_user_name(db_user, &unix_user)?;
validate_user_name(db_user, &unix_user)?;
// NOTE: see the note about SQL injections in `validate_ownershipt_of_user_name`
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?;
@@ -24,9 +49,13 @@ pub async fn create_database_user(db_user: &str, conn: &mut MySqlConnection) ->
pub async fn delete_database_user(db_user: &str, conn: &mut MySqlConnection) -> anyhow::Result<()> {
let unix_user = get_current_unix_user()?;
validate_ownership_of_user_name(db_user, &unix_user)?;
validate_user_name(db_user, &unix_user)?;
// NOTE: see the note about SQL injections in `validate_ownershipt_of_user_name`
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?;
@@ -40,9 +69,13 @@ pub async fn set_password_for_database_user(
conn: &mut MySqlConnection,
) -> anyhow::Result<()> {
let unix_user = crate::core::common::get_current_unix_user()?;
validate_ownership_of_user_name(db_user, &unix_user)?;
validate_user_name(db_user, &unix_user)?;
// NOTE: see the note about SQL injections in `validate_ownershipt_of_user_name`
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 {}",
@@ -57,6 +90,27 @@ pub async fn set_password_for_database_user(
Ok(())
}
#[derive(sqlx::FromRow)]
#[sqlx(transparent)]
pub struct PasswordIsSet(bool);
pub async fn password_is_set_for_database_user(
db_user: &str,
conn: &mut MySqlConnection,
) -> anyhow::Result<Option<bool>> {
let unix_user = crate::core::common::get_current_unix_user()?;
validate_user_name(db_user, &unix_user)?;
let user_has_password = sqlx::query_as::<_, PasswordIsSet>(
"SELECT authentication_string != '' FROM mysql.user WHERE User = ?"
)
.bind(db_user)
.fetch_optional(conn)
.await?;
Ok(user_has_password.map(|PasswordIsSet(is_set)| is_set))
}
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
pub struct DatabaseUser {
#[sqlx(rename = "User")]
@@ -111,29 +165,9 @@ pub async fn get_database_user_for_user(
/// 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_ownership_of_user_name(name: &str, user: &User) -> anyhow::Result<()> {
if name.contains(|c: char| !c.is_ascii_alphanumeric() && c != '_' && c != '-') {
anyhow::bail!(
indoc! {r#"
Username '{}' contains invalid characters.
Only A-Z, a-z, 0-9, _ (underscore) and - (dash) permitted.
"#},
name
);
}
// TODO: does the name have a length limit?
// if name.len() > 48 {
// anyhow::bail!(
// indoc! {r#"
// Username '{}' is too long.
// Maximum length is 48 characters. Skipping.
// "#},
// name
// );
// }
validate_prefix_for_user(name, user).context(format!("Invalid username: '{}'", name))?;
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(())
}