My HNG Journey. Stage One: Automating Linux User and Group Management using a Bash Script
Tags: bash, backend, github, linux
Published: 2024-07-02 23:07:00

## Introduction
With a new [HNG ](https://hng.tech/internship)stage, comes a new and slightly difficult task.
This is going to be a long read, so I'll keep the introduction short and just get into it

The source code can be found on my [GitHub repo](https://github.com/Ravencodess/shell-script-stage-1.git)

### Requirements
In this task, we will be writing a Bash script that takes in one argument which will be a TXT file that contains a list of usernames and group names. For example;
```javascript
john; admin, developer, tester
kourtney; hr, product
```
The script must accomplish the following tasks;
- Create Users and groups based on the file content, Usernames and user groups are separated by semicolon ";"- Ignore whitespace.

- Each User must have a personal group with the same group name as the username, this group name will not be written in the text file.

- A user can have multiple groups, each group delimited by comma ","

- The file `/var/log/user_management.log` should be created and contain a log of all actions performed by your script.

- The file `/var/secure/user_passwords.txt` should be created and contain a list of all users and their passwords delimited by comma, and only the file owner should be able to read it.

- Handle errors gracefully.

### Prerequisites

- Basic understanding of Linux CLI and Bash Scripting

#### Step 1
**Handle Command Line Arguments and Input File Errors**
We want to ensure the script is being passed only one argument and that argument is indeed a file.
If both cases are not true, we want to print an error to the terminal.
```javascript
#!/bin/bash
# Check if the correct number of command line arguments is provided
if [ "$#" -ne 1 ]; then
    echo "Usage: $0 <user_info_file>"
    exit 1
fi

# Assign the file name from the command line argument
input_file=$1

# Check if the input file exists
if [ ! -f "$input_file" ]; then
    echo "Error: File $input_file not found."
    exit 1
fi
```
#### Step 2
**Create a Logging Function**
One of our requirements states that we log all our actions to `/var/log/user_management.log`, let's create a function to handle that, and move it to the top of our script.
``` javascript
#!/bin/bash
# Function to log actions to /var/log/user_management.log
log_action() {
    local log_file="/var/log/user_management.log"
    local timestamp=$(date +"%Y-%m-%d %T")
    local action="$1"
    echo "[$timestamp] $action" | sudo tee -a "$log_file" > /dev/null
}
---
```

#### Step 3
**Create Log and Password File**
Now that we have our logging function defined we can log any action that happens during the script execution.
Next we need to create the log file itself and also create the password file, and also assign permissions to allow only the file owner to access it.
```javascript
---
# Check and create the log_file if it does not exist
log_file="/var/log/user_management.log"

if [ ! -f "$log_file" ]; then
    # Create the log file
    sudo touch "$log_file"
    log_action "$log_file has been created."
else
    log_action "Skipping creation of: $log_file (Already exists)"
fi

# Check and create the passwords_file if it does not exist
passwords_file="/var/secure/user_passwords.txt"

if [ ! -f "$passwords_file" ]; then
    # Create the file and set permissions
    sudo mkdir -p /var/secure/
    sudo touch "$passwords_file"
    log_action "$passwords_file has been created."
    # Set ownership permissions for passwords_file
    sudo chmod 600 "$passwords_file"
    log_action "Updated passwords_file permission to file owner"
else
    log_action "Skipping creation of: $passwords_file (Already exists)"
fi
```

#### Step 4
**Read Input File**
At this point the script has validated the command line argument and has confirmed it is a file, now it's time to loop through the file and read it line by line. We can accomplish this using a `while loop`. This will run until the last line of the input file. All our user and groups creation logic will be done inside this `while loop`.
```javascript
---
while IFS=';' read -r username groups; do
---
done < "$input_file"
```
This `while loop` reads each line of the file, uses an internal field separator (IFS) to separate the line based on the value assigned to the `IFS` variable and then assigns values to variables: username and groups. For example, a line like `dora; design, marketing` would be read as; `username=dora groups=design, marketing`
The -r option ensures we don't treat backslashes '\' as escape characters

##### Step 5
**Validate Username and Group Name**
Our script can now read a user input file line by line, but first we must validate the strings that are passed into our $username and $group variables to ensure they comply with Unix naming standards. We can handle this logic by creating a `validate_name` function and move it to the top of our script
```javascript
#!/bin/bash

# Function to validate username and group name
validate_name() {
    local name=$1
    local name_type=$2  # "username" or "groupname"

    # Check if the name contains only allowed characters and starts with a letter
    if [[ ! "$name" =~ ^[a-z][a-z0-9_-]*$ ]]; then
        log_action "Error: $name_type '$name' is invalid. It must start with a lowercase letter and contain only lowercase letters, digits, hyphens, and underscores."
        return 1
    fi

    # Check if the name is no longer than 32 characters
    if [ ${#name} -gt 32 ]; then
        log_action "Error: $name_type '$name' is too long. It must be 32 characters or less."
        return 1
    fi

    return 0
}
---
```
This function runs two checks;

- It checks if the name complies with naming standards using a Regex expression (name begins with a lowercase letter and name must only include lowercase letters, numbers, dashes and underscores)
- It makes sure the name is not longer than 32 characters.
Finally it logs all action into the log file created earlier

#### Step 6
**Check if User or Group Already Exists**
After validating the string passed into our variables, we also need to run a check to validate if these names already exist on the system. We don't want to create duplicate users or groups. We can achieve this by creating a `user_exists` and `group_exists` function and move it to the top of our script
```javascript
#!/bin/bash
# Function to check if a user exists
user_exists() {
    local username=$1
    if getent passwd "$username" > /dev/null 2>&1; then
        return 0  # User exists
    else
        return 1  # User does not exist
    fi
}

# Function to check if a group exists
group_exists() {
    local group_name=$1
    if getent group "$group_name" > /dev/null 2>&1; then
        return 0  # Group exists
    else
        return 1  # Group does not exist
    fi
}
---
```

#### Step 7
**Create User**
Now it's time to use our while loop to begin creating users,
we will carry out these tasks in this step
- String manipulation, which involves removing or collapsing white spaces
- Call the validate_name and user_exists functions to ensure we are creating a valid and unique username
- Generate a random password and assign it to the newly created user

Let's first define the `generate_password` function and place it alongside the functions we created earlier at the top of our script
```javascript
---
# Function to generate a random password
generate_password() {
    openssl rand -base64 12
}
---
```

Now everything is in place to create a user, we will utilize the `while loop` we created in **Step 4**.
```javascript
---
# Read the file line by line and process
while IFS=';' read -r username groups; do
    # Extract the user name
    username=$(echo "$username" | xargs)

    # Validate username
    if ! validate_name "$username" "username"; then
        log_action "Invalid username: $username. Skipping."
        continue
    fi

    # Check if the user already exists
    if user_exists "$username"; then
        log_action "Skipped creation of user: $username (Already exists)"
        continue
    else
        # Generate a random password for the user
        password=$(generate_password)

        # Create the user with home directory and set password
        sudo useradd -m -s /bin/bash "$username"
        echo "$username:$password" | sudo chpasswd

        log_action "Successfully Created User: $username"
    fi

    # Ensure the user has a group with their own name, This is the default behaviour in most linux distros
    if ! group_exists "$username"; then
        sudo groupadd "$username"
        log_action "Successfully created group: $username"
        sudo usermod -aG "$username" "$username"
        log_action "User: $username added to Group: $username"
    else
        log_action "User: $username added to Group: $username"
    fi
done < "$input_file"
```
#### Step 8
**Create Group(s)**
The next action to take is to create the groups for the user that was just created.
We need to also validate the group name and check if it already exists before creating it and adding our user into it. We need to form a `group_array` based on the content of the `groups` variable so we can loop through it and create a group for each name in the array.
Under the user creation logic, we can create groups with this.
```javascript
while IFS=';' read -r username groups; do
---
# Extract the groups and remove any spaces
    groups=$(echo "$groups" | tr -d ' ')

    # Split the groups by comma
    IFS=',' read -r -a group_array <<< "$groups"

    # Create the groups and add the user to each group
    for group in "${group_array[@]}"; do
        # Validate group name
        if ! validate_name "$group" "groupname"; then
            log_action "Invalid Group name: $group. Skipping Group for user $username."
            continue
        fi

        # Check if the group already exists
        if ! group_exists "$group"; then
            # Create the group if it does not exist
            sudo groupadd "$group"
            log_action "Successfully created Group: $group"
        else
            log_action "Group: $group already exists"
        fi
        # Add the user to the group
        sudo usermod -aG "$group" "$username"
    done
done < "$input_file"
```

### Step 9
**Store Password Information in Secure Password File**
Let's round up the script execution by setting proper home directory permissions and also sending username and password information to the `passwords_file` we created in **Step 3**.
```javascript
---
# Set permissions for home directory
    sudo chmod 700 "/home/$username"
    sudo chown "$username:$username" "/home/$username"
    log_action "Updated permissions for home directory: '/home/$username' of User: $username to '$username:$username'"

    # Log the user created action
    log_action "Successfully Created user: $username with Groups: $username ${group_array[*]}"

    # Store username and password in secure file
    echo "$username,$password" | sudo tee -a "$passwords_file" > /dev/null
    log_action "Stored username and password in $passwords_file"
done < "$input_file"
```

#### Step 10
**Putting it All Together**
We've come to the end of the script, I did mention it was a long one 😁. But I enjoyed explaining every paragraph to you 🤗. If you want to discover amazing talents at HNG click [here](https://hng.tech/hire)
Thank you for reading ♥

Here's the full script for your reference

```javascript
#!/bin/bash

# Function to check if a user exists
user_exists() {
    local username=$1
    if getent passwd "$username" > /dev/null 2>&1; then
        return 0  # User exists
    else
        return 1  # User does not exist
    fi
}

# Function to check if a group exists
group_exists() {
    local group_name=$1
    if getent group "$group_name" > /dev/null 2>&1; then
        return 0  # Group exists
    else
        return 1  # Group does not exist
    fi
}

# Function to validate username and group name
validate_name() {
    local name=$1
    local name_type=$2  # "username" or "groupname"

    # Check if the name contains only allowed characters and starts with a letter
    if [[ ! "$name" =~ ^[a-z][a-z0-9_-]*$ ]]; then
        log_action "Error: $name_type '$name' is invalid. It must start with a lowercase letter and contain only lowercase letters, digits, hyphens, and underscores."
        return 1
    fi

    # Check if the name is no longer than 32 characters
    if [ ${#name} -gt 32 ]; then
        log_action "Error: $name_type '$name' is too long. It must be 32 characters or less."
        return 1
    fi

    return 0
}

# Function to generate a random password
generate_password() {
    openssl rand -base64 12
}

# Function to log actions to /var/log/user_management.log
log_action() {
    local log_file="/var/log/user_management.log"
    local timestamp=$(date +"%Y-%m-%d %T")
    local action="$1"
    echo "[$timestamp] $action" | sudo tee -a "$log_file" > /dev/null
}

# Check if the correct number of command line arguments is provided
if [ "$#" -ne 1 ]; then
    echo "Usage: $0 <user_info_file>"
    exit 1
fi

# Assign the file name from the command line argument
input_file=$1

# Check if the input file exists
if [ ! -f "$input_file" ]; then
    echo "Error: File $input_file not found."
    exit 1
fi

# Check and create the log_file if it does not exist
log_file="/var/log/user_management.log"

if [ ! -f "$log_file" ]; then
    # Create the log file
    sudo touch "$log_file"
    log_action "$log_file has been created."
else
    log_action "Skipping creation of: $log_file (Already exists)"
fi

# Check and create the passwords_file if it does not exist
passwords_file="/var/secure/user_passwords.txt"

if [ ! -f "$passwords_file" ]; then
    # Create the file and set permissions
    sudo mkdir -p /var/secure/
    sudo touch "$passwords_file"
    log_action "$passwords_file has been created."
    # Set ownership permissions for passwords_file
    sudo chmod 600 "$passwords_file"
    log_action "Updated passwords_file permission to file owner"
else
    log_action "Skipping creation of: $passwords_file (Already exists)"
fi

echo "----------------------------------------"
echo "Generating Users and Groups"
echo "----------------------------------------"

# Read the file line by line and process
while IFS=';' read -r username groups; do
    # Extract the user name
    username=$(echo "$username" | xargs)

    # Validate username
    if ! validate_name "$username" "username"; then
        log_action "Invalid username: $username. Skipping."
        continue
    fi

    # Check if the user already exists
    if user_exists "$username"; then
        log_action "Skipped creation of user: $username (Already exists)"
        continue
    else
        # Generate a random password for the user
        password=$(generate_password)

        # Create the user with home directory and set password
        sudo useradd -m -s /bin/bash "$username"
        echo "$username:$password" | sudo chpasswd

        log_action "Successfully Created User: $username"
    fi

    # Ensure the user has a group with their own name, This is the default behaviour in most linux distros
    if ! group_exists "$username"; then
        sudo groupadd "$username"
        log_action "Successfully created group: $username"
        sudo usermod -aG "$username" "$username"
        log_action "User: $username added to Group: $username"
    else
        log_action "User: $username added to Group: $username"
    fi

    # Extract the groups and remove any spaces
    groups=$(echo "$groups" | tr -d ' ')

    # Split the groups by comma
    IFS=',' read -r -a group_array <<< "$groups"

    # Create the groups and add the user to each group
    for group in "${group_array[@]}"; do
        # Validate group name
        if ! validate_name "$group" "groupname"; then
            log_action "Invalid Group name: $group. Skipping Group for user $username."
            continue
        fi

        # Check if the group already exists
        if ! group_exists "$group"; then
            # Create the group if it does not exist
            sudo groupadd "$group"
            log_action "Successfully created Group: $group"
        else
            log_action "Group: $group already exists"
        fi
        # Add the user to the group
        sudo usermod -aG "$group" "$username"
    done

    # Set permissions for home directory
    sudo chmod 700 "/home/$username"
    sudo chown "$username:$username" "/home/$username"
    log_action "Updated permissions for home directory: '/home/$username' of User: $username to '$username:$username'"

    # Log the user created action
    log_action "Successfully Created user: $username with Groups: $username ${group_array[*]}"

    # Store username and password in secure file
    echo "$username,$password" | sudo tee -a "$passwords_file" > /dev/null
    log_action "Stored username and password in $passwords_file"
done < "$input_file"

# Log the script execution to standard output
echo "----------------------------------------"
echo "Script Executed Succesfully, logs have been published here: $log_file"
echo "----------------------------------------"
```