Automating User Creation and Management with Bash: A Step-by-Step Guide
Published: 2024-07-02 21:45:58

Automating user creation and management can save time, reduce errors, and enhance security for SysOps engineers. In this article, we will go over a Bash script that automates the creation of users and groups, sets up home directories, and manages passwords securely. The script reads from a text file where each line consists of a username and their corresponding groups, logs every action in a log file, and saves the randomly generated passwords for each user in a secure `.csv` file accessible only to the owner.

Let's dive into the bash script and break it down step by step.

## Line-by-Line Explanation

1.) First off, we have our shebang.
```
#!/bin/bash
```
This specifies the type of interpreter script will be run with. Since it is a "bash" script, it should be run with the Bourne Again Shell (Bash) interpreter. Also, some commands in the script may not be interpreted correctly outside of Bash.

2.) The paths for the log file and the password file are set to avoid unnecessary repetition in the script.
```
# Define log and password storage files
LOG_FILE="/var/log/user_management.log"
PASSWORD_FILE="/var/secure/user_passwords.csv"
```

3.) To ensure that the bash script runs with root privileges, an if statement checks if the Effective User ID (EUID) is equal to zero. The EUID determines the permissions the script will use to run, and 0 represents the root user ID in Linux systems. Only users with administrative privileges (users who can use sudo or the root user itself) can run the script. If someone attempts to run it without such privileges, an error message and the script's run process will be terminated.
```
# Check if the script is run with root privileges
if [[ $EUID -ne 0 ]]; then
  echo "This script must be run with root privileges." >&2
  exit 1
fi
```

4.) To ensure that an input file is provided as an argument when running the script, this `if` statement will terminate the script if no argument is provided. In this statement, `$#` represents the argument provided when running the script. If it is equal to zero (no argument is provided) or if it is greater than or equal to 2, an error message will be printed and the script's execution is halted.
```
# Check if the input file is provided
if [[ $# -eq 0 || $# -ge 2 ]]; then
  echo "Usage: $0 <user_file>" >&2
  exit 1
fi
```

5.) Next is the `log_action` function that records logs using bold lettering (formatted with ANSI escape codes: `\033[1m` and `\033[0m`) and a timestamp (using the `date` command to get the current date and the specified date format: `'%Y-%m-%d %H:%M:%S'`). This function is used to log important steps, success messages, and error messages in the script.
```
# Log function
log_action() {
  echo "--------------------------------------------------" | tee -a "$LOG_FILE"
  echo -e "$(date +'%Y-%m-%d %H:%M:%S') - \033[1m$1\033[0m" | tee -a "$LOG_FILE"
  echo "--------------------------------------------------" | tee -a "$LOG_FILE"
}
```

6.) Next is the `create_user_account` function that manages the entire process of creating a user, setting up their home directories with appropriate permissions and ownership, adding them to specified groups, and assigning randomly generated passwords. Every important step is logged.

**`create_user_account` function**

