5 Commits

Author SHA1 Message Date
oysteikt f16239aceb server/sql: fixes for new sqlx crate version
Build and test / check-license (push) Successful in 49s
Build and test / check (push) Successful in 1m51s
Build and test / build (push) Successful in 2m42s
Build and test / test (push) Successful in 5m2s
Build and test / docs (push) Successful in 7m6s
2026-05-31 00:24:53 +09:00
oysteikt 8f475eced1 CHANGELOG.md: add release notes, Cargo.toml: bump version number
Build and test / check-license (push) Successful in 49s
Build and test / check (push) Failing after 1m57s
Build and test / test (push) Failing after 2m53s
Build and test / build (push) Failing after 3m11s
Build and test / docs (push) Failing after 4m15s
2026-05-31 00:09:49 +09:00
oysteikt 6849e99c11 flake.lock: bump, Cargo.{toml,lock}: update inputs 2026-05-31 00:09:40 +09:00
oysteikt 759df9ef42 server/sql: flush privileges after modification
Build and test / check-license (push) Successful in 54s
Build and test / check (push) Successful in 1m42s
Build and test / test (push) Successful in 3m34s
Build and test / build (push) Successful in 3m39s
Build and test / docs (push) Successful in 5m33s
2026-04-28 19:10:16 +09:00
oysteikt a64d1fa1bf scripts/download-and-upload-debs: fix download path
Build and test / check-license (push) Successful in 47s
Build and test / check (push) Successful in 2m20s
Build and test / build (push) Successful in 3m6s
Build and test / test (push) Successful in 3m12s
Build and test / docs (push) Successful in 6m20s
2026-04-28 18:32:28 +09:00
9 changed files with 356 additions and 548 deletions
+10
View File
@@ -1,5 +1,15 @@
# Changelog # Changelog
## v1.0.2
Patch release with an important bug fix
### Notable changes
- Run `FLUSH PRIVILEGES` on the server whenever users modify privileges.
- You will have to grant `RELOAD` for the muscl admin user on all databases, see the [installation docs](./docs/installation.md) for details.
- Bump dependencies
## v1.0.1 ## v1.0.1
Patch release with some important bug fixes Patch release with some important bug fixes
Generated
+195 -418
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "muscl" name = "muscl"
version = "1.0.1" version = "1.0.2"
edition = "2024" edition = "2024"
resolver = "2" resolver = "2"
license = "BSD-3-Clause" license = "BSD-3-Clause"
@@ -23,7 +23,7 @@ async-bincode = "0.8.0"
bincode = "2.0.1" bincode = "2.0.1"
clap = { version = "4.6.1", features = ["cargo", "derive"] } clap = { version = "4.6.1", features = ["cargo", "derive"] }
clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ] } clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ] }
clap_complete = { version = "4.6.3", features = ["unstable-dynamic"] } clap_complete = { version = "4.6.5", features = ["unstable-dynamic"] }
color-print = "0.3.7" color-print = "0.3.7"
const_format = "0.2.36" const_format = "0.2.36"
derive_more = { version = "2.1.1", features = ["display", "error"] } derive_more = { version = "2.1.1", features = ["display", "error"] }
@@ -32,32 +32,32 @@ futures-util = "0.3.32"
humansize = "2.1.3" humansize = "2.1.3"
indoc = "2.0.7" indoc = "2.0.7"
itertools = "0.14.0" itertools = "0.14.0"
nix = { version = "0.31.2", features = ["fs", "process", "socket", "user"] } nix = { version = "0.31.3", features = ["fs", "process", "socket", "user"] }
num_cpus = "1.17.0" num_cpus = "1.17.0"
prettytable = "0.10.0" prettytable = "0.10.0"
rand = "0.10.1" rand = "0.10.1"
serde = "1.0.228" serde = "1.0.228"
serde_json = { version = "1.0.149", features = ["preserve_order"] } serde_json = { version = "1.0.150", features = ["preserve_order"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "mysql", "tls-rustls"] } sqlx = { version = "0.9.0", features = ["runtime-tokio", "mysql", "tls-rustls"] }
thiserror = "2.0.18" thiserror = "2.0.18"
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "signal"] } tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "signal"] }
tokio-serde = { version = "0.9.0", features = ["bincode"] } tokio-serde = { version = "0.9.0", features = ["bincode"] }
tokio-stream = "0.1.18" tokio-stream = "0.1.18"
tokio-util = { version = "0.7.18", features = ["codec", "rt"] } tokio-util = { version = "0.7.18", features = ["codec", "rt"] }
toml = "1.1.2" toml = "1.1.2"
tracing = { version = "0.1.44", features = ["log"] } tracing = { version = "0.1.44", features = ["log"] }
tracing-subscriber = "0.3.23" tracing-subscriber = "0.3.23"
uuid = { version = "1.23.1", features = ["v4"] } uuid = { version = "1.23.2", features = ["v4"] }
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
landlock = "0.4.4" landlock = "0.4.5"
sd-notify = "0.5.0" sd-notify = "0.5.0"
tracing-journald = "0.3.2" tracing-journald = "0.3.2"
[build-dependencies] [build-dependencies]
anyhow = "1.0.102" anyhow = "1.0.102"
build-info-build = "0.0.44" build-info-build = "0.0.44"
git2 = { version = "0.20.4", default-features = false } git2 = { version = "0.21.0", default-features = false }
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.1" pretty_assertions = "1.4.1"
+1 -1
View File
@@ -42,7 +42,7 @@ on the MySQL server as the admin user (or another user with sufficient privilege
```sql ```sql
CREATE USER `muscl`@`localhost` IDENTIFIED BY '<strong_password_here>'; CREATE USER `muscl`@`localhost` IDENTIFIED BY '<strong_password_here>';
GRANT SELECT, INSERT, UPDATE, DELETE ON `mysql`.* TO `muscl`@`localhost`; GRANT SELECT, INSERT, UPDATE, DELETE ON `mysql`.* TO `muscl`@`localhost`;
GRANT GRANT OPTION, CREATE, DROP ON *.* TO `muscl`@`localhost`; GRANT GRANT OPTION, CREATE, DROP, RELOAD ON *.* TO `muscl`@`localhost`;
FLUSH PRIVILEGES; FLUSH PRIVILEGES;
``` ```
Generated
+9 -9
View File
@@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1774313767, "lastModified": 1780099841,
"narHash": "sha256-hy0XTQND6avzGEUFrJtYBBpFa/POiiaGBr2vpU6Y9tY=", "narHash": "sha256-EVZd2RsbpreRUDSi9rBwPY+ZxoyMaiEBbZxxhljbaS4=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "3d9df76e29656c679c744968b17fbaf28f0e923d", "rev": "0532eb17955225173906d671fb36306bdeb1e2dc",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -17,11 +17,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1775036866, "lastModified": 1779560665,
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -45,11 +45,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1775099554, "lastModified": 1780110990,
"narHash": "sha256-3xBsGnGDLOFtnPZ1D3j2LU19wpAlYefRKTlkv648rU0=", "narHash": "sha256-6QBThUi7SuK+dgA+DCaEkQGZN4kYx6DpXmK45+MG9zI=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "8d6387ed6d8e6e6672fd3ed4b61b59d44b124d99", "rev": "85570ef134d92a8702de6afd1f6f0209c863fa91",
"type": "github" "type": "github"
}, },
"original": { "original": {
+1 -1
View File
@@ -52,7 +52,7 @@ declare -a OS_VARIANTS=(
for variant in "${OS_VARIANTS[@]}"; do for variant in "${OS_VARIANTS[@]}"; do
echo "Downloading and uploading debs for variant: $variant" echo "Downloading and uploading debs for variant: $variant"
curl "https://git.pvv.ntnu.no/Projects/muscl/actions/runs/$RUN_NUMBER/artifacts/muscl-deb-$variant-$GIT_SHA.zip" --output "$TMPDIR/muscl-deb-$variant-$GIT_SHA.zip" curl "https://git.pvv.ntnu.no/Projects/muscl/actions/runs/$RUN_NUMBER/artifacts/muscl-deb-$variant-$GIT_SHA" --output "$TMPDIR/muscl-deb-$variant-$GIT_SHA.zip"
unzip "$TMPDIR/muscl-deb-$variant-$GIT_SHA.zip" -d "$TMPDIR/muscl-deb-$variant-$GIT_SHA" unzip "$TMPDIR/muscl-deb-$variant-$GIT_SHA.zip" -d "$TMPDIR/muscl-deb-$variant-$GIT_SHA"
+19 -12
View File
@@ -1,5 +1,6 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use sqlx::AssertSqlSafe;
use sqlx::MySqlConnection; use sqlx::MySqlConnection;
use sqlx::prelude::*; use sqlx::prelude::*;
@@ -125,12 +126,15 @@ pub async fn create_databases(
_ => {} _ => {}
} }
let result = let statement = AssertSqlSafe(format!(
sqlx::query(format!("CREATE DATABASE {}", quote_identifier(&database_name)).as_str()) "CREATE DATABASE {}",
.execute(&mut *connection) quote_identifier(&database_name)
.await ));
.map(|_| ()) let result = sqlx::query(statement)
.map_err(|err| CreateDatabaseError::MySqlError(err.to_string())); .execute(&mut *connection)
.await
.map(|_| ())
.map_err(|err| CreateDatabaseError::MySqlError(err.to_string()));
if let Err(err) = &result { if let Err(err) = &result {
tracing::error!("Failed to create database '{}': {:?}", &database_name, err); tracing::error!("Failed to create database '{}': {:?}", &database_name, err);
@@ -181,12 +185,15 @@ pub async fn drop_databases(
_ => {} _ => {}
} }
let result = let statement = AssertSqlSafe(format!(
sqlx::query(format!("DROP DATABASE {}", quote_identifier(&database_name)).as_str()) "DROP DATABASE {}",
.execute(&mut *connection) quote_identifier(&database_name)
.await ));
.map(|_| ()) let result = sqlx::query(statement)
.map_err(|err| DropDatabaseError::MySqlError(err.to_string())); .execute(&mut *connection)
.await
.map(|_| ())
.map_err(|err| DropDatabaseError::MySqlError(err.to_string()));
if let Err(err) = &result { if let Err(err) = &result {
tracing::error!("Failed to drop database '{}': {:?}", &database_name, err); tracing::error!("Failed to drop database '{}': {:?}", &database_name, err);
+47 -38
View File
@@ -18,7 +18,7 @@ use std::collections::{BTreeMap, BTreeSet};
use indoc::indoc; use indoc::indoc;
use itertools::Itertools; use itertools::Itertools;
use sqlx::{MySqlConnection, mysql::MySqlRow, prelude::*}; use sqlx::{AssertSqlSafe, MySqlConnection, mysql::MySqlRow, prelude::*};
use crate::{ use crate::{
core::{ core::{
@@ -84,16 +84,17 @@ async fn unsafe_get_database_privileges(
database_name: &str, database_name: &str,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> Result<Vec<DatabasePrivilegeRow>, sqlx::Error> { ) -> Result<Vec<DatabasePrivilegeRow>, sqlx::Error> {
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!( let statement = AssertSqlSafe(format!(
"SELECT {} FROM `db` WHERE `Db` = ?", "SELECT {} FROM `db` WHERE `Db` = ?",
DATABASE_PRIVILEGE_FIELDS DATABASE_PRIVILEGE_FIELDS
.iter() .iter()
.map(|field| quote_identifier(field)) .map(|field| quote_identifier(field))
.join(","), .join(","),
)) ));
.bind(database_name) let result = sqlx::query_as::<_, DatabasePrivilegeRow>(statement)
.fetch_all(connection) .bind(database_name)
.await; .fetch_all(connection)
.await;
if let Err(e) = &result { if let Err(e) = &result {
tracing::error!( tracing::error!(
@@ -113,17 +114,18 @@ pub async fn unsafe_get_database_privileges_for_db_user_pair(
user_name: &MySQLUser, user_name: &MySQLUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> Result<Option<DatabasePrivilegeRow>, sqlx::Error> { ) -> Result<Option<DatabasePrivilegeRow>, sqlx::Error> {
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!( let statement = AssertSqlSafe(format!(
"SELECT {} FROM `db` WHERE `Db` = ? AND `User` = ? AND `Host` = '%'", "SELECT {} FROM `db` WHERE `Db` = ? AND `User` = ? AND `Host` = '%'",
DATABASE_PRIVILEGE_FIELDS DATABASE_PRIVILEGE_FIELDS
.iter() .iter()
.map(|field| quote_identifier(field)) .map(|field| quote_identifier(field))
.join(","), .join(","),
)) ));
.bind(database_name.as_str()) let result = sqlx::query_as::<_, DatabasePrivilegeRow>(statement)
.bind(user_name.as_str()) .bind(database_name.as_str())
.fetch_optional(connection) .bind(user_name.as_str())
.await; .fetch_optional(connection)
.await;
if let Err(e) = &result { if let Err(e) = &result {
tracing::error!( tracing::error!(
@@ -189,8 +191,8 @@ pub async fn get_databases_privilege_data(
} }
/// TODO: make this constant /// TODO: make this constant
fn get_all_db_privs_query() -> String { fn get_all_db_privs_query() -> AssertSqlSafe<String> {
format!( AssertSqlSafe(format!(
indoc! {r" indoc! {r"
SELECT {} FROM `db` WHERE `db` IN SELECT {} FROM `db` WHERE `db` IN
(SELECT DISTINCT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database` (SELECT DISTINCT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database`
@@ -202,7 +204,7 @@ fn get_all_db_privs_query() -> String {
.iter() .iter()
.map(|field| quote_identifier(field)) .map(|field| quote_identifier(field))
.join(","), .join(","),
) ))
} }
/// Get all database + user + privileges pairs that are owned by the current user. /// Get all database + user + privileges pairs that are owned by the current user.
@@ -212,7 +214,7 @@ pub async fn get_all_database_privileges(
_db_is_mariadb: bool, _db_is_mariadb: bool,
group_denylist: &GroupDenylist, group_denylist: &GroupDenylist,
) -> ListAllPrivilegesResponse { ) -> ListAllPrivilegesResponse {
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&get_all_db_privs_query()) let result = sqlx::query_as::<_, DatabasePrivilegeRow>(get_all_db_privs_query())
.bind(create_user_group_matching_regex(unix_user, group_denylist)) .bind(create_user_group_matching_regex(unix_user, group_denylist))
.fetch_all(connection) .fetch_all(connection)
.await .await
@@ -241,7 +243,10 @@ async fn unsafe_apply_privilege_diff(
let question_marks = let question_marks =
std::iter::repeat_n("?", DATABASE_PRIVILEGE_FIELDS.len() + 1).join(","); std::iter::repeat_n("?", DATABASE_PRIVILEGE_FIELDS.len() + 1).join(",");
sqlx::query(format!("INSERT INTO `db` ({tables}) VALUES ({question_marks})").as_str()) let statement = AssertSqlSafe(format!(
"INSERT INTO `db` ({tables}) VALUES ({question_marks})"
));
sqlx::query(statement)
.bind(p.db.to_string()) .bind(p.db.to_string())
.bind(p.user.to_string()) .bind(p.user.to_string())
.bind(yn(p.select_priv)) .bind(yn(p.select_priv))
@@ -280,27 +285,27 @@ async fn unsafe_apply_privilege_diff(
} }
} }
sqlx::query( let statement = AssertSqlSafe(format!(
format!("UPDATE `db` SET {changes} WHERE `Db` = ? AND `User` = ? AND `Host` = ?") "UPDATE `db` SET {changes} WHERE `Db` = ? AND `User` = ? AND `Host` = ?"
.as_str(), ));
) sqlx::query(statement)
.bind(p.select_priv.map(change_to_yn)) .bind(p.select_priv.map(change_to_yn))
.bind(p.insert_priv.map(change_to_yn)) .bind(p.insert_priv.map(change_to_yn))
.bind(p.update_priv.map(change_to_yn)) .bind(p.update_priv.map(change_to_yn))
.bind(p.delete_priv.map(change_to_yn)) .bind(p.delete_priv.map(change_to_yn))
.bind(p.create_priv.map(change_to_yn)) .bind(p.create_priv.map(change_to_yn))
.bind(p.drop_priv.map(change_to_yn)) .bind(p.drop_priv.map(change_to_yn))
.bind(p.alter_priv.map(change_to_yn)) .bind(p.alter_priv.map(change_to_yn))
.bind(p.index_priv.map(change_to_yn)) .bind(p.index_priv.map(change_to_yn))
.bind(p.create_tmp_table_priv.map(change_to_yn)) .bind(p.create_tmp_table_priv.map(change_to_yn))
.bind(p.lock_tables_priv.map(change_to_yn)) .bind(p.lock_tables_priv.map(change_to_yn))
.bind(p.references_priv.map(change_to_yn)) .bind(p.references_priv.map(change_to_yn))
.bind(p.db.to_string()) .bind(p.db.to_string())
.bind(p.user.to_string()) .bind(p.user.to_string())
.bind("%") .bind("%")
.execute(connection) .execute(connection)
.await .await
.map(|_| ()) .map(|_| ())
} }
DatabasePrivilegesDiff::Deleted(p) => { DatabasePrivilegesDiff::Deleted(p) => {
sqlx::query("DELETE FROM `db` WHERE `Db` = ? AND `User` = ? AND `Host` = ?") sqlx::query("DELETE FROM `db` WHERE `Db` = ? AND `User` = ? AND `Host` = ?")
@@ -487,6 +492,10 @@ pub async fn apply_privilege_diffs(
results.insert(key, result); results.insert(key, result);
} }
if let Err(err) = connection.execute("FLUSH PRIVILEGES").await {
tracing::error!("Failed to flush privileges: {}", err);
}
results results
.into_iter() .into_iter()
.map(|((k1, k2), v)| (k1, (k2, v))) .map(|((k1, k2), v)| (k1, (k2, v)))
+65 -60
View File
@@ -1,5 +1,6 @@
use indoc::formatdoc; use indoc::formatdoc;
use itertools::Itertools; use itertools::Itertools;
use sqlx::AssertSqlSafe;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -126,7 +127,8 @@ pub async fn create_database_users(
_ => {} _ => {}
} }
let result = sqlx::query(format!("CREATE USER {}@'%'", quote_literal(&db_user),).as_str()) let statement = AssertSqlSafe(format!("CREATE USER {}@'%'", quote_literal(&db_user),));
let result = sqlx::query(statement)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -172,7 +174,8 @@ pub async fn drop_database_users(
_ => {} _ => {}
} }
let result = sqlx::query(format!("DROP USER {}@'%'", quote_literal(&db_user),).as_str()) let statement = AssertSqlSafe(format!("DROP USER {}@'%'", quote_literal(&db_user),));
let result = sqlx::query(statement)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -205,18 +208,16 @@ pub async fn set_password_for_database_user(
_ => {} _ => {}
} }
let result = sqlx::query( let statement = AssertSqlSafe(format!(
format!( "ALTER USER {}@'%' IDENTIFIED BY {}",
"ALTER USER {}@'%' IDENTIFIED BY {}", quote_literal(db_user),
quote_literal(db_user), quote_literal(password).as_str(),
quote_literal(password).as_str(), ));
) let result = sqlx::query(statement)
.as_str(), .execute(&mut *connection)
) .await
.execute(&mut *connection) .map(|_| ())
.await .map_err(|err| SetPasswordError::MySqlError(err.to_string()));
.map(|_| ())
.map_err(|err| SetPasswordError::MySqlError(err.to_string()));
if result.is_err() { if result.is_err() {
tracing::error!( tracing::error!(
@@ -315,13 +316,15 @@ pub async fn lock_database_users(
} }
} }
let result = sqlx::query( let statement = AssertSqlSafe(format!(
format!("ALTER USER {}@'%' ACCOUNT LOCK", quote_literal(&db_user),).as_str(), "ALTER USER {}@'%' ACCOUNT LOCK",
) quote_literal(&db_user),
.execute(&mut *connection) ));
.await let result = sqlx::query(statement)
.map(|_| ()) .execute(&mut *connection)
.map_err(|err| LockUserError::MySqlError(err.to_string())); .await
.map(|_| ())
.map_err(|err| LockUserError::MySqlError(err.to_string()));
if let Err(err) = &result { if let Err(err) = &result {
tracing::error!("Failed to lock database user '{}': {:?}", &db_user, err); tracing::error!("Failed to lock database user '{}': {:?}", &db_user, err);
@@ -375,13 +378,15 @@ pub async fn unlock_database_users(
_ => {} _ => {}
} }
let result = sqlx::query( let statement = AssertSqlSafe(format!(
format!("ALTER USER {}@'%' ACCOUNT UNLOCK", quote_literal(&db_user),).as_str(), "ALTER USER {}@'%' ACCOUNT UNLOCK",
) quote_literal(&db_user),
.execute(&mut *connection) ));
.await let result = sqlx::query(statement)
.map(|_| ()) .execute(&mut *connection)
.map_err(|err| UnlockUserError::MySqlError(err.to_string())); .await
.map(|_| ())
.map_err(|err| UnlockUserError::MySqlError(err.to_string()));
if let Err(err) = &result { if let Err(err) = &result {
tracing::error!("Failed to unlock database user '{}': {:?}", &db_user, err); tracing::error!("Failed to unlock database user '{}': {:?}", &db_user, err);
@@ -459,16 +464,17 @@ pub async fn list_database_users(
continue; continue;
} }
let mut result = sqlx::query_as::<_, DatabaseUser>( let statement = AssertSqlSafe(
&(if db_is_mariadb { if db_is_mariadb {
DB_USER_SELECT_STATEMENT_MARIADB.to_string() DB_USER_SELECT_STATEMENT_MARIADB.to_string()
} else { } else {
DB_USER_SELECT_STATEMENT_MYSQL.to_string() DB_USER_SELECT_STATEMENT_MYSQL.to_string()
} + "WHERE `mysql`.`user`.`User` = ? AND `mysql`.`user`.`Host` = '%'"), } + "WHERE `mysql`.`user`.`User` = ? AND `mysql`.`user`.`Host` = '%'",
) );
.bind(db_user.as_str()) let mut result = sqlx::query_as::<_, DatabaseUser>(statement)
.fetch_optional(&mut *connection) .bind(db_user.as_str())
.await; .fetch_optional(&mut *connection)
.await;
if let Err(err) = &result { if let Err(err) = &result {
tracing::error!("Failed to list database user '{}': {:?}", &db_user, err); tracing::error!("Failed to list database user '{}': {:?}", &db_user, err);
@@ -496,17 +502,18 @@ pub async fn list_all_database_users_for_unix_user(
db_is_mariadb: bool, db_is_mariadb: bool,
group_denylist: &GroupDenylist, group_denylist: &GroupDenylist,
) -> ListAllUsersResponse { ) -> ListAllUsersResponse {
let mut result = sqlx::query_as::<_, DatabaseUser>( let statement = AssertSqlSafe(
&(if db_is_mariadb { if db_is_mariadb {
DB_USER_SELECT_STATEMENT_MARIADB.to_string() DB_USER_SELECT_STATEMENT_MARIADB.to_string()
} else { } else {
DB_USER_SELECT_STATEMENT_MYSQL.to_string() DB_USER_SELECT_STATEMENT_MYSQL.to_string()
} + "WHERE `user`.`User` REGEXP ? AND `user`.`Host` = '%'"), } + "WHERE `user`.`User` REGEXP ? AND `user`.`Host` = '%'",
) );
.bind(create_user_group_matching_regex(unix_user, group_denylist)) let mut result = sqlx::query_as::<_, DatabaseUser>(statement)
.fetch_all(&mut *connection) .bind(create_user_group_matching_regex(unix_user, group_denylist))
.await .fetch_all(&mut *connection)
.map_err(|err| ListAllUsersError::MySqlError(err.to_string())); .await
.map_err(|err| ListAllUsersError::MySqlError(err.to_string()));
if let Err(err) = &result { if let Err(err) = &result {
tracing::error!("Failed to list all database users: {:?}", err); tracing::error!("Failed to list all database users: {:?}", err);
@@ -531,23 +538,21 @@ pub async fn set_databases_where_user_has_privileges(
db_user: &mut DatabaseUser, db_user: &mut DatabaseUser,
connection: &mut MySqlConnection, connection: &mut MySqlConnection,
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
let database_list = sqlx::query( let statement = AssertSqlSafe(formatdoc!(
formatdoc!( r"
r" SELECT `Db` AS `database`
SELECT `Db` AS `database` FROM `db`
FROM `db` WHERE `User` = ? AND `Host` = '%' AND ({})
WHERE `User` = ? AND `Host` = '%' AND ({}) ",
", DATABASE_PRIVILEGE_FIELDS
DATABASE_PRIVILEGE_FIELDS .iter()
.iter() .map(|field| format!("`{field}` = 'Y'"))
.map(|field| format!("`{field}` = 'Y'")) .join(" OR "),
.join(" OR "), ));
) let database_list = sqlx::query(statement)
.as_str(), .bind(db_user.user.as_str())
) .fetch_all(&mut *connection)
.bind(db_user.user.as_str()) .await;
.fetch_all(&mut *connection)
.await;
if let Err(err) = &database_list { if let Err(err) = &database_list {
tracing::error!( tracing::error!(