1 Commits

Author SHA1 Message Date
3c84dd5be1 Create deb package
All checks were successful
Build / check (pull_request) Successful in 12m17s
Build / docs (pull_request) Successful in 12m58s
Build / build (pull_request) Successful in 13m44s
2025-01-18 23:40:55 +01:00
25 changed files with 515 additions and 766 deletions

View File

@@ -68,6 +68,6 @@ jobs:
target: ${{ gitea.ref_name }}/docs/
username: gitea-web
ssh-key: ${{ secrets.WEB_SYNC_SSH_KEY }}
host: pages.pvv.ntnu.no
known-hosts: "pages.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH2QjfFB+city1SYqltkVqWACfo1j37k+oQQfj13mtgg"
host: bekkalokk.pvv.ntnu.no
known-hosts: "bekkalokk.pvv.ntnu.no ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEI6VSaDrMG8+flg4/AeHlAFIen8RUzWh6URQKqFegSx"

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ config.toml
/.direnv/
result
result-*
# Packaging
/assets/completions/

1050
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,37 +2,47 @@
name = "mysqladm-rs"
version = "0.1.0"
edition = "2021"
license = "BSD3"
authors = [
"oysteikt@pvv.ntnu.no",
"felixalb@pvv.ntnu.no",
]
repository = "https://git.pvv.ntnu.no/Projects/mysqladm-rs"
description = "A command-line utility for MySQL administration for non-admin users"
categories = ["command-line-interface", "command-line-utilities"]
keywords = ["mysql", "cli", "administration"]
readme = "README.md"
[dependencies]
anyhow = "1.0.98"
async-bincode = "0.8.0"
bincode = "2.0.1"
clap = { version = "4.5.41", features = ["derive"] }
clap-verbosity-flag = "3.0.3"
clap_complete = "4.5.55"
derive_more = { version = "2.0.1", features = ["display", "error"] }
anyhow = "1.0.95"
async-bincode = "0.7.3"
bincode = "1.3.3"
clap = { version = "4.5.26", features = ["derive"] }
clap-verbosity-flag = "2.2.3"
clap_complete = "4.5.42"
derive_more = { version = "1.0.0", features = ["display", "error"] }
dialoguer = "0.11.0"
env_logger = "0.11.8"
env_logger = "0.11.6"
futures = "0.3.31"
futures-util = "0.3.31"
indoc = "2.0.6"
itertools = "0.14.0"
log = "0.4.27"
nix = { version = "0.30.1", features = ["fs", "process", "socket", "user"] }
indoc = "2.0.5"
itertools = "0.13.0"
log = "0.4.25"
nix = { version = "0.29.0", features = ["fs", "process", "socket", "user"] }
prettytable = "0.10.0"
rand = "0.9.1"
ratatui = { version = "0.29.0", optional = true }
rand = "0.8.5"
ratatui = { version = "0.28.1", optional = true }
sd-notify = "0.4.5"
serde = "1.0.219"
serde_json = { version = "1.0.140", features = ["preserve_order"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "mysql", "tls-rustls"] }
systemd-journal-logger = "2.2.2"
tokio = { version = "1.46.1", features = ["rt", "macros"] }
serde = "1.0.217"
serde_json = { version = "1.0.135", features = ["preserve_order"] }
sqlx = { version = "0.8.3", features = ["runtime-tokio", "mysql", "tls-rustls"] }
systemd-journal-logger = "2.2.0"
tokio = { version = "1.43.0", features = ["rt", "macros"] }
tokio-serde = { version = "0.9.0", features = ["bincode"] }
tokio-stream = "0.1.17"
tokio-util = { version = "0.7.15", features = ["codec"] }
toml = "0.8.23"
uuid = { version = "1.17.0", features = ["v4"] }
tokio-util = { version = "0.7.13", features = ["codec"] }
toml = "0.8.19"
uuid = { version = "1.12.0", features = ["v4"] }
[features]
default = ["mysql-admutils-compatibility"]
@@ -50,7 +60,41 @@ lto = true
codegen-units = 1
[build-dependencies]
anyhow = "1.0.98"
anyhow = "1.0.95"
[dev-dependencies]
regex = "1.11.1"
# TODO: package shell completions
[package.metadata.deb]
maintainer = "Programvareverkstedet <projects@pvv.ntnu.no>"
section = "admin"
assets = [
[
"target/release/mysqladm",
"usr/bin/",
"755",
],
[
"example-config.toml",
"etc/mysqladm/config.toml",
"644",
],
[
"assets/completions/_*",
"usr/share/zsh/site-functions/completions/",
"644",
],
[
"assets/completions/*.bash",
"usr/share/bash-completion/completions/",
"644",
],
[
"assets/completions/*.fish",
"usr/share/fish/vendor_completions.d/",
"644",
],
]
conf-files = ["etc/mysqladm/config.toml"]
depends = []