```
create_user_account() {
  local username="$1"
  local groups="$2"

  log_action "Creating user account '$username'..."
  
  # Check if user already exists
  if id "$username" &> /dev/null; then
    echo "User '$username' already exists. Skipping..." | tee -a "$LOG_FILE"
    return 1
  fi

  # Create user with home directory and set shell
  if useradd -m -s /bin/bash "$username"; then
    echo "User $username created successfully." | tee -a "$LOG_FILE"
  else
    echo "Error creating user $username." | tee -a "$LOG_FILE"
    return 1
  fi

  # Create user group if it does not exist (in case the script is run in other linux distributions that do not create user groups by default)
  if ! getent group "$username" >/dev/null; then
    groupadd "$username"
    usermod -g "$username" "$username"
    log_action "Group $username created."
  fi

  # Set up home directory permissions
  echo "Setting permissions for /home/$username..." | tee -a "$LOG_FILE"
  chmod 700 "/home/$username" && chown "$username:$username" "/home/$username"
  if [[ $? -eq 0 ]]; then
    echo "Permissions set for /home/$username." | tee -a "$LOG_FILE"
  else
    echo "Error setting permissions for /home/$username." | tee -a "$LOG_FILE"
    return 1
  fi

  # Add user to additional groups (comma separated)
  echo "Adding user $username to specified additional groups..." | tee -a "$LOG_FILE"
  IFS=',' read -ra group_array <<< "$groups"
  for group in "${group_array[@]}"; do
    group=$(echo "$group" | xargs)
    
    # Check if group exists, if not create it
    if ! getent group "$group" &>/dev/null; then
      if groupadd "$group"; then
        echo "Group $group did not exist. Now created." | tee -a "$LOG_FILE"
      else
        echo "Error creating group $group." | tee -a "$LOG_FILE"
        continue
      fi
    fi

    # Add user to group
    if gpasswd -a "$username" "$group"; then
      echo "User $username added to group $group." | tee -a "$LOG_FILE"
    else
      echo "Error adding user $username to group $group." | tee -a "$LOG_FILE"
    fi
  done

  # Log if no additional groups are specified
  if [[ -z "$groups" ]]; then
    echo "No additional groups specified." | tee -a "$LOG_FILE"
  fi

  # Generate random password, set it for the user, and store it in a file
  echo "Setting password for user $username..." | tee -a "$LOG_FILE"
  password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)
  echo "$username:$password" | chpasswd
  if [[ $? -eq 0 ]]; then
    echo "Password set for user $username." | tee -a "$LOG_FILE"
    echo "$username,$password" >> "$PASSWORD_FILE"
  else
    echo "Error setting password for user $username. Deleting $username user account" | tee -a "$LOG_FILE"
    userdel -r "$username"
    return 1
  fi
}
```
- In the function, I initially set local variables to hold the values of the specified username and groups, to avoid repetition.
```
local username="$1"
local groups="$2"
```
- In this next section, I use the `log_action` function to record the start of each user account creation. Additionally, I verify whether the user already exists. If the user does exist, an error message is displayed and the script's execution is stopped.
```
log_action "Creating user account '$username'..."
  
  # Check if user already exists
  if id "$username" &> /dev/null; then
    echo "User '$username' already exists. Skipping..." | tee -a "$LOG_FILE"
    return 1
  fi
```
- Next, there is an `if` statement that uses the `useradd` command with the `-m` and `-s` flags to create a user with a login shell (In this case, the login shell is set to be `/bin/bash`. If you want, you can modify or remove the `-s /bin/bash` part entirely) and assigns a home directory to the user. It stops the script's run process if an error occurs during the execution of the command.
```
# Create user with home directory and set shell
  if useradd -m -s /bin/bash "$username"; then
    echo "User $username created successfully." | tee -a "$LOG_FILE"
  else
    echo "Error creating user $username." | tee -a "$LOG_FILE"
    return 1
  fi
```
- Next, in the case that the script is run on other Linux distributions that do not create and assign a primary group with the same name as the newly created user, the `if` statement here will create a group with the same name as the user and add it as the user's primary group.
```
# Create user group if it does not exist (in case the script is run in other linux distributions that do not create user groups by default)
  if ! getent group "$username" >/dev/null; then
    groupadd "$username"
    usermod -g "$username" "$username"
    log_action "Group $username created."
  fi
```
- In this section of the function, the newly created user is designated as the owner of the newly created home directory. The owner is also granted all possible permissions for the directory. This is all done using the `chmod` and `chown` commands. If this process is unsuccessful, an error message is printed and the execution is halted.
```
# Set up home directory permissions
  echo "Setting permissions for /home/$username..." | tee -a "$LOG_FILE"
  chmod 700 "/home/$username" && chown "$username:$username" "/home/$username"
  if [[ $? -eq 0 ]]; then
    echo "Permissions set for /home/$username." | tee -a "$LOG_FILE"
  else
    echo "Error setting permissions for /home/$username." | tee -a "$LOG_FILE"
    return 1
  fi
```
- In this section, the newly created user is added to additional groups specified in the input file. By setting the Internal Field Separator (IFS) to expect comma-separated values and using the `read` command with the `-ra` flags, the groups are individually placed inside an array called `group_array` to be used in the subsequent `for` loop. Within the loop, for every value in the group_array, the `xargs` command removes any whitespace, creates the group if it does not exist, and finally adds the user to the group using the `gpasswd` command with the `-a` flag. In the case where no group is specified for the user in the input file, a message will be printed.
```
# Add user to additional groups (comma separated)
  echo "Adding user $username to specified additional groups..." | tee -a "$LOG_FILE"
  IFS=',' read -ra group_array <<< "$groups"
  for group in "${group_array[@]}"; do
    group=$(echo "$group" | xargs)
    
    # Check if group exists, if not create it
    if ! getent group "$group" &>/dev/null; then
      if groupadd "$group"; then
        echo "Group $group did not exist. Now created." | tee -a "$LOG_FILE"
      else
        echo "Error creating group $group." | tee -a "$LOG_FILE"
        continue
      fi
    fi

    # Add user to group
    if gpasswd -a "$username" "$group"; then
      echo "User $username added to group $group." | tee -a "$LOG_FILE"
    else
      echo "Error adding user $username to group $group." | tee -a "$LOG_FILE"
    fi
  done

  # Log if no additional groups are specified
  if [[ -z "$groups" ]]; then
    echo "No additional groups specified." | tee -a "$LOG_FILE"
  fi
```
- For the final section of the function, a random 12-character password is generated and set for the user. The head command collects a stream of random bytes from the `/dev/urandom` file. This stream is piped to the `tr` command, which filters the bytes to include only alphanumeric characters (A-Z, a-z, 0-9) using the `-dc` flag. The filtered result is then piped to another head command, which selects only the first 12 characters from the edited stream. The password is then set by piping the user's name and the randomly generated password to the `chpasswd` command. The user and the generated password are saved in the designated password `.csv` file. If setting the password fails, the script deletes the user account and logs the error, to avoid any possible security risk.
```
# Generate random password, set it for the user, and store it in a file
  echo "Setting password for user $username..." | tee -a "$LOG_FILE"
  password=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 12)
  echo "$username:$password" | chpasswd
  if [[ $? -eq 0 ]]; then
    echo "Password set for user $username." | tee -a "$LOG_FILE"
    echo "$username,$password" >> "$PASSWORD_FILE"
  else
    echo "Error setting password for user $username. Deleting $username user account" | tee -a "$LOG_FILE"
    userdel -r "$username"
    return 1
  fi
```

