7 Commits

Author SHA1 Message Date
oysteikt 43e4cc45ca server/sql: great performance improvements for listing databases
Build and test / check-license (push) Successful in 53s
Build and test / check (push) Successful in 1m54s
Build and test / build (push) Successful in 3m5s
Build and test / test (push) Successful in 3m9s
Build and test / docs (push) Successful in 5m36s
2026-05-31 02:01:44 +09:00
oysteikt 62b1b66bb6 CHANGELOG.md: fix broken link
Build and test / check-license (push) Successful in 55s
Build and test / check (push) Successful in 2m38s
Build and test / build (push) Successful in 2m42s
Build and test / test (push) Successful in 3m8s
Build and test / docs (push) Successful in 6m8s
2026-05-31 00:44:52 +09:00
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 445 additions and 583 deletions
+11 -1
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
@@ -66,7 +76,7 @@ This is the initial release of `muscl`.
interactive tool, there shouldn't have been any scripts relying on the old formatting. interactive tool, there shouldn't have been any scripts relying on the old formatting.
- The configuration file is shared for all variants of the program, and `muscl` will use - The configuration file is shared for all variants of the program, and `muscl` will use
its new logic to look for and parse this file. See the example config and its new logic to look for and parse this file. See the example config and
[installation instructions][installation-instructions] for more information about how to [installation instructions](./docs/installation.md) for more information about how to
configure the software. configure the software.
- The order in which input is validated might be differ from the original - The order in which input is validated might be differ from the original
(e.g. database ownership checks, invalid character checks, existence checks, ...). (e.g. database ownership checks, invalid character checks, existence checks, ...).
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"
+99 -38
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,8 +126,11 @@ pub async fn create_databases(
_ => {} _ => {}
} }
let result = let statement = AssertSqlSafe(format!(
sqlx::query(format!("CREATE DATABASE {}", quote_identifier(&database_name)).as_str()) "CREATE DATABASE {}",
quote_identifier(&database_name)
));
let result = sqlx::query(statement)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -181,8 +185,11 @@ pub async fn drop_databases(
_ => {} _ => {}
} }
let result = let statement = AssertSqlSafe(format!(
sqlx::query(format!("DROP DATABASE {}", quote_identifier(&database_name)).as_str()) "DROP DATABASE {}",
quote_identifier(&database_name)
));
let result = sqlx::query(statement)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -265,26 +272,49 @@ pub async fn list_databases(
let result = sqlx::query_as::<_, DatabaseRow>( let result = sqlx::query_as::<_, DatabaseRow>(
r" r"
SELECT SELECT
CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`, CAST(s.SCHEMA_NAME AS CHAR(64)) AS `database`,
GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`, t.tables,
GROUP_CONCAT(DISTINCT CAST(`mysql`.`db`.`User` AS CHAR(64)) SEPARATOR ',') AS `users`, u.users,
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_COLLATION_NAME`) AS `collation`, s.DEFAULT_COLLATION_NAME AS `collation`,
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_CHARACTER_SET_NAME`) AS `character_set`, s.DEFAULT_CHARACTER_SET_NAME AS `character_set`,
CAST(IFNULL( CAST(COALESCE(t.size_bytes, 0) AS UNSIGNED) AS `size_bytes`
SUM(`information_schema`.`TABLES`.`DATA_LENGTH` + `information_schema`.`TABLES`.`INDEX_LENGTH`), FROM information_schema.SCHEMATA s
0
) AS UNSIGNED INTEGER) AS `size_bytes`
FROM `information_schema`.`SCHEMATA`
LEFT OUTER JOIN `information_schema`.`TABLES`
ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `TABLES`.`TABLE_SCHEMA`
LEFT OUTER JOIN `mysql`.`db`
ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `mysql`.`db`.`DB`
WHERE `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = ?
GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME`
",
LEFT JOIN (
SELECT
TABLE_SCHEMA,
GROUP_CONCAT(
DISTINCT CAST(TABLE_NAME AS CHAR(64))
ORDER BY TABLE_NAME
SEPARATOR ','
) AS tables,
SUM(DATA_LENGTH + INDEX_LENGTH) AS size_bytes
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = ?
GROUP BY TABLE_SCHEMA
) t
ON t.TABLE_SCHEMA = s.SCHEMA_NAME
LEFT JOIN (
SELECT
DB,
GROUP_CONCAT(
DISTINCT CAST(User AS CHAR(64))
ORDER BY User
SEPARATOR ','
) AS users
FROM mysql.db
WHERE DB = ?
GROUP BY DB
) u
ON u.DB = s.SCHEMA_NAME
WHERE s.SCHEMA_NAME = ?;
",
) )
.bind(database_name.to_string()) .bind(database_name.to_string())
.bind(database_name.to_string())
.bind(database_name.to_string())
.fetch_optional(&mut *connection) .fetch_optional(&mut *connection)
.await .await
.map_err(|err| ListDatabasesError::MySqlError(err.to_string())) .map_err(|err| ListDatabasesError::MySqlError(err.to_string()))
@@ -313,26 +343,57 @@ pub async fn list_all_databases_for_user(
let result = sqlx::query_as::<_, DatabaseRow>( let result = sqlx::query_as::<_, DatabaseRow>(
r" r"
SELECT SELECT
CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`, CAST(s.SCHEMA_NAME AS CHAR(64)) AS `database`,
GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`, t.tables,
GROUP_CONCAT(DISTINCT CAST(`mysql`.`db`.`User` AS CHAR(64)) SEPARATOR ',') AS `users`, u.users,
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_COLLATION_NAME`) AS `collation`, s.DEFAULT_COLLATION_NAME AS collation,
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_CHARACTER_SET_NAME`) AS `character_set`, s.DEFAULT_CHARACTER_SET_NAME AS character_set,
CAST(IFNULL( CAST(COALESCE(t.size_bytes, 0) AS UNSIGNED) AS size_bytes
SUM(`information_schema`.`TABLES`.`DATA_LENGTH` + `information_schema`.`TABLES`.`INDEX_LENGTH`), FROM information_schema.SCHEMATA s
0
) AS UNSIGNED INTEGER) AS `size_bytes` LEFT JOIN (
FROM `information_schema`.`SCHEMATA` SELECT
LEFT OUTER JOIN `information_schema`.`TABLES` TABLE_SCHEMA,
ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `TABLES`.`TABLE_SCHEMA` GROUP_CONCAT(
LEFT OUTER JOIN `mysql`.`db` DISTINCT CAST(TABLE_NAME AS CHAR(64))
ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `mysql`.`db`.`DB` ORDER BY TABLE_NAME
WHERE `information_schema`.`SCHEMATA`.`SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys') SEPARATOR ','
AND `information_schema`.`SCHEMATA`.`SCHEMA_NAME` REGEXP ? ) AS tables,
GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME` SUM(DATA_LENGTH + INDEX_LENGTH) AS size_bytes
FROM information_schema.TABLES
WHERE TABLE_SCHEMA REGEXP ?
GROUP BY TABLE_SCHEMA
) t
ON t.TABLE_SCHEMA = s.SCHEMA_NAME
LEFT JOIN (
SELECT
DB,
GROUP_CONCAT(
DISTINCT CAST(User AS CHAR(64))
ORDER BY User
SEPARATOR ','
) AS users
FROM mysql.db
WHERE DB REGEXP ?
GROUP BY DB
) u
ON u.DB = s.SCHEMA_NAME
WHERE s.SCHEMA_NAME REGEXP ?
AND s.SCHEMA_NAME NOT IN (
'information_schema',
'performance_schema',
'mysql',
'sys'
)
ORDER BY s.SCHEMA_NAME
", ",
) )
.bind(create_user_group_matching_regex(unix_user, group_denylist)) .bind(create_user_group_matching_regex(unix_user, group_denylist))
.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
.map_err(|err| ListAllDatabasesError::MySqlError(err.to_string())); .map_err(|err| ListAllDatabasesError::MySqlError(err.to_string()));
+23 -14
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,13 +84,14 @@ 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(","),
)) ));
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(statement)
.bind(database_name) .bind(database_name)
.fetch_all(connection) .fetch_all(connection)
.await; .await;
@@ -113,13 +114,14 @@ 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(","),
)) ));
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(statement)
.bind(database_name.as_str()) .bind(database_name.as_str())
.bind(user_name.as_str()) .bind(user_name.as_str())
.fetch_optional(connection) .fetch_optional(connection)
@@ -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,10 +285,10 @@ 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))
@@ -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)))
+31 -26
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,14 +208,12 @@ 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(),
) ));
.as_str(), let result = sqlx::query(statement)
)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -315,9 +316,11 @@ 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),
));
let result = sqlx::query(statement)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -375,9 +378,11 @@ 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),
));
let result = sqlx::query(statement)
.execute(&mut *connection) .execute(&mut *connection)
.await .await
.map(|_| ()) .map(|_| ())
@@ -459,13 +464,14 @@ 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` = '%'",
) );
let mut result = sqlx::query_as::<_, DatabaseUser>(statement)
.bind(db_user.as_str()) .bind(db_user.as_str())
.fetch_optional(&mut *connection) .fetch_optional(&mut *connection)
.await; .await;
@@ -496,13 +502,14 @@ 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` = '%'",
) );
let mut result = sqlx::query_as::<_, DatabaseUser>(statement)
.bind(create_user_group_matching_regex(unix_user, group_denylist)) .bind(create_user_group_matching_regex(unix_user, group_denylist))
.fetch_all(&mut *connection) .fetch_all(&mut *connection)
.await .await
@@ -531,8 +538,7 @@ 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`
@@ -542,9 +548,8 @@ pub async fn set_databases_where_user_has_privileges(
.iter() .iter()
.map(|field| format!("`{field}` = 'Y'")) .map(|field| format!("`{field}` = 'Y'"))
.join(" OR "), .join(" OR "),
) ));
.as_str(), let database_list = sqlx::query(statement)
)
.bind(db_user.user.as_str()) .bind(db_user.user.as_str())
.fetch_all(&mut *connection) .fetch_all(&mut *connection)
.await; .await;