View File

@@ -2,26 +2,7 @@
# mysqladm-rs
Healing mysql spasms since 2024
## What is this?
This is a CLI tool that let's normal users perform administrative operations on a MySQL DBMS, with some restrictions.
The default restriction is to only let the user perform these actions on databases and database users that are prefixed with their username,
or with the name of any unix group that the user is a part of. i.e. `<user>_mydb`, `<user>_mydbuser`, or `<group>_myotherdb`.
The administrative actions available to the user includes:
- creating/listing/modifying/deleting databases and database users
- modifying database user privileges
- changing the passwords of the database users
- locking and unlocking database user accounts
- ... more to come
The software is split into a client and a server. The server has administrative access to the mysql server,
and is responsible for checking client authorization for the different types of actions the client might request.
This is designed for (and is only really useful for) multi-user servers, like tilde servers, university unix servers, etc.
Work in progress rewrite of https://git.pvv.ntnu.no/Projects/mysql-admutils
## Installation

19
create-deb.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
cargo build --release
mkdir -p assets/completions
./target/release/mysqladm generate-completions --shell bash > assets/completions/mysqladm.bash
./target/release/mysqladm generate-completions --shell zsh > assets/completions/_mysqladm
./target/release/mysqladm generate-completions --shell fish > assets/completions/mysqladm.fish
./target/release/mysqladm generate-completions --shell bash --command mysql-dbadm > assets/completions/mysql-dbadm.bash
./target/release/mysqladm generate-completions --shell zsh --command mysql-dbadm > assets/completions/_mysql-dbadm
./target/release/mysqladm generate-completions --shell fish --command mysql-dbadm > assets/completions/mysql-dbadm.fish
./target/release/mysqladm generate-completions --shell bash --command mysql-useradm > assets/completions/mysql-useradm.bash
./target/release/mysqladm generate-completions --shell zsh --command mysql-useradm > assets/completions/_mysql-useradm
./target/release/mysqladm generate-completions --shell fish --command mysql-useradm > assets/completions/mysql-useradm.fish
cargo deb

View File

@@ -19,4 +19,4 @@ port = 3306
username = "root"
password = "secret"
timeout = 2 # seconds
timeout = 2 # seconds

12
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1751984180,
"narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=",
"lastModified": 1737062831,
"narHash": "sha256-Tbk1MZbtV2s5aG+iM99U8FqwxU/YNArMcWAv6clcsBc=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0",
"rev": "5df43628fdf08d642be8ba5b3625a6c70731c19c",
"type": "github"
},
"original": {
@@ -29,11 +29,11 @@
]
},
"locked": {
"lastModified": 1752201818,
"narHash": "sha256-d8KczaVT8WFEZdWg//tMAbv8EDyn2YTWcJvSY8gqKBU=",
"lastModified": 1737166965,
"narHash": "sha256-vlDROBAgq+7PEVM0vaS2zboY6DXs3oKK0qW/1dVuFs4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "bd8f8329780b348fedcd37b53dbbee48c08c496d",
"rev": "fc839c9d5d1ebc789b4657c43c4d54838c7c01de",
"type": "github"
},
"original": {

View File

@@ -47,8 +47,8 @@
toolchain
mysql-client
cargo-nextest
cargo-edit
cargo-deny
cargo-deb
];
RUST_SRC_PATH = "${toolchain}/lib/rustlib/src/rust/library";

View File

@@ -1 +0,0 @@
style_edition = "2024"

View File

@@ -2,7 +2,7 @@ use anyhow::Context;
use clap::Parser;
use dialoguer::{Confirm, Editor};
use futures_util::{SinkExt, StreamExt};
use nix::unistd::{User, getuid};
use nix::unistd::{getuid, User};
use prettytable::{Cell, Row, Table};
use crate::{
@@ -15,13 +15,13 @@ use crate::{
parse_privilege_table_cli_arg,
},
protocol::{
ClientToServerMessageStream, MySQLDatabase, Request, Response,
print_create_databases_output_status, print_create_databases_output_status_json,
print_drop_databases_output_status, print_drop_databases_output_status_json,
print_modify_database_privileges_output_status,
print_modify_database_privileges_output_status, ClientToServerMessageStream,
MySQLDatabase, Request, Response,
},
},
server::sql::database_privilege_operations::{DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow},
server::sql::database_privilege_operations::{DatabasePrivilegeRow, DATABASE_PRIVILEGE_FIELDS},
};
#[derive(Parser, Debug, Clone)]

