Compare commits
3 Commits
9892370632
...
a160407906
Author | SHA1 | Date |
---|---|---|
Oystein Kristoffer Tveit | a160407906 | |
Oystein Kristoffer Tveit | 69870147f5 | |
Oystein Kristoffer Tveit | f78c88517f |
29
Cargo.toml
29
Cargo.toml
|
@ -2,6 +2,17 @@
|
||||||
name = "mysqladm-rs"
|
name = "mysqladm-rs"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
# TODO: change licensing?
|
||||||
|
license = "GPL-2.0-only"
|
||||||
|
authors = ["oysteikt@pvv.ntnu.no"]
|
||||||
|
repository = "https://git.pvv.ntnu.no/Projects/mysqladm-rs"
|
||||||
|
# TODO: write a proper description
|
||||||
|
description = """\
|
||||||
|
asdf
|
||||||
|
"""
|
||||||
|
categories = ["command-line-interface", "command-line-utilities"]
|
||||||
|
keywords = ["mysql", "cli", "administration"]
|
||||||
|
readme = "README.md"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
|
@ -37,3 +48,21 @@ codegen-units = 1
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
|
|
||||||
|
# TODO: package shell completions
|
||||||
|
[package.metadata.deb]
|
||||||
|
maintainer = "Programvareverkstedet <projects@pvv.ntnu.no>"
|
||||||
|
section = "admin"
|
||||||
|
assets = [
|
||||||
|
[
|
||||||
|
"target/release/mysqladm",
|
||||||
|
"usr/bin/",
|
||||||
|
"4755",
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"example-config.toml",
|
||||||
|
"etc/mysqladm/config.toml",
|
||||||
|
"644",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
conf-files = ["etc/mysqladm/config.toml"]
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
# This should go to `/etc/mysqladm/config.toml`
|
|
||||||
|
|
||||||
[mysql]
|
[mysql]
|
||||||
host = "localhost"
|
host = "localhost"
|
||||||
port = 3306
|
port = 3306
|
||||||
|
|
|
@ -178,6 +178,8 @@ async fn create_databases(
|
||||||
eprintln!("Failed to create database '{}': {}", name, e);
|
eprintln!("Failed to create database '{}': {}", name, e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...");
|
||||||
result = CommandStatus::PartiallySuccessfullyModified;
|
result = CommandStatus::PartiallySuccessfullyModified;
|
||||||
|
} else {
|
||||||
|
println!("Database '{}' created.", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,6 +202,8 @@ async fn drop_databases(
|
||||||
eprintln!("Failed to drop database '{}': {}", name, e);
|
eprintln!("Failed to drop database '{}': {}", name, e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...");
|
||||||
result = CommandStatus::PartiallySuccessfullyModified;
|
result = CommandStatus::PartiallySuccessfullyModified;
|
||||||
|
} else {
|
||||||
|
println!("Database '{}' dropped.", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,14 @@ pub enum UserCommand {
|
||||||
/// If no username is provided, all users you have access will be shown.
|
/// If no username is provided, all users you have access will be shown.
|
||||||
#[command()]
|
#[command()]
|
||||||
ShowUser(UserShowArgs),
|
ShowUser(UserShowArgs),
|
||||||
|
|
||||||
|
/// Lock account for one or more users
|
||||||
|
#[command()]
|
||||||
|
LockUser(UserLockArgs),
|
||||||
|
|
||||||
|
/// Unlock account for one or more users
|
||||||
|
#[command()]
|
||||||
|
UnlockUser(UserUnlockArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
|
@ -75,6 +83,18 @@ pub struct UserShowArgs {
|
||||||
json: bool,
|
json: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct UserLockArgs {
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
username: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct UserUnlockArgs {
|
||||||
|
#[arg(num_args = 1..)]
|
||||||
|
username: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_command(
|
pub async fn handle_command(
|
||||||
command: UserCommand,
|
command: UserCommand,
|
||||||
mut connection: MySqlConnection,
|
mut connection: MySqlConnection,
|
||||||
|
@ -87,6 +107,8 @@ pub async fn handle_command(
|
||||||
UserCommand::DropUser(args) => drop_users(args, txn).await,
|
UserCommand::DropUser(args) => drop_users(args, txn).await,
|
||||||
UserCommand::PasswdUser(args) => change_password_for_user(args, txn).await,
|
UserCommand::PasswdUser(args) => change_password_for_user(args, txn).await,
|
||||||
UserCommand::ShowUser(args) => show_users(args, txn).await,
|
UserCommand::ShowUser(args) => show_users(args, txn).await,
|
||||||
|
UserCommand::LockUser(args) => lock_users(args, txn).await,
|
||||||
|
UserCommand::UnlockUser(args) => unlock_users(args, txn).await,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -154,6 +176,8 @@ async fn drop_users(
|
||||||
eprintln!("{}", e);
|
eprintln!("{}", e);
|
||||||
eprintln!("Skipping...");
|
eprintln!("Skipping...");
|
||||||
result = CommandStatus::PartiallySuccessfullyModified;
|
result = CommandStatus::PartiallySuccessfullyModified;
|
||||||
|
} else {
|
||||||
|
println!("User '{}' dropped.", username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,6 +260,7 @@ async fn show_users(
|
||||||
json!({
|
json!({
|
||||||
"user": user.user,
|
"user": user.user,
|
||||||
"has_password": user.has_password,
|
"has_password": user.has_password,
|
||||||
|
"is_locked": user.is_locked,
|
||||||
"databases": user_databases.get(&user.user).unwrap_or(&vec![]),
|
"databases": user_databases.get(&user.user).unwrap_or(&vec![]),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -252,12 +277,14 @@ async fn show_users(
|
||||||
table.add_row(row![
|
table.add_row(row![
|
||||||
"User",
|
"User",
|
||||||
"Password is set",
|
"Password is set",
|
||||||
|
"Locked",
|
||||||
"Databases where user has privileges"
|
"Databases where user has privileges"
|
||||||
]);
|
]);
|
||||||
for user in users {
|
for user in users {
|
||||||
table.add_row(row![
|
table.add_row(row![
|
||||||
user.user,
|
user.user,
|
||||||
user.has_password,
|
user.has_password,
|
||||||
|
user.is_locked,
|
||||||
user_databases.get(&user.user).unwrap_or(&vec![]).join("\n")
|
user_databases.get(&user.user).unwrap_or(&vec![]).join("\n")
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -266,3 +293,49 @@ async fn show_users(
|
||||||
|
|
||||||
Ok(CommandStatus::NoModificationsIntended)
|
Ok(CommandStatus::NoModificationsIntended)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn lock_users(
|
||||||
|
args: UserLockArgs,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<CommandStatus> {
|
||||||
|
if args.username.is_empty() {
|
||||||
|
anyhow::bail!("No usernames provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = CommandStatus::SuccessfullyModified;
|
||||||
|
|
||||||
|
for username in args.username {
|
||||||
|
if let Err(e) = lock_database_user(&username, connection).await {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
eprintln!("Skipping...");
|
||||||
|
result = CommandStatus::PartiallySuccessfullyModified;
|
||||||
|
} else {
|
||||||
|
println!("User '{}' locked.", username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unlock_users(
|
||||||
|
args: UserUnlockArgs,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<CommandStatus> {
|
||||||
|
if args.username.is_empty() {
|
||||||
|
anyhow::bail!("No usernames provided");
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = CommandStatus::SuccessfullyModified;
|
||||||
|
|
||||||
|
for username in args.username {
|
||||||
|
if let Err(e) = unlock_database_user(&username, connection).await {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
eprintln!("Skipping...");
|
||||||
|
result = CommandStatus::PartiallySuccessfullyModified;
|
||||||
|
} else {
|
||||||
|
println!("User '{}' unlocked.", username);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
|
@ -99,6 +99,79 @@ pub async fn set_password_for_database_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn user_is_locked(db_user: &str, connection: &mut MySqlConnection) -> anyhow::Result<bool> {
|
||||||
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
|
||||||
|
if !user_exists(db_user, connection).await? {
|
||||||
|
anyhow::bail!("User '{}' does not exist", db_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_locked = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT JSON_EXTRACT(`mysql`.`global_priv`.`priv`, "$.account_locked") = 'true'
|
||||||
|
FROM `mysql`.`global_priv`
|
||||||
|
WHERE `User` = ?
|
||||||
|
AND `Host` = '%'
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(db_user)
|
||||||
|
.fetch_one(connection)
|
||||||
|
.await?
|
||||||
|
.get::<bool, _>(0);
|
||||||
|
|
||||||
|
Ok(is_locked)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn lock_database_user(
|
||||||
|
db_user: &str,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
|
||||||
|
if !user_exists(db_user, connection).await? {
|
||||||
|
anyhow::bail!("User '{}' does not exist", db_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if user_is_locked(db_user, connection).await? {
|
||||||
|
anyhow::bail!("User '{}' is already locked", db_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
|
||||||
|
sqlx::query(format!("ALTER USER {}@'%' ACCOUNT LOCK", quote_literal(db_user),).as_str())
|
||||||
|
.execute(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unlock_database_user(
|
||||||
|
db_user: &str,
|
||||||
|
connection: &mut MySqlConnection,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let unix_user = get_current_unix_user()?;
|
||||||
|
|
||||||
|
validate_user_name(db_user, &unix_user)?;
|
||||||
|
|
||||||
|
if !user_exists(db_user, connection).await? {
|
||||||
|
anyhow::bail!("User '{}' does not exist", db_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user_is_locked(db_user, connection).await? {
|
||||||
|
anyhow::bail!("User '{}' is already unlocked", db_user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: see the note about SQL injections in `validate_ownership_of_user_name`
|
||||||
|
sqlx::query(format!("ALTER USER {}@'%' ACCOUNT UNLOCK", quote_literal(db_user),).as_str())
|
||||||
|
.execute(connection)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// This struct contains information about a database user.
|
/// This struct contains information about a database user.
|
||||||
/// This can be extended if we need more information in the future.
|
/// This can be extended if we need more information in the future.
|
||||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
|
||||||
|
@ -111,10 +184,28 @@ pub struct DatabaseUser {
|
||||||
#[sqlx(rename = "Host")]
|
#[sqlx(rename = "Host")]
|
||||||
pub host: String,
|
pub host: String,
|
||||||
|
|
||||||
#[sqlx(rename = "`Password` != '' OR `authentication_string` != ''")]
|
#[sqlx(rename = "has_password")]
|
||||||
pub has_password: bool,
|
pub has_password: bool,
|
||||||
|
|
||||||
|
#[sqlx(rename = "is_locked")]
|
||||||
|
pub is_locked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DB_USER_SELECT_STATEMENT: &str = r#"
|
||||||
|
SELECT
|
||||||
|
`mysql`.`user`.`User`,
|
||||||
|
`mysql`.`user`.`Host`,
|
||||||
|
`mysql`.`user`.`Password` != '' OR `mysql`.`user`.`authentication_string` != '' AS `has_password`,
|
||||||
|
COALESCE(
|
||||||
|
JSON_EXTRACT(`mysql`.`global_priv`.`priv`, "$.account_locked"),
|
||||||
|
'false'
|
||||||
|
) != 'false' AS `is_locked`
|
||||||
|
FROM `mysql`.`user`
|
||||||
|
JOIN `mysql`.`global_priv` ON
|
||||||
|
`mysql`.`user`.`User` = `mysql`.`global_priv`.`User`
|
||||||
|
AND `mysql`.`user`.`Host` = `mysql`.`global_priv`.`Host`
|
||||||
|
"#;
|
||||||
|
|
||||||
/// This function fetches all database users that have a prefix matching the
|
/// This function fetches all database users that have a prefix matching the
|
||||||
/// unix username and group names of the given unix user.
|
/// unix username and group names of the given unix user.
|
||||||
pub async fn get_all_database_users_for_unix_user(
|
pub async fn get_all_database_users_for_unix_user(
|
||||||
|
@ -122,14 +213,7 @@ pub async fn get_all_database_users_for_unix_user(
|
||||||
connection: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<Vec<DatabaseUser>> {
|
) -> anyhow::Result<Vec<DatabaseUser>> {
|
||||||
let users = sqlx::query_as::<_, DatabaseUser>(
|
let users = sqlx::query_as::<_, DatabaseUser>(
|
||||||
r#"
|
&(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` REGEXP ?"),
|
||||||
SELECT
|
|
||||||
`User`,
|
|
||||||
`Host`,
|
|
||||||
`Password` != '' OR `authentication_string` != ''
|
|
||||||
FROM `mysql`.`user`
|
|
||||||
WHERE `User` REGEXP ?
|
|
||||||
"#,
|
|
||||||
)
|
)
|
||||||
.bind(create_user_group_matching_regex(unix_user))
|
.bind(create_user_group_matching_regex(unix_user))
|
||||||
.fetch_all(connection)
|
.fetch_all(connection)
|
||||||
|
@ -144,14 +228,7 @@ pub async fn get_database_user_for_user(
|
||||||
connection: &mut MySqlConnection,
|
connection: &mut MySqlConnection,
|
||||||
) -> anyhow::Result<Option<DatabaseUser>> {
|
) -> anyhow::Result<Option<DatabaseUser>> {
|
||||||
let user = sqlx::query_as::<_, DatabaseUser>(
|
let user = sqlx::query_as::<_, DatabaseUser>(
|
||||||
r#"
|
&(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` = ?"),
|
||||||
SELECT
|
|
||||||
`User`,
|
|
||||||
`Host`,
|
|
||||||
`Password` != '' OR `authentication_string` != ''
|
|
||||||
FROM `mysql`.`user`
|
|
||||||
WHERE `User` = ?
|
|
||||||
"#,
|
|
||||||
)
|
)
|
||||||
.bind(username)
|
.bind(username)
|
||||||
.fetch_optional(connection)
|
.fetch_optional(connection)
|
||||||
|
|
Loading…
Reference in New Issue