Misc 6 #77

Merged
oysteikt merged 9 commits from misc into main 2024-08-19 18:14:14 +02:00
18 changed files with 407 additions and 113 deletions

1
Cargo.lock generated
View File

@ -1077,6 +1077,7 @@ dependencies = [
"prettytable",
"rand",
"ratatui",
"regex",
"sd-notify",
"serde",
"serde_json",

View File

@ -49,3 +49,6 @@ codegen-units = 1
[build-dependencies]
anyhow = "1.0.82"
[dev-dependencies]
regex = "1.10.6"

View File

@ -1,8 +1,22 @@
# This should go to `/etc/mysqladm/config.toml`
[server]
# Note that this gets ignored if you are using socket activation.
socket_path = "/var/run/mysqladm/mysqladm.sock"
[mysql]
# if you use a socket, the host and port will be ignored
# socket_path = "/var/run/mysql/mysql.sock"
host = "localhost"
port = 3306
# The username and password can be omitted if you are using
# socket based authentication. However, the vendored systemd
# service is running as DynamicUser, so by default you need
# to at least specify the username.
username = "root"
password = "secret"
timeout = 2 # seconds

14
flake.lock generated
View File

@ -2,16 +2,16 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1723637854,
"narHash": "sha256-med8+5DSWa2UnOqtdICndjDAEjxr5D7zaIiK4pn0Q7c=",
"lastModified": 1723938990,
"narHash": "sha256-9tUadhnZQbWIiYVXH8ncfGXGvkNq3Hag4RCBEMUk7MI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c3aa7b8938b17aebd2deecf7be0636000d62a2b9",
"rev": "c42fcfbdfeae23e68fc520f9182dde9f38ad1890",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1723947704,
"narHash": "sha256-TcVf66N2NgGhxORFytzgqWcg0XJ+kk8uNLNsTRI5sYM=",
"lastModified": 1724034091,
"narHash": "sha256-b1g7w0sw+MDAhUAeCoX1vlTghsqcDZkxr+k9OZmxPa8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "456e78a55feade2c3bc6d7bc0bf5e710c9d86120",
"rev": "c7d36e0947826e0751a5214ffe82533fbc909bc0",
"type": "github"
},
"original": {

View File

@ -1,6 +1,6 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05";
rust-overlay.url = "github:oxalica/rust-overlay";
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
@ -52,11 +52,16 @@
overlays = {
default = self.overlays.mysqladm-rs;
greg-ng = final: prev: {
mysqladm-rs = final: prev: {
inherit (self.packages.${prev.system}) mysqladm-rs;
};
};
nixosModules = {
default = self.nixosModules.mysqladm-rs;
mysqladm-rs = import ./nix/module.nix;
};
packages = let
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
cargoLock = ./Cargo.lock;

141
nix/module.nix Normal file
View File

@ -0,0 +1,141 @@
{ config, pkgs, lib, ... }:
let
cfg = config.services.mysqladm-rs;
format = pkgs.formats.toml { };
in
{
options.services.mysqladm-rs = {
enable = lib.mkEnableOption "Enable mysqladm-rs";
package = lib.mkPackageOption pkgs "mysqladm-rs" { };
createLocalDatabaseUser = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Create a local database user for mysqladm-rs";
};
settings = lib.mkOption {
default = { };
type = lib.types.submodule {
freeformType = format.type;
options = {
server = {
socket_path = lib.mkOption {
type = lib.types.path;
default = "/var/run/mysqladm/mysqladm.sock";
description = "Path to the MySQL socket";
};
};
mysql = {
socket_path = lib.mkOption {
type = with lib.types; nullOr path;
default = "/var/run/mysqld/mysqld.sock";
description = "Path to the MySQL socket";
};
host = lib.mkOption {
type = with lib.types; nullOr str;
default = null;
description = "MySQL host";
};
port = lib.mkOption {
type = with lib.types; nullOr port;
default = 3306;
description = "MySQL port";
};
username = lib.mkOption {
type = lib.types.str;
default = "mysqladm";
description = "MySQL username";
};
passwordFile = lib.mkOption {
type = with lib.types; nullOr path;
default = null;
description = "Path to a file containing the MySQL password";
};
timeout = lib.mkOption {
type = lib.types.ints.positive;
default = 2;
description = "Number of seconds to wait for a response from the MySQL server";
};
};
};
};
};
};
config = let
nullStrippedConfig = lib.filterAttrsRecursive (_: v: v != null) cfg.settings;
configFile = format.generate "mysqladm-rs.conf" nullStrippedConfig;
in lib.mkIf config.services.mysqladm-rs.enable {
environment.systemPackages = [ cfg.package ];
services.mysql.ensureUsers = lib.mkIf cfg.createLocalDatabaseUser [
{
name = cfg.settings.mysql.username;
ensurePermissions = {
"mysql.*" = "SELECT, INSERT, UPDATE, DELETE";
"*.*" = "CREATE USER, GRANT OPTION";
};
}
];
systemd.services."mysqladm@" = {
description = "MySQL administration tool for non-admin users";
environment.RUST_LOG = "debug";
serviceConfig = {
Type = "notify";
ExecStart = "${lib.getExe cfg.package} server socket-activate --config ${configFile}";
User = "mysqladm";
Group = "mysqladm";
DynamicUser = true;
# This is required to read unix user/group details.
PrivateUsers = false;
CapabilityBoundingSet = "";
LockPersonality = true;
MemoryDenyWriteExecute = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = "yes";
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProtectSystem = "strict";
RemoveIPC = true;
UMask = "0000";
RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
};
};
systemd.sockets."mysqladm" = {
description = "MySQL administration tool for non-admin users";
wantedBy = [ "sockets.target" ];
restartTriggers = [ configFile ];
socketConfig = {
ListenStream = cfg.settings.server.socket_path;
Accept = "yes";
PassCredentials = true;
};
};
};
}

View File

@ -56,11 +56,11 @@ The Y/N-values corresponds to the following mysql privileges:
/// Please consider using the newer mysqladm command instead.
#[derive(Parser)]
#[command(
bin_name = "mysql-dbadm",
version,
about,
disable_help_subcommand = true,
verbatim_doc_comment,
bin_name = "mysql-dbadm",
version,
about,
disable_help_subcommand = true,
verbatim_doc_comment
)]
pub struct Args {
#[command(subcommand)]

View File

@ -32,11 +32,11 @@ use crate::{
/// Please consider using the newer mysqladm command instead.
#[derive(Parser)]
#[command(
bin_name = "mysql-useradm",
version,
about,
disable_help_subcommand = true,
verbatim_doc_comment,
bin_name = "mysql-useradm",
version,
about,
disable_help_subcommand = true,
verbatim_doc_comment
)]
pub struct Args {
#[command(subcommand)]

View File

@ -140,6 +140,7 @@ async fn create_users(
"Do you want to set a password for user '{}'?",
username
))
.default(false)
.interact()?
{
let password = read_password_from_stdin_with_double_check(username)?;

View File

@ -7,7 +7,7 @@ use tokio::net::UnixStream as TokioUnixStream;
use crate::{
core::common::{UnixUser, DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH},
server::{config::read_config_form_path, server_loop::handle_requests_for_single_session},
server::{config::read_config_from_path, server_loop::handle_requests_for_single_session},
};
// TODO: this function is security critical, it should be integration tested
@ -32,15 +32,18 @@ pub fn drop_privs() -> anyhow::Result<()> {
/// This function is used to bootstrap the connection to the server.
/// This can happen in two ways:
///
/// 1. If a socket path is provided, or exists in the default location,
/// the function will connect to the socket and authenticate with the
/// server to ensure that the server knows the uid of the client.
///
/// 2. If a config path is provided, or exists in the default location,
/// and the config is readable, the function will assume it is either
/// setuid or setgid, and will fork a child process to run the server
/// with the provided config. The server will exit silently by itself
/// when it is done, and this function will only return for the client
/// with the socket for the server.
///
/// If neither of these options are available, the function will fail.
pub fn bootstrap_server_connection_and_drop_privileges(
server_socket_path: Option<PathBuf>,
@ -140,7 +143,7 @@ fn run_forked_server(
server_socket: StdUnixStream,
unix_user: UnixUser,
) -> anyhow::Result<()> {
let config = read_config_form_path(Some(config_path))?;
let config = read_config_from_path(Some(config_path))?;
let result: anyhow::Result<()> = tokio::runtime::Builder::new_current_thread()
.enable_all()

View File

@ -14,8 +14,8 @@ use crate::server::sql::database_privilege_operations::{
pub fn db_priv_field_human_readable_name(name: &str) -> String {
match name {
"db" => "Database".to_owned(),
"user" => "User".to_owned(),
"Db" => "Database".to_owned(),
"User" => "User".to_owned(),
"select_priv" => "Select".to_owned(),
"insert_priv" => "Insert".to_owned(),
"update_priv" => "Update".to_owned(),
@ -128,8 +128,8 @@ pub fn format_privileges_line_for_editor(
DATABASE_PRIVILEGE_FIELDS
.into_iter()
.map(|field| match field {
"db" => format!("{:width$}", privs.db, width = database_name_len),
"user" => format!("{:width$}", privs.user, width = username_len),
"Db" => format!("{:width$}", privs.db, width = database_name_len),
"User" => format!("{:width$}", privs.user, width = username_len),
privilege => format!(
"{:width$}",
yn(privs.get_privilege_by_name(privilege)),

View File

@ -73,7 +73,6 @@ pub enum Response {
UnlockUsers(UnlockUsersOutput),
// Generic responses
OperationAborted,
Ready,
Error(String),
Exit,
}

View File

@ -9,10 +9,12 @@ use std::path::PathBuf;
use std::os::unix::net::UnixStream as StdUnixStream;
use tokio::net::UnixStream as TokioUnixStream;
use futures::StreamExt;
use crate::{
core::{
bootstrap::{bootstrap_server_connection_and_drop_privileges, drop_privs},
protocol::create_client_to_server_message_stream,
bootstrap::bootstrap_server_connection_and_drop_privileges,
protocol::{create_client_to_server_message_stream, Response},
},
server::command::ServerArgs,
};
@ -107,17 +109,17 @@ fn main() -> anyhow::Result<()> {
env_logger::init();
#[cfg(feature = "mysql-admutils-compatibility")]
if let Some(_) = handle_mysql_admutils_command()? {
if handle_mysql_admutils_command()?.is_some() {
return Ok(());
}
let args: Args = Args::parse();
if let Some(_) = handle_server_command(&args)? {
if handle_server_command(&args)?.is_some() {
return Ok(());
}
if let Some(_) = handle_generate_completions_command(&args)? {
if handle_generate_completions_command(&args)?.is_some() {
return Ok(());
}
@ -137,8 +139,8 @@ fn handle_mysql_admutils_command() -> anyhow::Result<Option<()>> {
});
match argv0.as_deref() {
Some("mysql-dbadm") => mysql_dbadm::main().map(|result| Some(result)),
Some("mysql-useradm") => mysql_useradm::main().map(|result| Some(result)),
Some("mysql-dbadm") => mysql_dbadm::main().map(Some),
Some("mysql-useradm") => mysql_useradm::main().map(Some),
_ => Ok(None),
}
}
@ -146,7 +148,6 @@ fn handle_mysql_admutils_command() -> anyhow::Result<Option<()>> {
fn handle_server_command(args: &Args) -> anyhow::Result<Option<()>> {
match args.command {
Command::Server(ref command) => {
drop_privs()?;
tokio_start_server(
args.server_socket_path.clone(),
args.config.clone(),
@ -205,7 +206,20 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
.unwrap()
.block_on(async {
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
let message_stream = create_client_to_server_message_stream(tokio_socket);
let mut message_stream = create_client_to_server_message_stream(tokio_socket);
while let Some(Ok(message)) = message_stream.next().await {
match message {
Response::Error(err) => {
anyhow::bail!("{}", err);
}
Response::Ready => break,
message => {
eprintln!("Unexpected message from server: {:?}", message);
}
}
}
match command {
Command::User(user_args) => {
cli::user_command::handle_command(user_args, message_stream).await

View File

@ -1,11 +1,51 @@
use crate::core::common::UnixUser;
use sqlx::prelude::*;
/// This function creates a regex that matches items (users, databases)
/// that belong to the user or any of the user's groups.
pub fn create_user_group_matching_regex(user: &UnixUser) -> String {
if user.groups.is_empty() {
format!("{}(_.+)?", user.username)
format!("{}_.+", user.username)
} else {
format!("({}|{})(_.+)?", user.username, user.groups.join("|"))
format!("({}|{})_.+", user.username, user.groups.join("|"))
}
}
/// Some mysql versions with some collations mark some columns as binary fields,
/// which in the current version of sqlx is not parsable as string.
/// See: https://github.com/launchbadge/sqlx/issues/3387
#[inline]
pub fn try_get_with_binary_fallback(
row: &sqlx::mysql::MySqlRow,
column: &str,
) -> Result<String, sqlx::Error> {
row.try_get(column).or_else(|_| {
row.try_get::<Vec<u8>, _>(column)
.map(|v| String::from_utf8_lossy(&v).to_string())
})
}
#[cfg(test)]
mod tests {
use super::*;
use regex::Regex;
#[test]
fn test_create_user_group_matching_regex() {
let user = UnixUser {
username: "user".to_owned(),
groups: vec!["group1".to_owned(), "group2".to_owned()],
};
let regex = create_user_group_matching_regex(&user);
let re = Regex::new(&regex).unwrap();
assert!(re.is_match("user_something"));
assert!(re.is_match("group1_something"));
assert!(re.is_match("group2_something"));
assert!(!re.is_match("other_something"));
assert!(!re.is_match("user"));
assert!(!re.is_match("usersomething"));
}
}

View File

@ -21,21 +21,37 @@ pub struct ServerConfig {
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename = "mysql")]
pub struct MysqlConfig {
pub host: String,
pub socket_path: Option<PathBuf>,
pub host: Option<String>,
pub port: Option<u16>,
pub username: String,
pub password: String,
pub username: Option<String>,
pub password: Option<String>,
pub password_file: Option<PathBuf>,
pub timeout: Option<u64>,
}
#[derive(Parser, Debug, Clone)]
pub struct ServerConfigArgs {
/// Path to the socket of the MySQL server.
#[arg(long, value_name = "PATH", global = true)]
socket_path: Option<PathBuf>,
/// Hostname of the MySQL server.
#[arg(long, value_name = "HOST", global = true)]
#[arg(
long,
value_name = "HOST",
global = true,
conflicts_with = "socket_path"
)]
mysql_host: Option<String>,
/// Port of the MySQL server.
#[arg(long, value_name = "PORT", global = true)]
#[arg(
long,
value_name = "PORT",
global = true,
conflicts_with = "socket_path"
)]
mysql_port: Option<u16>,
/// Username to use for the MySQL connection.
@ -44,7 +60,7 @@ pub struct ServerConfigArgs {
/// Path to a file containing the MySQL password.
#[arg(long, value_name = "PATH", global = true)]
mysql_password_file: Option<String>,
mysql_password_file: Option<PathBuf>,
/// Seconds to wait for the MySQL connection to be established.
#[arg(long, value_name = "SECONDS", global = true)]
@ -57,30 +73,40 @@ pub fn read_config_from_path_with_arg_overrides(
config_path: Option<PathBuf>,
args: ServerConfigArgs,
) -> anyhow::Result<ServerConfig> {
let config = read_config_form_path(config_path)?;
let config = read_config_from_path(config_path)?;
let mysql = &config.mysql;
let mysql = config.mysql;
let password = if let Some(path) = args.mysql_password_file {
fs::read_to_string(path)
.context("Failed to read MySQL password file")
.map(|s| s.trim().to_owned())?
let password = if let Some(path) = &args.mysql_password_file {
Some(
fs::read_to_string(path)
.context("Failed to read MySQL password file")
.map(|s| s.trim().to_owned())?,
)
} else if let Some(path) = &mysql.password_file {
Some(
fs::read_to_string(path)
.context("Failed to read MySQL password file")
.map(|s| s.trim().to_owned())?,
)
} else {
mysql.password.to_owned()
};
Ok(ServerConfig {
mysql: MysqlConfig {
host: args.mysql_host.unwrap_or(mysql.host.to_owned()),
socket_path: args.socket_path.or(mysql.socket_path),
host: args.mysql_host.or(mysql.host),
port: args.mysql_port.or(mysql.port),
username: args.mysql_user.unwrap_or(mysql.username.to_owned()),
username: args.mysql_user.or(mysql.username.to_owned()),
password,
password_file: args.mysql_password_file.or(mysql.password_file),
timeout: args.mysql_connect_timeout.or(mysql.timeout),
},
})
}
pub fn read_config_form_path(config_path: Option<PathBuf>) -> anyhow::Result<ServerConfig> {
pub fn read_config_from_path(config_path: Option<PathBuf>) -> anyhow::Result<ServerConfig> {
let config_path = config_path.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH));
log::debug!("Reading config from {:?}", &config_path);
@ -97,30 +123,52 @@ pub fn read_config_form_path(config_path: Option<PathBuf>) -> anyhow::Result<Ser
))
}
/// Use the provided configuration to establish a connection to a MySQL server.
pub async fn create_mysql_connection_from_config(
config: &MysqlConfig,
) -> anyhow::Result<MySqlConnection> {
fn log_config(config: &MysqlConfig) {
let mut display_config = config.clone();
"<REDACTED>".clone_into(&mut display_config.password);
display_config.password = display_config
.password
.as_ref()
.map(|_| "<REDACTED>".to_owned());
log::debug!(
"Connecting to MySQL server with parameters: {:#?}",
display_config
);
}
/// Use the provided configuration to establish a connection to a MySQL server.
pub async fn create_mysql_connection_from_config(
config: &MysqlConfig,
) -> anyhow::Result<MySqlConnection> {
log_config(config);
let mut mysql_options = MySqlConnectOptions::new().database("mysql");
if let Some(username) = &config.username {
mysql_options = mysql_options.username(username);
}
if let Some(password) = &config.password {
mysql_options = mysql_options.password(password);
}
if let Some(socket_path) = &config.socket_path {
mysql_options = mysql_options.socket(socket_path);
} else if let Some(host) = &config.host {
mysql_options = mysql_options.host(host);
mysql_options = mysql_options.port(config.port.unwrap_or(DEFAULT_PORT));
} else {
anyhow::bail!("No MySQL host or socket path provided");
}
match tokio::time::timeout(
Duration::from_secs(config.timeout.unwrap_or(DEFAULT_TIMEOUT)),
MySqlConnectOptions::new()
.host(&config.host)
.username(&config.username)
.password(&config.password)
.port(config.port.unwrap_or(DEFAULT_PORT))
.database("mysql")
.connect(),
mysql_options.connect(),
)
.await
{
Ok(connection) => connection.context("Failed to connect to MySQL"),
Err(_) => Err(anyhow!("Timed out after 2 seconds")).context("Failed to connect to MySQL"),
Ok(connection) => connection.context("Failed to connect to the database"),
Err(_) => {
Err(anyhow!("Timed out after 2 seconds")).context("Failed to connect to the database")
}
}
}

View File

@ -1,7 +1,7 @@
use std::{collections::BTreeSet, fs, path::PathBuf};
use futures_util::{SinkExt, StreamExt};
use tokio::io::AsyncWriteExt;
use indoc::concatdoc;
use tokio::net::{UnixListener, UnixStream};
use sqlx::prelude::*;
@ -57,15 +57,43 @@ pub async fn listen_for_incoming_connections(
sd_notify::notify(true, &[sd_notify::NotifyState::Ready]).ok();
while let Ok((mut conn, _addr)) = listener.accept().await {
let uid = conn.peer_cred()?.uid();
while let Ok((conn, _addr)) = listener.accept().await {
let uid = match conn.peer_cred() {
Ok(cred) => cred.uid(),
Err(e) => {
log::error!("Failed to get peer credentials from socket: {}", e);
let mut message_stream = create_server_to_client_message_stream(conn);
message_stream
.send(Response::Error(
(concatdoc! {
"Server failed to get peer credentials from socket\n",
"Please check the server logs or contact the system administrators"
})
.to_string(),
))
.await
.ok();
continue;
}
};
log::trace!("Accepted connection from uid {}", uid);
let unix_user = match UnixUser::from_uid(uid) {
Ok(user) => user,
Err(e) => {
eprintln!("Failed to get UnixUser from uid: {}", e);
conn.shutdown().await?;
log::error!("Failed to get username from uid: {}", e);
let mut message_stream = create_server_to_client_message_stream(conn);
message_stream
.send(Response::Error(
(concatdoc! {
"Server failed to get user data from the system\n",
"Please check the server logs or contact the system administrators"
})
.to_string(),
))
.await
.ok();
continue;
}
};
@ -73,9 +101,9 @@ pub async fn listen_for_incoming_connections(
log::info!("Accepted connection from {}", unix_user.username);
match handle_requests_for_single_session(conn, &unix_user, &config).await {
Ok(_) => {}
Ok(()) => {}
Err(e) => {
eprintln!("Failed to run server: {}", e);
log::error!("Failed to run server: {}", e);
}
}
}
@ -88,8 +116,24 @@ pub async fn handle_requests_for_single_session(
unix_user: &UnixUser,
config: &ServerConfig,
) -> anyhow::Result<()> {
let message_stream = create_server_to_client_message_stream(socket);
let mut db_connection = create_mysql_connection_from_config(&config.mysql).await?;
let mut message_stream = create_server_to_client_message_stream(socket);
let mut db_connection = match create_mysql_connection_from_config(&config.mysql).await {
Ok(connection) => connection,
Err(err) => {
message_stream
.send(Response::Error(
(concatdoc! {
"Server failed to connect to database\n",
"Please check the server logs or contact the system administrators"
})
.to_string(),
))
.await?;
message_stream.flush().await?;
return Err(err);
}
};
log::debug!("Successfully connected to database");
let result = handle_requests_for_single_session_with_db_connection(
@ -100,9 +144,9 @@ pub async fn handle_requests_for_single_session(
.await;
if let Err(e) = db_connection.close().await {
eprintln!("Failed to close database connection: {}", e);
eprintln!("{}", e);
eprintln!("Ignoring...");
log::error!("Failed to close database connection: {}", e);
log::error!("{}", e);
log::error!("Ignoring...");
}
result
@ -116,6 +160,7 @@ pub async fn handle_requests_for_single_session_with_db_connection(
unix_user: &UnixUser,
db_connection: &mut MySqlConnection,
) -> anyhow::Result<()> {
stream.send(Response::Ready).await?;
loop {
// TODO: better error handling
let request = match stream.next().await {
@ -133,17 +178,14 @@ pub async fn handle_requests_for_single_session_with_db_connection(
Request::CreateDatabases(databases_names) => {
let result = create_databases(databases_names, unix_user, db_connection).await;
stream.send(Response::CreateDatabases(result)).await?;
stream.flush().await?;
}
Request::DropDatabases(databases_names) => {
let result = drop_databases(databases_names, unix_user, db_connection).await;
stream.send(Response::DropDatabases(result)).await?;
stream.flush().await?;
}
Request::ListDatabases => {
let result = list_databases_for_user(unix_user, db_connection).await;
stream.send(Response::ListAllDatabases(result)).await?;
stream.flush().await?;
}
Request::ListPrivileges(database_names) => {
let response = match database_names {
@ -161,7 +203,6 @@ pub async fn handle_requests_for_single_session_with_db_connection(
};
stream.send(response).await?;
stream.flush().await?;
}
Request::ModifyPrivileges(database_privilege_diffs) => {
let result = apply_privilege_diffs(
@ -171,24 +212,20 @@ pub async fn handle_requests_for_single_session_with_db_connection(
)
.await;
stream.send(Response::ModifyPrivileges(result)).await?;
stream.flush().await?;
}
Request::CreateUsers(db_users) => {
let result = create_database_users(db_users, unix_user, db_connection).await;
stream.send(Response::CreateUsers(result)).await?;
stream.flush().await?;
}
Request::DropUsers(db_users) => {
let result = drop_database_users(db_users, unix_user, db_connection).await;
stream.send(Response::DropUsers(result)).await?;
stream.flush().await?;
}
Request::PasswdUser(db_user, password) => {
let result =
set_password_for_database_user(&db_user, &password, unix_user, db_connection)
.await;
stream.send(Response::PasswdUser(result)).await?;
stream.flush().await?;
}
Request::ListUsers(db_users) => {
let response = match db_users {
@ -203,22 +240,21 @@ pub async fn handle_requests_for_single_session_with_db_connection(
}
};
stream.send(response).await?;
stream.flush().await?;
}
Request::LockUsers(db_users) => {
let result = lock_database_users(db_users, unix_user, db_connection).await;
stream.send(Response::LockUsers(result)).await?;
stream.flush().await?;
}
Request::UnlockUsers(db_users) => {
let result = unlock_database_users(db_users, unix_user, db_connection).await;
stream.send(Response::UnlockUsers(result)).await?;
stream.flush().await?;
}
Request::Exit => {
break;
}
}
stream.flush().await?;
}
Ok(())

View File

@ -32,7 +32,7 @@ use crate::{
},
},
server::{
common::create_user_group_matching_regex,
common::{create_user_group_matching_regex, try_get_with_binary_fallback},
input_sanitization::{quote_identifier, validate_name, validate_ownership_by_unix_user},
sql::database_operations::unsafe_database_exists,
},
@ -42,8 +42,8 @@ use crate::{
/// from the `db` table in the database. If you need to add or remove privilege
/// fields, this is a good place to start.
pub const DATABASE_PRIVILEGE_FIELDS: [&str; 13] = [
"db",
"user",
"Db",
"User",
"select_priv",
"insert_priv",
"update_priv",
@ -97,6 +97,8 @@ impl DatabasePrivilegeRow {
}
}
// TODO: get by name instead of row tuple position
#[inline]
fn get_mysql_row_priv_field(row: &MySqlRow, position: usize) -> Result<bool, sqlx::Error> {
let field = DATABASE_PRIVILEGE_FIELDS[position];
@ -113,8 +115,8 @@ fn get_mysql_row_priv_field(row: &MySqlRow, position: usize) -> Result<bool, sql
impl FromRow<'_, MySqlRow> for DatabasePrivilegeRow {
fn from_row(row: &MySqlRow) -> Result<Self, sqlx::Error> {
Ok(Self {
db: row.try_get("db")?,
user: row.try_get("user")?,
db: try_get_with_binary_fallback(row, "Db")?,
user: try_get_with_binary_fallback(row, "User")?,
select_priv: get_mysql_row_priv_field(row, 2)?,
insert_priv: get_mysql_row_priv_field(row, 3)?,
update_priv: get_mysql_row_priv_field(row, 4)?,
@ -137,7 +139,7 @@ async fn unsafe_get_database_privileges(
connection: &mut MySqlConnection,
) -> Result<Vec<DatabasePrivilegeRow>, sqlx::Error> {
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
"SELECT {} FROM `db` WHERE `db` = ?",
"SELECT {} FROM `db` WHERE `Db` = ?",
DATABASE_PRIVILEGE_FIELDS
.iter()
.map(|field| quote_identifier(field))
@ -166,7 +168,7 @@ pub async fn unsafe_get_database_privileges_for_db_user_pair(
connection: &mut MySqlConnection,
) -> Result<Option<DatabasePrivilegeRow>, sqlx::Error> {
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&format!(
"SELECT {} FROM `db` WHERE `db` = ? AND `user` = ?",
"SELECT {} FROM `db` WHERE `Db` = ? AND `User` = ?",
DATABASE_PRIVILEGE_FIELDS
.iter()
.map(|field| quote_identifier(field))
@ -316,7 +318,7 @@ async fn unsafe_apply_privilege_diff(
.join(",");
sqlx::query(
format!("UPDATE `db` SET {} WHERE `db` = ? AND `user` = ?", changes).as_str(),
format!("UPDATE `db` SET {} WHERE `Db` = ? AND `User` = ?", changes).as_str(),
)
.bind(p.db.to_string())
.bind(p.user.to_string())
@ -325,7 +327,7 @@ async fn unsafe_apply_privilege_diff(
.map(|_| ())
}
DatabasePrivilegesDiff::Deleted(p) => {
sqlx::query("DELETE FROM `db` WHERE `db` = ? AND `user` = ?")
sqlx::query("DELETE FROM `db` WHERE `Db` = ? AND `User` = ?")
.bind(p.db.to_string())
.bind(p.user.to_string())
.execute(connection)

View File

@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
use sqlx::prelude::*;
use sqlx::MySqlConnection;
use crate::server::common::try_get_with_binary_fallback;
use crate::{
core::{
common::UnixUser,
@ -350,20 +351,6 @@ pub struct DatabaseUser {
pub databases: Vec<String>,
}
/// Some mysql versions with some collations mark some columns as binary fields,
/// which in the current version of sqlx is not parsable as string.
/// See: https://github.com/launchbadge/sqlx/issues/3387
#[inline]
fn try_get_with_binary_fallback(
row: &sqlx::mysql::MySqlRow,
column: &str,
) -> Result<String, sqlx::Error> {
row.try_get(column).or_else(|_| {
row.try_get::<Vec<u8>, _>(column)
.map(|v| String::from_utf8_lossy(&v).to_string())
})
}
impl FromRow<'_, sqlx::mysql::MySqlRow> for DatabaseUser {
fn from_row(row: &sqlx::mysql::MySqlRow) -> Result<Self, sqlx::Error> {
Ok(Self {