View File

@@ -19,8 +19,8 @@ use crate::{
core::{
bootstrap::bootstrap_server_connection_and_drop_privileges,
protocol::{
ClientToServerMessageStream, GetDatabasesPrivilegeDataError, MySQLDatabase, Request,
Response, create_client_to_server_message_stream,
create_client_to_server_message_stream, ClientToServerMessageStream,
GetDatabasesPrivilegeDataError, MySQLDatabase, Request, Response,
},
},
server::sql::database_privilege_operations::DatabasePrivilegeRow,
@@ -307,7 +307,11 @@ async fn show_databases(
#[inline]
fn yn(value: bool) -> &'static str {
if value { "Y" } else { "N" }
if value {
"Y"
} else {
"N"
}
}
fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) -> anyhow::Result<()> {

View File

@@ -19,8 +19,8 @@ use crate::{
core::{
bootstrap::bootstrap_server_connection_and_drop_privileges,
protocol::{
ClientToServerMessageStream, MySQLUser, Request, Response,
create_client_to_server_message_stream,
create_client_to_server_message_stream, ClientToServerMessageStream, MySQLUser,
Request, Response,
},
},
server::sql::user_operations::DatabaseUser,

View File

@@ -4,12 +4,12 @@ use dialoguer::{Confirm, Password};
use futures_util::{SinkExt, StreamExt};
use crate::core::protocol::{
ClientToServerMessageStream, ListUsersError, MySQLUser, Request, Response,
print_create_users_output_status, print_create_users_output_status_json,
print_drop_users_output_status, print_drop_users_output_status_json,
print_lock_users_output_status, print_lock_users_output_status_json,
print_set_password_output_status, print_unlock_users_output_status,
print_unlock_users_output_status_json,
print_unlock_users_output_status_json, ClientToServerMessageStream, ListUsersError, MySQLUser,
Request, Response,
};
use super::common::erroneous_server_response;

View File

@@ -1,12 +1,12 @@
use std::{fs, path::PathBuf};
use anyhow::Context;
use nix::libc::{EXIT_SUCCESS, exit};
use nix::libc::{exit, EXIT_SUCCESS};
use std::os::unix::net::UnixStream as StdUnixStream;
use tokio::net::UnixStream as TokioUnixStream;
use crate::{
core::common::{DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser},
core::common::{UnixUser, DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH},
server::{config::read_config_from_path, server_loop::handle_requests_for_single_session},
};

View File

@@ -66,7 +66,11 @@ impl UnixUser {
#[inline]
pub(crate) fn yn(b: bool) -> &'static str {
if b { "Y" } else { "N" }
if b {
"Y"
} else {
"N"
}
}
#[inline]

View File

@@ -1,4 +1,4 @@
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use itertools::Itertools;
use prettytable::Table;
use serde::{Deserialize, Serialize};
@@ -12,7 +12,7 @@ use super::{
protocol::{MySQLDatabase, MySQLUser},
};
use crate::server::sql::database_privilege_operations::{
DATABASE_PRIVILEGE_FIELDS, DatabasePrivilegeRow,
DatabasePrivilegeRow, DATABASE_PRIVILEGE_FIELDS,
};
pub fn db_priv_field_human_readable_name(name: &str) -> String {
@@ -288,10 +288,10 @@ fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
match parts.len() {
n if (n < DATABASE_PRIVILEGE_FIELDS.len()) => {
return PrivilegeRowParseResult::TooFewFields(n);
return PrivilegeRowParseResult::TooFewFields(n)
}
n if (n > DATABASE_PRIVILEGE_FIELDS.len()) => {
return PrivilegeRowParseResult::TooManyFields(n);
return PrivilegeRowParseResult::TooManyFields(n)
}
_ => {}
}

View File

@@ -7,7 +7,7 @@ use std::{
use serde::{Deserialize, Serialize};
use tokio::net::UnixStream;
use tokio_serde::{Framed as SerdeFramed, formats::Bincode};
use tokio_serde::{formats::Bincode, Framed as SerdeFramed};
use tokio_util::codec::{Framed, LengthDelimitedCodec};
use crate::core::{database_privileges::DatabasePrivilegesDiff, protocol::*};

View File

@@ -2,7 +2,7 @@
extern crate prettytable;
use clap::{CommandFactory, Parser, ValueEnum};
use clap_complete::{Shell, generate};
use clap_complete::{generate, Shell};
use clap_verbosity_flag::Verbosity;
use std::path::PathBuf;
@@ -15,7 +15,7 @@ use futures::StreamExt;
use crate::{
core::{
bootstrap::bootstrap_server_connection_and_drop_privileges,
protocol::{Response, create_client_to_server_message_stream},
protocol::{create_client_to_server_message_stream, Response},
},
server::command::ServerArgs,
};

View File

@@ -12,7 +12,7 @@ use std::os::unix::net::UnixStream as StdUnixStream;
use tokio::net::UnixStream as TokioUnixStream;
use crate::core::common::UnixUser;
use crate::core::protocol::{Response, create_server_to_client_message_stream};
use crate::core::protocol::{create_server_to_client_message_stream, Response};
use crate::server::config::read_config_from_path_with_arg_overrides;
use crate::server::server_loop::listen_for_incoming_connections;
use crate::server::{

View File

@@ -1,9 +1,9 @@
use std::{fs, path::PathBuf, time::Duration};
use anyhow::{Context, anyhow};
use anyhow::{anyhow, Context};
use clap::Parser;
use serde::{Deserialize, Serialize};
use sqlx::{ConnectOptions, MySqlConnection, mysql::MySqlConnectOptions};
use sqlx::{mysql::MySqlConnectOptions, ConnectOptions, MySqlConnection};
use crate::core::common::DEFAULT_CONFIG_PATH;

View File

@@ -4,20 +4,20 @@ use futures_util::{SinkExt, StreamExt};
use indoc::concatdoc;
use tokio::net::{UnixListener, UnixStream};
use sqlx::MySqlConnection;
use sqlx::prelude::*;
use sqlx::MySqlConnection;
use crate::core::protocol::SetPasswordError;
use crate::server::sql::database_operations::list_databases;
use crate::{
core::{
common::{DEFAULT_SOCKET_PATH, UnixUser},
common::{UnixUser, DEFAULT_SOCKET_PATH},
protocol::request_response::{
Request, Response, ServerToClientMessageStream, create_server_to_client_message_stream,
create_server_to_client_message_stream, Request, Response, ServerToClientMessageStream,
},
},
server::{
config::{ServerConfig, create_mysql_connection_from_config},
config::{create_mysql_connection_from_config, ServerConfig},
sql::{
database_operations::{create_databases, drop_databases, list_all_databases_for_user},
database_privilege_operations::{

View File

@@ -1,7 +1,7 @@
use std::collections::BTreeMap;
use sqlx::MySqlConnection;
use sqlx::prelude::*;
use sqlx::MySqlConnection;
use serde::{Deserialize, Serialize};

View File

@@ -19,11 +19,11 @@ use std::collections::{BTreeMap, BTreeSet};
use indoc::indoc;
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use sqlx::{MySqlConnection, mysql::MySqlRow, prelude::*};
use sqlx::{mysql::MySqlRow, prelude::*, MySqlConnection};
use crate::{
core::{
common::{UnixUser, rev_yn, yn},
common::{rev_yn, yn, UnixUser},
database_privileges::{DatabasePrivilegeChange, DatabasePrivilegesDiff},
protocol::{
DiffDoesNotApplyError, GetAllDatabasesPrivilegeData, GetAllDatabasesPrivilegeDataError,
@@ -280,8 +280,9 @@ async fn unsafe_apply_privilege_diff(
.map(|field| quote_identifier(field))
.join(",");
let question_marks =
std::iter::repeat_n("?", DATABASE_PRIVILEGE_FIELDS.len()).join(",");
let question_marks = std::iter::repeat("?")
.take(DATABASE_PRIVILEGE_FIELDS.len())
.join(",");
sqlx::query(
format!("INSERT INTO `db` ({}) VALUES ({})", tables, question_marks).as_str(),

View File

@@ -4,8 +4,8 @@ use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use sqlx::MySqlConnection;
use sqlx::prelude::*;
use sqlx::MySqlConnection;
use crate::{
core::{