change-user-run 0.1.2

Run commands as other users and create users
Documentation
//! User creation.

use std::{path::Path, process::Command};

use log::debug;
use rand::{RngExt, distr::Alphanumeric, rng};
use yescrypt::{PasswordHasher, Yescrypt};

use crate::{Error, get_command};

/// The default shell used for new users (`/usr/bin/bash`).
pub const DEFAULT_SHELL: &str = "/usr/bin/bash";

/// Creates a set of users using [useradd] and sets random passphrases for them using [usermod].
///
/// Optionally, the base directory for the home directory of all users and the shell can be
/// provided. By default [`DEFAULT_SHELL`] is used as the user's shell.
///
/// # Note
///
/// User creation is a privileged action which requires calling this function as root.
///
/// # Errors
///
/// Returns an error if
///
/// - the [useradd] or [usermod] commands cannot be found,
/// - the [useradd] command cannot be executed,
/// - a user and/or its home cannot be created,
/// - the [usermod] command cannot be executed,
/// - or setting a passphrase for a created user fails.
///
/// # Examples
///
/// ```no_run
/// use std::path::Path;
///
/// use change_user_run::create_users;
///
/// # fn main() -> testresult::TestResult {
/// // Create a single user in the default location with the default shell.
/// create_users(&["testuser"], None, None)?;
///
/// // Create a set of users, with home directories under a custom directory, using a custom shell.
/// create_users(
///     &["testuser"],
///     Some(&Path::new("/var/lib/custom")),
///     Some(&Path::new("/usr/bin/fish")),
/// )?;
/// # Ok(())
/// # }
/// ```
///
/// [useradd]: https://man.archlinux.org/man/useradd.8
/// [usermod]: https://man.archlinux.org/man/usermod.8
pub fn create_users(
    users: &[&str],
    home_base_dir: Option<&Path>,
    shell: Option<&Path>,
) -> Result<(), Error> {
    let useradd_command = get_command("useradd")?;
    let usermod_command = get_command("usermod")?;

    debug!("Creating users: {}", users.join(", "));

    for user in users {
        debug!("Creating user: {user}");

        // Create the user and its home.
        let mut command = Command::new(&useradd_command);
        let command = command
            .arg("--create-home")
            .arg("--user-group")
            .arg("--shell")
            .arg(shell.unwrap_or(Path::new(DEFAULT_SHELL)));
        let command = if let Some(path) = home_base_dir.as_ref() {
            command.arg("--base-dir").arg(path)
        } else {
            command
        };
        let command = command.arg(user);

        let command_output = command.output().map_err(|source| Error::CommandExec {
            command: format!("{command:?}"),
            source,
        })?;
        if !command_output.status.success() {
            return Err(crate::Error::CommandNonZero {
                command: format!("{command:?}"),
                exit_status: command_output.status,
                stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
            });
        }

        // Set random 30 char password for the user.
        let random_passphrase: String = rng()
            .sample_iter(&Alphanumeric)
            .take(30)
            .map(char::from)
            .collect();
        let yescrypt = Yescrypt::default();
        let passphrase_hash = yescrypt
            .hash_password(random_passphrase.as_bytes())
            .map_err(|source| Error::PasswordHash {
                context: format!("creating a passphrase hash for user {user}"),
                source,
            })?;
        let mut command = Command::new(&usermod_command);
        command.arg("--password");
        command.arg(passphrase_hash.as_str());
        command.arg(user);
        let command_output = command.output().map_err(|source| Error::CommandExec {
            command: format!("{command:?}"),
            source,
        })?;
        if !command_output.status.success() {
            return Err(Error::CommandNonZero {
                command: format!("{command:?}"),
                exit_status: command_output.status,
                stderr: String::from_utf8_lossy(&command_output.stderr).into_owned(),
            });
        }
    }

    Ok(())
}