Add lock-user and unlock-user
			#43
		
		
	@@ -40,6 +40,14 @@ pub enum UserCommand {
 | 
			
		||||
    /// If no username is provided, all users you have access will be shown.
 | 
			
		||||
    #[command()]
 | 
			
		||||
    ShowUser(UserShowArgs),
 | 
			
		||||
 | 
			
		||||
    /// Lock account for one or more users
 | 
			
		||||
    #[command()]
 | 
			
		||||
    LockUser(UserLockArgs),
 | 
			
		||||
 | 
			
		||||
    /// Unlock account for one or more users
 | 
			
		||||
    #[command()]
 | 
			
		||||
    UnlockUser(UserUnlockArgs),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Parser)]
 | 
			
		||||
@@ -75,6 +83,18 @@ pub struct UserShowArgs {
 | 
			
		||||
    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(
 | 
			
		||||
    command: UserCommand,
 | 
			
		||||
    mut connection: MySqlConnection,
 | 
			
		||||
@@ -87,6 +107,8 @@ pub async fn handle_command(
 | 
			
		||||
                    UserCommand::DropUser(args) => drop_users(args, txn).await,
 | 
			
		||||
                    UserCommand::PasswdUser(args) => change_password_for_user(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,
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
@@ -236,6 +258,7 @@ async fn show_users(
 | 
			
		||||
                json!({
 | 
			
		||||
                    "user": user.user,
 | 
			
		||||
                    "has_password": user.has_password,
 | 
			
		||||
                    "is_locked": user.is_locked,
 | 
			
		||||
                    "databases": user_databases.get(&user.user).unwrap_or(&vec![]),
 | 
			
		||||
                })
 | 
			
		||||
            })
 | 
			
		||||
@@ -252,12 +275,14 @@ async fn show_users(
 | 
			
		||||
        table.add_row(row![
 | 
			
		||||
            "User",
 | 
			
		||||
            "Password is set",
 | 
			
		||||
            "Locked",
 | 
			
		||||
            "Databases where user has privileges"
 | 
			
		||||
        ]);
 | 
			
		||||
        for user in users {
 | 
			
		||||
            table.add_row(row![
 | 
			
		||||
                user.user,
 | 
			
		||||
                user.has_password,
 | 
			
		||||
                user.is_locked,
 | 
			
		||||
                user_databases.get(&user.user).unwrap_or(&vec![]).join("\n")
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
@@ -266,3 +291,49 @@ async fn show_users(
 | 
			
		||||
 | 
			
		||||
    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(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 can be extended if we need more information in the future.
 | 
			
		||||
#[derive(Debug, Clone, FromRow, Serialize, Deserialize)]
 | 
			
		||||
@@ -111,10 +184,28 @@ pub struct DatabaseUser {
 | 
			
		||||
    #[sqlx(rename = "Host")]
 | 
			
		||||
    pub host: String,
 | 
			
		||||
 | 
			
		||||
    #[sqlx(rename = "`Password` != '' OR `authentication_string` != ''")]
 | 
			
		||||
    #[sqlx(rename = "has_password")]
 | 
			
		||||
    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
 | 
			
		||||
/// unix username and group names of the given 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,
 | 
			
		||||
) -> anyhow::Result<Vec<DatabaseUser>> {
 | 
			
		||||
    let users = sqlx::query_as::<_, DatabaseUser>(
 | 
			
		||||
        r#"
 | 
			
		||||
          SELECT
 | 
			
		||||
            `User`,
 | 
			
		||||
            `Host`,
 | 
			
		||||
            `Password` != '' OR `authentication_string` != ''
 | 
			
		||||
          FROM `mysql`.`user`
 | 
			
		||||
          WHERE `User` REGEXP ?
 | 
			
		||||
        "#,
 | 
			
		||||
        &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` REGEXP ?"),
 | 
			
		||||
    )
 | 
			
		||||
    .bind(create_user_group_matching_regex(unix_user))
 | 
			
		||||
    .fetch_all(connection)
 | 
			
		||||
@@ -144,14 +228,7 @@ pub async fn get_database_user_for_user(
 | 
			
		||||
    connection: &mut MySqlConnection,
 | 
			
		||||
) -> anyhow::Result<Option<DatabaseUser>> {
 | 
			
		||||
    let user = sqlx::query_as::<_, DatabaseUser>(
 | 
			
		||||
        r#"
 | 
			
		||||
          SELECT
 | 
			
		||||
            `User`,
 | 
			
		||||
            `Host`,
 | 
			
		||||
            `Password` != '' OR `authentication_string` != ''
 | 
			
		||||
          FROM `mysql`.`user`
 | 
			
		||||
          WHERE `User` = ?
 | 
			
		||||
        "#,
 | 
			
		||||
        &(DB_USER_SELECT_STATEMENT.to_string() + "WHERE `mysql`.`user`.`User` = ?"),
 | 
			
		||||
    )
 | 
			
		||||
    .bind(username)
 | 
			
		||||
    .fetch_optional(connection)
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user