My HNG Journey. Stage One: Creating a multi-purpose Bash Script

RMAG news

Introduction

With a new HNG 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

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;

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.

#!/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.

#!/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.


# 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.


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

#!/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 =~ ^[az][az09_]*$ ]]; 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

#!/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


# 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.


# 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.

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.


# 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
Thank you for reading ♥

Here’s the full script for your reference

#!/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 =~ ^[az][az09_]*$ ]]; 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 —————————————-