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" =~ ^[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
#!/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" =~ ^[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 "----------------------------------------"
Top comments (2)
Nice oneππ½ππ½ππ½, but why no password less login into the server?
Thank you Joshua π, This was just a POC, In a Real working environment, ssh would be prefered over username and password.