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.
 | 
					    /// 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,
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            })
 | 
					            })
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
@@ -236,6 +258,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 +275,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 +291,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)
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user