How To Create Users and Groups from a File Using Bash Script
Tags: ubuntu, bash, linux, devops
Published: 2024-07-04 00:29:17

This article explains how to create users and assign them groups and passwords from a given text file.

## Contents
- [Objectives](#objectives)
- [Project Setup](#project-setup)
- [Bash Script](#bash-script)
- [Test](#test)

## Objectives

By the end of this tutorial, you will be able to:
1. Create users with a bash script
2. Create groups for these users
3. Assign users to groups
4. Generate and set passwords for users
5. Log actions and manage permissions for security


## Project Setup

1. Create a `.sh` file named `create_users.sh`.
```bash
touch create_users.sh
```

2. Create a text file named `sample.txt` with user and group data.
```bash
touch sample.txt
```

## Bash Script

To get started, we first need to declare that this is a bash script using:
```bash
#!/bin/bash
```

This line tells the system to use the bash shell to interpret the script.

Next, we define our log file path and the password file path. This ensures we have paths to store logs and passwords securely:

```bash
# Define the log file and password file paths
LOGFILE="/var/log/user_management.log"
PASSWORD_FILE="/var/secure/user_passwords.csv"
```

The next step ensures the script is run with root permissions. If you don't have the right permissions, the script will exit and inform you:

```bash
# Ensure the script is run with root permissions
if [[ $EUID -ne 0 ]]; then
  echo "This script must be run as root or with sudo"
  exit 1
fi
```

Here, `[[ $EUID -ne 0 ]]` checks if the effective user ID is not zero (i.e., not root). The _effective user ID (EUID)_ represents the user identity the operating system uses to decide if you have permission to perform certain actions. If the `EUID` is not zero, it means you're not running the script as the root user, and the script will print a message and exit with status 1.

We then define a function to log actions:

```bash
# Function to log actions
log_action() {
  local message="$1"
  echo "$message" | tee -a "$LOGFILE"
}
```

This function takes a message as an argument and appends it to the log file, also displaying it on the terminal.

Let us create another function that will help us check if a group already exists.

```bash
does_group_exists() {
    local group_name=$1
    if getent group "$group_name" > /dev/null 2>&1; then
        return 0
    else
        return 1
    fi
}
```
If the function returns a 0 then it means that the group exists. If it returns 1, then it means that it does not exist.

Next, we check if the log file exists, create it if it doesn't, and set the correct permissions:

```bash
# Check if the log file exists, create it if it doesn't, and set the correct permissions
if [[ ! -f "$LOGFILE" ]]; then
  touch "$LOGFILE"
  log_action "Created log file: $LOGFILE"
else
  log_action "Log file already exists, skipping creation of logfile ' $LOGFILE ' "
fi
```

Here, `[[ ! -f "$LOGFILE" ]]` checks if the log file does not exist. If true, it creates the file and logs the action.

We repeat a similar process for the password file:

```bash
# Check if the password file exists, create it if it doesn't, and set the correct permissions
if [[ ! -f "$PASSWORD_FILE" ]]; then
  mkdir -p SECURE_DIR
  touch "$PASSWORD_FILE"
  log_action "Created password file: $PASSWORD_FILE"
  chmod 600 "$PASSWORD_FILE"
  log_action "Password file permissions set to 600: $PASSWORD_FILE"
else
  log_action "Password file already exists, skipping creation of password file: $PASSWORD_FILE"
fi
```

This ensures the password file is created if it doesn't exist and sets the correct permissions for security.

Next, we define a function to generate random passwords:

```bash
# Function to generate random passwords
generate_password() {
  local password_length=12
  tr -dc A-Za-z0-9 </dev/urandom | head -c $password_length
}
```
The `local` keyword is used to define a variable that is local to the function. This means that the variable `password_length` is only accessible with the `generate_passsword` function, preventing it from interfering with other parts of the script.

Let's break down what `tr -dc A-Za-z0-9 </dev/urandom | head -c $password_length` does, shall we?

- `/dev/urandom`: This is a special file in Unix-like systems that provides random data. It's often used for generating cryptographic keys, passwords, or any other data requiring randomness.

- `tr -dc A-Za-z0-9`: The `tr` command is used to translate or delete characters. Here:

    - `-d` tells `tr` to delete characters.
    - `-c` complements the set of characters, meaning it includes everything except the specified set.
    - `A-Za-z0-9` specifies the set of allowed characters: uppercase letters (A-Z), lowercase letters (a-z), and digits (0-9).
    - `</dev/urandom`: Redirects the random data from `/dev/urandom` into the `tr` command.

- `| head -c $password_length`: The head command is used to output the first part of files.

    - `-c $password_length` specifies the number of characters to output, defined by the variable password_length (which is 12). This ensures the generated password is exactly 12 characters long.
This function uses `tr` to generate a random string of 12 characters from `/dev/urandom`.

We then define the `create_user_groups_from_file` function, which creates a user and assigns groups:
```bash
create_user_groups_from_file() {
  local filename="$1"

  # Check if the file exists
  if [[ ! -f "$filename" ]]; then
    log_action "File not found: $filename"
    return 1
  fi

  # Read the file line by line
  while IFS= read -r line; do
    # Remove whitespace and extract user and groups
    username=$(echo "$username" | xargs)
    groups=$(echo "$groups" | tr -d ' ')

```

- `[[ ! -f "$filename" ]]`: checks if the file exists. `-f`: This is a file test operator. It checks if the provided path (in this case, stored in the variable `$filename`) exists and is a regular file.
- `while IFS= read -r line; do ... done < "$filename"`: reads the file line by line.
- `username=$(echo "$username" | xargs)`: extracts the username from the file. `xargs` trims any leading or trailing whitespace and ensures that the result is treated as a single entity.
- `groups=$(echo "$groups" | tr -d ' ')`: extracts the groups from the file.

Now, let us add the logic to actually create the users and the groups.

```bash
create_user_groups_from_file() {
  # ... previous code here

  # Check if the user already exists
  if ! id "$username" &>/dev/null; then
    # Create the user with a home directory
    useradd -m -s /bin/bash "$username"
    if [[ $? -ne 0 ]]; then
        echo "ERROR: Failed to create user $username." >> "$LOG_FILE"
        continue
    fi
        log_action "User $username created."


 # Generate a password and set it for the user
    password=$(generate_password)
    # and set password
    echo "$username:$password" | chpasswd
    echo "$username,$password" >> "$PASSWORD_FILE"
    log_action "Created user: $username"
  else
    log_action "User $username already exists, skipping creation"
  fi

  # Create a personal group for the user if it doesn't exist
  if ! does_group_exists "$username"; then
        groupadd "$username"
        log_action "Successfully created group: $username"
        usermod -aG "$username" "$username"
        log_action "User: $username added to Group: $username"
    else
        log_action "User: $username added to Group: $username"
    fi

    # Add the user to additional groups
  IFS=',' read -r -a group_lst <<< "$groups"
  for group in "${group_lst[@]}"; do
    if ! does_group_exists "$group"; then
            # Create the group if it does not exist
            groupadd "$group"
            log_action "Successfully created Group: $group"
        else
            log_action "Group: $group already exists"
        fi
        # Add the user to the group
        usermod -aG "$group" "$username"
   done

  # Set up home directory permissions
  chown -R "$username:$username" "/home/$username"
  chmod 700 "/home/$username"
  done < "$filename"
}
```

- `if id "$username" &>/dev/null; then ...`: This checks if the user exists then redirects both the standard output (stdout) and standard error (stderr) to `/dev/null`. `/dev/null` is a special device file that discards any data written to it. 
- `useradd -m "$username"`: creates the user with a home directory. `-m` flag tells `useradd` to create a home directory for the user.
- `groupadd "$username"`: creates a personal group for the user.
- `usermod -aG "$username" "$username"`: adds the user to their personal group.`-a` flag stands for "append". It tells `usermod` to append the user to the specified group(s) without removing them from any other groups. On the other hand, `-G` flag specifies that the following argument (`"$username"`) is a list of supplementary groups which the user is also a member of.
- `IFS=',' read -ra group_lst <<< "$groups"` splits additional groups by commas. Here the _Internal Field Separator_ (`IFS`) means the shell will split strings into parts based on commas.
- `groupadd "$group"`: creates additional groups if they don't exist.
- `chown -R "$username":"$username" `"/home/$username" sets the ownership of the home directory. `chown` stands for change owner and the command recursively changes ownership for all files and subdirectories within `/home/$username`.
- `chmod 700 "/home/$username"` sets permissions of the home directory. `chmod` stands for change mode and it changes file permission to 700. 7 grants read, write and execute permissions to the owner. 0 denies all permissions to others.


## Test
To test the code simply run the following command in your terminal:
```bash
bash create_users.sh sample.txt
```


That's it! You can find the full code in my [repository](https://github.com/Toluwalemi/hng-devops-stage-one-task).

Cheers!

_[This article was written in completion of the [HNG Internship]( https://hng.tech/internship) stage one DevOps task. Learn more about the program [here](https://hng.tech/premium).]_