7.) After creating the create_user_account function, the script processes a file containing user information and creates user accounts accordingly.
```
# Process the user file
user_file="$1"
while IFS=';' read -r username groups; do
  if create_user_account "$username" "${groups%%[ ;]}"; then
    log_action "User account '$username' created successfully."
  else
    log_action "Error creating user account '$username'."
  fi
done < "$user_file"
```
- The script takes the user file as its argument and assigns it to the variable `user_file`.
- A while loop reads each line of the user file. The `IFS=';'` part sets the Internal Field Separator to a semicolon (;), splitting each line at the semicolon. The `read -r` username groups part reads the split parts into the username and groups variables.
- For each line in the file, the script calls the create_user_account function with the username and the groups (with trailing spaces removed using `${groups%%[ ;]}`). The script also logs a message if the result of the `create_user_account` was a success or failure.

8.) After all that is done, the script gives only the owner (root) and those with root privileges access to the password file, logs the completion of the script's execution and prints the log file and password file location.
```
# Keep password file accessible only to those with root privileges
chmod 600 "$PASSWORD_FILE"

# Log completion
log_action "User creation script completed."

# Print log file and password file location
echo "Check $LOG_FILE for details."
echo "Check $PASSWORD_FILE for user passwords."
```

**Prerequisites for running the script**

- A Linux system
- A bash terminal (Optional. You can use any available shell terminal on the Linux system).
- Root privileges on your system.
- The text file containing the usernames and groups, must be formatted as `username;group1,group2`.

**To run the script:**
1.) Copy the file from or clone the repository [HNG_Stage1](https://github.com/VictortheGreat7/HNG_Stage1)

2.) Copy the text file containing the usernames and groups to the folder where the script is located.

3.) Then, in the directory where both the script and the text file are now located, run `sudo ./create_users.sh <text file>`

## Conclusion

This script simplifies some administrative tasks. With such a script, SysOps engineers can automate user and group creation and management, allowing them to focus on more critical work while ensuring efficient and secure user management.

This is one of the project assignments in the [HNG Internship](https://hng.tech/internship) program designed to enhance your resume and deepen your knowledge of bash scripting. For the best experience, visit [HNG Premium](https://hng.tech/premium).