Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
772942eed0
|
|||
|
43e4cc45ca
|
|||
|
62b1b66bb6
|
|||
|
f16239aceb
|
|||
|
8f475eced1
|
|||
|
6849e99c11
|
|||
|
759df9ef42
|
|||
|
a64d1fa1bf
|
+11
-1
@@ -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
File diff suppressed because it is too large
Load Diff
+9
-9
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use crate::{
|
|||||||
parse_privilege_data_from_editor_content, reduce_privilege_diffs,
|
parse_privilege_data_from_editor_content, reduce_privilege_diffs,
|
||||||
},
|
},
|
||||||
protocol::{
|
protocol::{
|
||||||
ClientToServerMessageStream, ListDatabasesError, ListUsersError,
|
ClientToServerMessageStream, ListDatabasesError, ListDatabasesRequest, ListUsersError,
|
||||||
ModifyDatabasePrivilegesError, Request, Response,
|
ModifyDatabasePrivilegesError, Request, Response,
|
||||||
print_modify_database_privileges_output_status, request_validation::ValidationError,
|
print_modify_database_privileges_output_status, request_validation::ValidationError,
|
||||||
},
|
},
|
||||||
@@ -132,7 +132,7 @@ async fn databases_exist(
|
|||||||
.map(|diff| diff.get_database_name().clone())
|
.map(|diff| diff.get_database_name().clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let message = Request::ListDatabases(Some(database_list));
|
let message = Request::ListDatabases(ListDatabasesRequest::new(Some(database_list), false));
|
||||||
server_connection.send(message).await?;
|
server_connection.send(message).await?;
|
||||||
|
|
||||||
let result = match server_connection.next().await {
|
let result = match server_connection.next().await {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ use crate::{
|
|||||||
core::{
|
core::{
|
||||||
completion::mysql_database_completer,
|
completion::mysql_database_completer,
|
||||||
protocol::{
|
protocol::{
|
||||||
ClientToServerMessageStream, ListDatabasesError, Request, Response,
|
ClientToServerMessageStream, ListDatabasesError, ListDatabasesRequest, Request,
|
||||||
print_list_databases_output_status, print_list_databases_output_status_json,
|
Response, print_list_databases_output_status, print_list_databases_output_status_json,
|
||||||
request_validation::ValidationError,
|
request_validation::ValidationError,
|
||||||
},
|
},
|
||||||
types::MySQLDatabase,
|
types::MySQLDatabase,
|
||||||
@@ -27,6 +27,10 @@ pub struct ShowDbArgs {
|
|||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
|
||||||
|
/// Show all tables and users for each database
|
||||||
|
#[arg(short = 'a', long)]
|
||||||
|
all: bool,
|
||||||
|
|
||||||
/// Show sizes in bytes instead of human-readable format
|
/// Show sizes in bytes instead of human-readable format
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
bytes: bool,
|
bytes: bool,
|
||||||
@@ -36,11 +40,14 @@ pub async fn show_databases(
|
|||||||
args: ShowDbArgs,
|
args: ShowDbArgs,
|
||||||
mut server_connection: ClientToServerMessageStream,
|
mut server_connection: ClientToServerMessageStream,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let message = if args.name.is_empty() {
|
let message = Request::ListDatabases(ListDatabasesRequest::new(
|
||||||
Request::ListDatabases(None)
|
if args.name.is_empty() {
|
||||||
} else {
|
None
|
||||||
Request::ListDatabases(Some(args.name.clone()))
|
} else {
|
||||||
};
|
Some(args.name.clone())
|
||||||
|
},
|
||||||
|
args.all || args.json,
|
||||||
|
));
|
||||||
|
|
||||||
server_connection.send(message).await?;
|
server_connection.send(message).await?;
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ use crate::{
|
|||||||
completion::{mysql_database_completer, prefix_completer},
|
completion::{mysql_database_completer, prefix_completer},
|
||||||
database_privileges::DatabasePrivilegeRow,
|
database_privileges::DatabasePrivilegeRow,
|
||||||
protocol::{
|
protocol::{
|
||||||
ClientToServerMessageStream, ListPrivilegesError, Request, Response,
|
ClientToServerMessageStream, ListDatabasesRequest, ListPrivilegesError, Request,
|
||||||
create_client_to_server_message_stream,
|
Response, create_client_to_server_message_stream,
|
||||||
},
|
},
|
||||||
types::MySQLDatabase,
|
types::MySQLDatabase,
|
||||||
},
|
},
|
||||||
@@ -285,7 +285,7 @@ async fn show_databases(
|
|||||||
args.name.iter().map(trim_db_name_to_32_chars).collect();
|
args.name.iter().map(trim_db_name_to_32_chars).collect();
|
||||||
|
|
||||||
let message = if database_names.is_empty() {
|
let message = if database_names.is_empty() {
|
||||||
let message = Request::ListDatabases(None);
|
let message = Request::ListDatabases(ListDatabasesRequest::new(None, false));
|
||||||
server_connection.send(message).await?;
|
server_connection.send(message).await?;
|
||||||
let response = server_connection.next().await;
|
let response = server_connection.next().await;
|
||||||
let databases = match response {
|
let databases = match response {
|
||||||
|
|||||||
@@ -142,7 +142,8 @@ impl Request {
|
|||||||
Request::ListDatabases(req) => format!(
|
Request::ListDatabases(req) => format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
self.command_name(),
|
self.command_name(),
|
||||||
req.as_ref()
|
req.names
|
||||||
|
.as_ref()
|
||||||
.map_or("".to_string(), |r| format!("({})", r.len()))
|
.map_or("".to_string(), |r| format!("({})", r.len()))
|
||||||
),
|
),
|
||||||
Request::ListPrivileges(req) => format!(
|
Request::ListPrivileges(req) => format!(
|
||||||
@@ -206,9 +207,12 @@ impl Request {
|
|||||||
Request::CompleteUserName(_) => Default::default(),
|
Request::CompleteUserName(_) => Default::default(),
|
||||||
Request::CreateDatabases(databases) => databases.iter().cloned().collect(),
|
Request::CreateDatabases(databases) => databases.iter().cloned().collect(),
|
||||||
Request::DropDatabases(databases) => databases.iter().cloned().collect(),
|
Request::DropDatabases(databases) => databases.iter().cloned().collect(),
|
||||||
Request::ListDatabases(databases) => {
|
Request::ListDatabases(request) => request
|
||||||
databases.clone().unwrap_or_default().into_iter().collect()
|
.names
|
||||||
}
|
.clone()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
Request::ListPrivileges(databases) => {
|
Request::ListPrivileges(databases) => {
|
||||||
databases.clone().unwrap_or_default().into_iter().collect()
|
databases.clone().unwrap_or_default().into_iter().collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,21 @@ use crate::{
|
|||||||
server::sql::database_operations::DatabaseRow,
|
server::sql::database_operations::DatabaseRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub type ListDatabasesRequest = Option<Vec<MySQLDatabase>>;
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct ListDatabasesRequest {
|
||||||
|
pub names: Option<Vec<MySQLDatabase>>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub include_all_tables_and_users: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListDatabasesRequest {
|
||||||
|
pub fn new(names: Option<Vec<MySQLDatabase>>, include_all_tables_and_users: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
names,
|
||||||
|
include_all_tables_and_users,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub type ListDatabasesResponse = BTreeMap<MySQLDatabase, Result<DatabaseRow, ListDatabasesError>>;
|
pub type ListDatabasesResponse = BTreeMap<MySQLDatabase, Result<DatabaseRow, ListDatabasesError>>;
|
||||||
|
|
||||||
@@ -144,7 +158,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_deserialize_request() {
|
fn test_serialize_deserialize_request() {
|
||||||
let request = Some(vec!["db1".into(), "db2".into()]);
|
let request = ListDatabasesRequest::new(Some(vec!["db1".into(), "db2".into()]), true);
|
||||||
let json = serde_json::to_string_pretty(&request).unwrap();
|
let json = serde_json::to_string_pretty(&request).unwrap();
|
||||||
println!("Serialized request:\n{}", json);
|
println!("Serialized request:\n{}", json);
|
||||||
|
|
||||||
@@ -152,6 +166,20 @@ mod tests {
|
|||||||
assert_eq!(request, deserialized);
|
assert_eq!(request, deserialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialize_request_without_include_all_tables_and_users_defaults_to_false() {
|
||||||
|
let json = serde_json::json!({
|
||||||
|
"names": ["db1", "db2"]
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let deserialized: ListDatabasesRequest = serde_json::from_str(&json).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
deserialized,
|
||||||
|
ListDatabasesRequest::new(Some(vec!["db1".into(), "db2".into()]), false)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serialize_deserialize_response() {
|
fn test_serialize_deserialize_response() {
|
||||||
let response: ListDatabasesResponse = vec![
|
let response: ListDatabasesResponse = vec![
|
||||||
|
|||||||
@@ -332,14 +332,15 @@ async fn handle_request(
|
|||||||
.await;
|
.await;
|
||||||
Response::DropDatabases(result)
|
Response::DropDatabases(result)
|
||||||
}
|
}
|
||||||
Request::ListDatabases(ref database_names) => {
|
Request::ListDatabases(ref request) => {
|
||||||
if let Some(database_names) = database_names {
|
if let Some(database_names) = &request.names {
|
||||||
let result = list_databases(
|
let result = list_databases(
|
||||||
database_names,
|
database_names,
|
||||||
unix_user,
|
unix_user,
|
||||||
db_connection,
|
db_connection,
|
||||||
db_is_mariadb,
|
db_is_mariadb,
|
||||||
group_denylist,
|
group_denylist,
|
||||||
|
request.include_all_tables_and_users,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Response::ListDatabases(result)
|
Response::ListDatabases(result)
|
||||||
@@ -349,6 +350,7 @@ async fn handle_request(
|
|||||||
db_connection,
|
db_connection,
|
||||||
db_is_mariadb,
|
db_is_mariadb,
|
||||||
group_denylist,
|
group_denylist,
|
||||||
|
request.include_all_tables_and_users,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
Response::ListAllDatabases(result)
|
Response::ListAllDatabases(result)
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|
||||||
@@ -23,6 +24,8 @@ use crate::{
|
|||||||
server::{common::create_user_group_matching_regex, sql::quote_identifier},
|
server::{common::create_user_group_matching_regex, sql::quote_identifier},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const MAX_SHOW_DB_RELATED_ITEMS: usize = 5;
|
||||||
|
|
||||||
// NOTE: this function is unsafe because it does no input validation.
|
// NOTE: this function is unsafe because it does no input validation.
|
||||||
pub(super) async fn unsafe_database_exists(
|
pub(super) async fn unsafe_database_exists(
|
||||||
database_name: &str,
|
database_name: &str,
|
||||||
@@ -125,12 +128,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 +187,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);
|
||||||
@@ -241,12 +250,84 @@ impl FromRow<'_, sqlx::mysql::MySqlRow> for DatabaseRow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_database_query(include_all_tables_and_users: bool) -> AssertSqlSafe<String> {
|
||||||
|
let limit_clause = if include_all_tables_and_users {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
format!(" LIMIT {}", MAX_SHOW_DB_RELATED_ITEMS)
|
||||||
|
};
|
||||||
|
|
||||||
|
AssertSqlSafe(format!(
|
||||||
|
r"
|
||||||
|
SELECT
|
||||||
|
CAST(s.SCHEMA_NAME AS CHAR(64)) AS `database`,
|
||||||
|
t.tables,
|
||||||
|
u.users,
|
||||||
|
s.DEFAULT_COLLATION_NAME AS `collation`,
|
||||||
|
s.DEFAULT_CHARACTER_SET_NAME AS `character_set`,
|
||||||
|
CAST(COALESCE(sz.size_bytes, 0) AS UNSIGNED) AS size_bytes
|
||||||
|
FROM information_schema.SCHEMATA s
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
x.TABLE_SCHEMA,
|
||||||
|
GROUP_CONCAT(x.TABLE_NAME ORDER BY x.TABLE_NAME SEPARATOR ',') AS tables
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
TABLE_SCHEMA,
|
||||||
|
TABLE_NAME
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = ?
|
||||||
|
ORDER BY TABLE_NAME{limit_clause}
|
||||||
|
) x
|
||||||
|
GROUP BY x.TABLE_SCHEMA
|
||||||
|
) t
|
||||||
|
ON t.TABLE_SCHEMA = s.SCHEMA_NAME
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
x.DB,
|
||||||
|
GROUP_CONCAT(DISTINCT x.User ORDER BY x.User SEPARATOR ',') AS users
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
DB,
|
||||||
|
User
|
||||||
|
FROM mysql.db
|
||||||
|
WHERE DB = ?
|
||||||
|
ORDER BY User{limit_clause}
|
||||||
|
) x
|
||||||
|
GROUP BY x.DB
|
||||||
|
) u
|
||||||
|
ON u.DB = s.SCHEMA_NAME
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
TABLE_SCHEMA,
|
||||||
|
SUM(DATA_LENGTH + INDEX_LENGTH) AS size_bytes
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = ?
|
||||||
|
GROUP BY TABLE_SCHEMA
|
||||||
|
) sz
|
||||||
|
ON sz.TABLE_SCHEMA = s.SCHEMA_NAME
|
||||||
|
|
||||||
|
WHERE s.SCHEMA_NAME REGEXP ?
|
||||||
|
AND s.SCHEMA_NAME NOT IN (
|
||||||
|
'information_schema',
|
||||||
|
'performance_schema',
|
||||||
|
'mysql',
|
||||||
|
'sys'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_databases(
|
pub async fn list_databases(
|
||||||
database_names: &[MySQLDatabase],
|
database_names: &[MySQLDatabase],
|
||||||
unix_user: &UnixUser,
|
unix_user: &UnixUser,
|
||||||
connection: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
_db_is_mariadb: bool,
|
_db_is_mariadb: bool,
|
||||||
group_denylist: &GroupDenylist,
|
group_denylist: &GroupDenylist,
|
||||||
|
include_all_tables_and_users: bool,
|
||||||
) -> ListDatabasesResponse {
|
) -> ListDatabasesResponse {
|
||||||
let mut results = BTreeMap::new();
|
let mut results = BTreeMap::new();
|
||||||
|
|
||||||
@@ -262,35 +343,19 @@ pub async fn list_databases(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = sqlx::query_as::<_, DatabaseRow>(
|
let query = list_database_query(include_all_tables_and_users);
|
||||||
r"
|
|
||||||
SELECT
|
|
||||||
CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`,
|
|
||||||
GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`,
|
|
||||||
GROUP_CONCAT(DISTINCT CAST(`mysql`.`db`.`User` AS CHAR(64)) SEPARATOR ',') AS `users`,
|
|
||||||
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_COLLATION_NAME`) AS `collation`,
|
|
||||||
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_CHARACTER_SET_NAME`) AS `character_set`,
|
|
||||||
CAST(IFNULL(
|
|
||||||
SUM(`information_schema`.`TABLES`.`DATA_LENGTH` + `information_schema`.`TABLES`.`INDEX_LENGTH`),
|
|
||||||
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`
|
|
||||||
",
|
|
||||||
|
|
||||||
)
|
let result = sqlx::query_as::<_, DatabaseRow>(query)
|
||||||
.bind(database_name.to_string())
|
.bind(database_name.to_string())
|
||||||
.fetch_optional(&mut *connection)
|
.bind(database_name.to_string())
|
||||||
.await
|
.bind(database_name.to_string())
|
||||||
.map_err(|err| ListDatabasesError::MySqlError(err.to_string()))
|
.bind(database_name.to_string())
|
||||||
.and_then(|database| {
|
.fetch_optional(&mut *connection)
|
||||||
database.map_or_else(|| Err(ListDatabasesError::DatabaseDoesNotExist), Ok)
|
.await
|
||||||
});
|
.map_err(|err| ListDatabasesError::MySqlError(err.to_string()))
|
||||||
|
.and_then(|database| {
|
||||||
|
database.map_or_else(|| Err(ListDatabasesError::DatabaseDoesNotExist), Ok)
|
||||||
|
});
|
||||||
|
|
||||||
if let Err(err) = &result {
|
if let Err(err) = &result {
|
||||||
tracing::error!("Failed to list database '{}': {:?}", &database_name, err);
|
tracing::error!("Failed to list database '{}': {:?}", &database_name, err);
|
||||||
@@ -304,38 +369,97 @@ pub async fn list_databases(
|
|||||||
results
|
results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_all_databases_for_user_query(include_all_tables_and_users: bool) -> AssertSqlSafe<String> {
|
||||||
|
let limit_clause = if include_all_tables_and_users {
|
||||||
|
"".to_string()
|
||||||
|
} else {
|
||||||
|
format!(" LIMIT {}", MAX_SHOW_DB_RELATED_ITEMS)
|
||||||
|
};
|
||||||
|
|
||||||
|
AssertSqlSafe(format!(
|
||||||
|
r"
|
||||||
|
SELECT
|
||||||
|
CAST(s.SCHEMA_NAME AS CHAR(64)) AS `database`,
|
||||||
|
t.tables,
|
||||||
|
u.users,
|
||||||
|
s.DEFAULT_COLLATION_NAME AS `collation`,
|
||||||
|
s.DEFAULT_CHARACTER_SET_NAME AS `character_set`,
|
||||||
|
CAST(COALESCE(sz.size_bytes, 0) AS UNSIGNED) AS size_bytes
|
||||||
|
FROM information_schema.SCHEMATA s
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
x.TABLE_SCHEMA,
|
||||||
|
GROUP_CONCAT(x.TABLE_NAME ORDER BY x.TABLE_NAME SEPARATOR ',') AS tables
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
TABLE_SCHEMA,
|
||||||
|
TABLE_NAME
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA REGEXP ?
|
||||||
|
ORDER BY TABLE_NAME{limit_clause}
|
||||||
|
) x
|
||||||
|
GROUP BY x.TABLE_SCHEMA
|
||||||
|
) t
|
||||||
|
ON t.TABLE_SCHEMA = s.SCHEMA_NAME
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
x.DB,
|
||||||
|
GROUP_CONCAT(DISTINCT x.User ORDER BY x.User SEPARATOR ',') AS users
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
DB,
|
||||||
|
User
|
||||||
|
FROM mysql.db
|
||||||
|
WHERE DB REGEXP ?
|
||||||
|
ORDER BY User{limit_clause}
|
||||||
|
) x
|
||||||
|
GROUP BY x.DB
|
||||||
|
) u
|
||||||
|
ON u.DB = s.SCHEMA_NAME
|
||||||
|
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT
|
||||||
|
TABLE_SCHEMA,
|
||||||
|
SUM(DATA_LENGTH + INDEX_LENGTH) AS size_bytes
|
||||||
|
FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA REGEXP ?
|
||||||
|
GROUP BY TABLE_SCHEMA
|
||||||
|
) sz
|
||||||
|
ON sz.TABLE_SCHEMA = 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
|
||||||
|
"
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn list_all_databases_for_user(
|
pub async fn list_all_databases_for_user(
|
||||||
unix_user: &UnixUser,
|
unix_user: &UnixUser,
|
||||||
connection: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
_db_is_mariadb: bool,
|
_db_is_mariadb: bool,
|
||||||
group_denylist: &GroupDenylist,
|
group_denylist: &GroupDenylist,
|
||||||
|
include_all_tables_and_users: bool,
|
||||||
) -> ListAllDatabasesResponse {
|
) -> ListAllDatabasesResponse {
|
||||||
let result = sqlx::query_as::<_, DatabaseRow>(
|
let query = list_all_databases_for_user_query(include_all_tables_and_users);
|
||||||
r"
|
let user_group_regex = create_user_group_matching_regex(unix_user, group_denylist);
|
||||||
SELECT
|
|
||||||
CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`,
|
let result = sqlx::query_as::<_, DatabaseRow>(query)
|
||||||
GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`,
|
.bind(&user_group_regex)
|
||||||
GROUP_CONCAT(DISTINCT CAST(`mysql`.`db`.`User` AS CHAR(64)) SEPARATOR ',') AS `users`,
|
.bind(&user_group_regex)
|
||||||
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_COLLATION_NAME`) AS `collation`,
|
.bind(&user_group_regex)
|
||||||
MAX(`information_schema`.`SCHEMATA`.`DEFAULT_CHARACTER_SET_NAME`) AS `character_set`,
|
.bind(&user_group_regex)
|
||||||
CAST(IFNULL(
|
.fetch_all(connection)
|
||||||
SUM(`information_schema`.`TABLES`.`DATA_LENGTH` + `information_schema`.`TABLES`.`INDEX_LENGTH`),
|
.await
|
||||||
0
|
.map_err(|err| ListAllDatabasesError::MySqlError(err.to_string()));
|
||||||
) 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` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
|
||||||
AND `information_schema`.`SCHEMATA`.`SCHEMA_NAME` REGEXP ?
|
|
||||||
GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME`
|
|
||||||
",
|
|
||||||
)
|
|
||||||
.bind(create_user_group_matching_regex(unix_user, group_denylist))
|
|
||||||
.fetch_all(connection)
|
|
||||||
.await
|
|
||||||
.map_err(|err| ListAllDatabasesError::MySqlError(err.to_string()));
|
|
||||||
|
|
||||||
// TODO: should we assert that the users are also owned by the unix_user from the request?
|
// TODO: should we assert that the users are also owned by the unix_user from the request?
|
||||||
|
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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!(
|
||||||
|
|||||||
Reference in New Issue
Block a user