#!/usr/bin/env bash
#
# Author: Georg Voell - georg.voell@standby.cloud
# Version: @(#)user-management 3.2.0 12.08.2024 (c)2024 Standby.cloud
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
#
# This script can be used free of charge. Use it as is or customize as needed. It is not guaranteed to be
# error free and you will not be reimbursed for any damage it may cause.
#
#@  Manages users within Linux.
#@
#@Usage: user-management [options] [action] username [groupname]
#@  Options:
#@    -h, --help            : Displays helptext.
#@    -v, --version         : Displays the version of the script.
#@    -a, --all             : Display all users.
#@    -u, --uid <number>    : User ID.
#@    -c, --comment <string>: Comment.
#@    -s, --shell <string>  : Login Shell.
#@    -d, --home <string>   : Home directory.
#@    -g, --groups <string> : Additional groups (comma separated) or empty string to remove groups.
#@
#@  Action:
#@    show  : Show users.
#@    check : Check if user exists and has the given attributes - otherwise create or change.
#@    lock  : Lock user.
#@    unlock: Unlock user.
#@    delete: Delete user.
#@
#@Examples:
#@  user-management check oracle dba -c "Oracle user"
#@    Create user 'oracle' (if it doesn't exist) and assign group 'dba'.
#@  user-management delete oracle
#@    Delete user 'oracle'.
#
# Exit codes:
#  01: Unknown or wrong parameter and only LINUX supported and script needs to be executed with ROOT privileges.
#  02: Unknown action.
#  03: No username specified.
#  99: User interrupt.
#
# See also:
#  **bootstrap**(1), **disk-management**(1), **key-management**(1), **install-scripts**(1)
#
# ToDo:
#
# Known bugs:
#
# Update history:
#
# V 3.0.0 15.07.2020 New version
# V 3.0.1 16.05.2021 List all users
# V 3.0.2 22.05.2021 More actions
# V 3.0.3 04.06.2023 Minor bugfixes (create new user with specified home directory)
# V 3.1.0 05.06.2023 New copyright
# V 3.2.0 12.08.2024 New minor version
#

# Find executable bash library and source it
lib=`which lib.bash 2>/dev/null | sed 's|^no 'lib.bash' in .*||'`
if [ "$lib" != "" ]; then
	source "$lib"
else
	progdir=`dirname "$0"`
	if [ -r "${progdir}/lib.bash" ]; then
		source "${progdir}/lib.bash"
	else
		echo "Unexpected error: Unable to locate bash library 'lib.bash'."
		exit 1
	fi
fi

# Read configuration for users
function ReadConfig() {
	local config="/etc/login.defs"
	
	if [ -r "$config" ]; then
		cat $config | tr -s '	' ' ' | tr -s ' ' ' ' > $scratchfile
		uid_min=`grep "^UID_MIN " $scratchfile | cut -d' ' -f2`
		uid_max=`grep "^UID_MAX " $scratchfile | cut -d' ' -f2`
		create_home=`grep "^CREATE_HOME " $scratchfile | cut -d' ' -f2 | tolower`
	fi
}

# Display all regular users
function DisplayUsers() {
	local username=""
	local pass=""
	local uid=0
	local gid=0
	local comment=""
	local home=""
	local shell=""
	local result=""
	local mgroup=""
	local ogroup=""
	local agroups=""
	local keys=""
	local token=""
	local key=""
	local name=""

	if [ -r "$passwdfile" ]; then
		printf "username\tpassword\tuid\tgid\tgroups\tcomment\thome\tshell\tkeys\n" > $scratchfile
		
		while IFS=':' read -r username pass uid gid comment home shell; do
			if [ "$showall" = true -o $uid -ge $uid_min -a $uid -le $uid_max ]; then
				if [ "$pass" = " " -o "$pass" = "" ]; then
					pass="null"
				elif [ "$pass" = "*" ]; then
					pass="Nologin"
				elif [ "$pass" != "x" ]; then
					result=`grep "^!.*"`
					if [ "$result" != "" ]; then
						pass="Locked"
					else
						pass="Set"
					fi
				else
					pass="Unknown"
					if [ -r "$shadowfile" ]; then
						result=`grep "^${username}:" $shadowfile`
						if [ "$result" != "" ]; then
							pass=`echo "$result" | cut -d':' -f2`
							if [ "$pass" = "!!" ]; then
								pass="null"
							elif [ "$pass" = '*' -o "$pass" = '!' ]; then
								pass="Nologin"
							else
								result=`echo "$pass" | grep "^!.*"`
								if [ "$result" != "" ]; then
									pass="Locked"
								else
									pass="Set"
								fi
							fi
						fi
					fi
				fi
				
				if [ "$comment" = "" ]; then
					comment="null"
				fi

				mgroup=`grep "^.*:.*:$gid:" $groupfile | cut -d':' -f1`
				ogroup=`grep "$username" $groupfile | grep -v "^${mgroup}:" | cut -d':' -f1`
				if [ "$ogroup" = "" ]; then
					if [ "$mgroup" = "" ]; then
						agroups=""
					else
						agroups="($mgroup)"
					fi
				else
					if [ "$mgroup" = "" ]; then
						agroups=`printf "$ogroup" | tr '\n' ','`
					else
						agroups=`printf "(${mgroup}),$ogroup" | tr '\n' ','`
					fi
				fi
				
				keys=""
				if [ ! -r "${home}/$keyfile" ]; then
					keys="null"
				else
					while IFS=' ' read -r token key name; do
						if [ "$token" = "ssh-rsa" ]; then
							if [ "$name" = "" ]; then
								name="Nameless"
							fi
							
							if [ "$keys" = "" ]; then
								keys="$name"
							else
								keys="${keys},$name"
							fi
						fi
					done < <(cat "${home}/$keyfile" | stripcomment | sed 's|^.*ssh-rsa |ssh-rsa |')
					
					if [ "$keys" = "" ]; then
						keys="null"
					fi
				fi
				
				printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$username" "$pass" "$uid" "$gid" "$agroups" "$comment" "$home" "$shell" "$keys" >> $scratchfile
			fi
		done < <(cat "$passwdfile" | stripcomment | grep .)
		
		if [ "$showall" = true ]; then
			printf "\nAll users:\n\n"
		else
			printf "\nRegular users:\n\n"
		fi
		
		print-table --import $scratchfile
	fi
}

function CreateUser() {
	local username=${1}
	local groupname=${2}
	local newhome=""
	local stat=0

	if [ "$username" != "" -a "$groupname" != "" ]; then
		# If we don't have a specific homedir - use a default
		if [ "$home" = "" ]; then
			newhome="/home/$username"
		else
			newhome="$home"
		fi
		
		# Create the user
		if [ "$create_home" = "yes" ]; then
			useradd "$username" -g "$groupname" -m -d "$newhome" 2>/dev/null
			stat=$?
		else
			useradd "$username" -g "$groupname" -d "$newhome" 2>/dev/null
			stat=$?
		fi
		
		if [ $stat -gt 0 ]; then
			errormsg 0 "Unable to create user '$username'."
		else
			# Modify
			if [ "$uid" != "" ]; then
				usermod -u "$uid" "$username" 2>/dev/null
				stat=$?
			
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to set UID to '$uid' for user '$username'. Using default."
				fi
			fi
			
			if [ "$comment" != "" ]; then
				usermod -c "$comment" "$username" 2>/dev/null
				stat=$?
				
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to set comment to '$comment' for user '$username'."
				fi
			fi
			
			if [ "$shell" != "" ]; then
				usermod -s "$shell" "$username" 2>/dev/null
				stat=$?
				
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to set login shell to '$shell' for user '$username'."
				fi
			fi
			
			if [ "$addgroups" != "NoneSpecified" ]; then
				usermod -G "$addgroups" "$username" 2>/dev/null
				stat=$?
				
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to change secondary groups '$addgroups' for user '$username'."
				fi
			fi
		fi
	else
		errormsg 0 "Unexpected: Empty username or groupname."
	fi
}

function LockUser() {
	local username=${1}
	local userstr=""
	local result=""
	local pass=""
	local stat=0

	if [ -r "$passwdfile" -a "$username" != "" ]; then
		userstr=`cat "$passwdfile" | stripcomment | grep "^${username}:"`
		if [ "$userstr" != "" ]; then
			if [ -r "$shadowfile" ]; then
				result=`grep "^${username}:" $shadowfile`
				if [ "$result" != "" ]; then
					pass=`echo "$result" | cut -d':' -f2`
				fi
			fi
			
			if [ "$pass" = "!!" ]; then
				errormsg 0 "Password not set for user '$username'."
			else
				usermod -L "$username" 2>/dev/null
				stat=$?
			
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to lock user '$username'."
				fi
			fi
		else
			errormsg 0 "User '$username' does not exist."
		fi
	fi
}

function UnlockUser() {
	local username=${1}
	local userstr=""
	local stat=0

	if [ -r "$passwdfile" -a "$username" != "" ]; then
		userstr=`cat "$passwdfile" | stripcomment | grep "^${username}:"`
		if [ "$userstr" != "" ]; then
			usermod -U "$username" 2>/dev/null
			stat=$?
			
			if [ $stat -gt 0 ]; then
				errormsg 0 "Unable to unlock user '$username'."
			fi
		else
			errormsg 0 "User '$username' does not exist."
		fi
	fi
}

function DeleteUser() {
	local username=${1}
	local userstr=""
	local stat=0

	if [ -r "$passwdfile" -a "$username" != "" ]; then
		userstr=`cat "$passwdfile" | stripcomment | grep "^${username}:"`
		if [ "$userstr" != "" ]; then
			userdel -f -r "$username" 2>/dev/null
			stat=$?
			
			if [ $stat -gt 0 ]; then
				errormsg 0 "Unable to delete user '$username'."
			fi
		else
			errormsg 0 "User '$username' does not exist."
		fi
	fi
}

function CheckUser() {
	local username=${1}
	local groupname=${2}
	local userstr=""
	local groupstr=""
	local olduid=""
	local oldgid=""
	local oldcomment=""
	local oldhome=""
	local oldshell=""
	local gid=100
	local stat=0

	if [ -r "$passwdfile" -a -r "$groupfile" -a "$username" != "" ]; then
		userstr=`cat "$passwdfile" | stripcomment | grep "^${username}:"`
		if [ "$userstr" != "" ]; then
			# User exists
			# username pass uid gid comment home shell
			olduid=`echo "$userstr" | cut -d':' -f3`
			oldgid=`echo "$userstr" | cut -d':' -f4`
			oldcomment=`echo "$userstr" | cut -d':' -f5`
			oldhome=`echo "$userstr" | cut -d':' -f6`
			oldshell=`echo "$userstr" | cut -d':' -f7`
			
			if [ "$uid" != "" ]; then
				if [ "$olduid" != "$uid" ]; then
					usermod -u "$uid" "$username" 2>/dev/null
					stat=$?
					
					if [ $stat -gt 0 ]; then
						errormsg 0 "Unable to change UID from '$olduid' to '$uid' for user '$username'."
					fi
				fi
			fi
			
			if [ "$groupname" != "" ]; then
				groupstr=`cat "$groupfile" | stripcomment | grep "^${groupname}:"`
				if [ "$groupstr" = "" ]; then
					# Group does not exists - create it first
					groupadd "$groupname"
					stat=$?
					
					if [ $stat -gt 0 ]; then
						errormsg 0 "Unable to create group '$groupname'. Using default group 'users'."
						groupname="users"
					fi
					
					groupstr=`cat "$groupfile" | stripcomment | grep "^${groupname}:"`
				fi
				
				if [ "$groupstr" != "" ]; then
					# groupname pass gid member
					gid=`echo "$groupstr" | cut -d':' -f3`
					if [ "$oldgid" != "$gid" ]; then
						usermod -g "$gid" "$username" 2>/dev/null
						stat=$?
						
						if [ $stat -gt 0 ]; then
							errormsg 0 "Unable to change group '$groupname' from '$oldgid' to '$gid' for user '$username'."
						fi
					fi
				fi
			fi
			
			if [ "$comment" != "" ]; then
				if [ "$oldcomment" != "$comment" ]; then
					usermod -c "$comment" "$username" 2>/dev/null
					stat=$?
					
					if [ $stat -gt 0 ]; then
						errormsg 0 "Unable to change comment from '$oldcomment' to '$comment' for user '$username'."
					fi
				fi
			fi
			
			if [ "$home" != "" ]; then
				if [ "$oldhome" != "$home" ]; then
					if [ "$create_home" = "yes" ]; then
						usermod -m -d "$home" "$username" 2>/dev/null
						stat=$?
					else
						usermod -d "$home" "$username" 2>/dev/null
						stat=$?
					fi
					
					if [ $stat -gt 0 ]; then
						errormsg 0 "Unable to change home dir from '$oldhome' to '$home' for user '$username'."
					fi
				fi
			fi
			
			if [ "$shell" != "" ]; then
				if [ "$oldshell" != "$shell" ]; then
					usermod -s "$shell" "$username" 2>/dev/null
					stat=$?
					
					if [ $stat -gt 0 ]; then
						errormsg 0 "Unable to change login shell from '$oldshell' to '$shell' for user '$username'."
					fi
				fi
			fi
			
			if [ "$addgroups" != "NoneSpecified" ]; then
				usermod -G "$addgroups" "$username" 2>/dev/null
				stat=$?
				
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to change secondary groups '$addgroups' for user '$username'."
				fi
			fi
		else
			# User does not exist yet
			if [ "$groupname" = "" ]; then
				# Default group for users
				groupname="users"
			fi

			groupstr=`cat "$groupfile" | stripcomment | grep "^${groupname}:"`
			if [ "$groupstr" = "" ]; then
				# Group does not exists - create it first
				groupadd "$groupname"
				stat=$?
				
				if [ $stat -gt 0 ]; then
					errormsg 0 "Unable to create group '$groupname'. Using default group 'users'."
					groupname="users"
				fi
			fi

			# Create user
			CreateUser "$username" "$groupname"
		fi
	fi
}

# Check if we are running on Linux and have root privileges.
if [ "$OS" != "Linux" ]; then
	errstr="This script only supports LINUX."
elif [ "$USER" != "root" ]; then
	# Try sudo
	check-sudo
	stat=$?

	if [ $stat -eq 0 ]; then
		printf 'sudo -n "%s"' "$script" > $scratchfile
		while [ $# -gt 0 ]; do
			pname=${1}
			printf ' "%s"' "$pname" >> $scratchfile
			shift
		done
		
		# Execute script
		chmod 700 $scratchfile
		$scratchfile
		stat=$?
		
		# Cleanup
		rm -f $scratchfile
		exit $stat
	else
		# Need root privileges
		errstr="Root privileges needed to continue."
	fi
fi

# Preset
action=""
param1=""
param2=""
comment=""
shell=""
home=""
uid=""
addgroups="NoneSpecified"
showall=false
uid_min=1000
uid_max=60000
create_home="yes"

# Check parameters: Loop until all parameters are used up
while [ $# -gt 0 ]; do
	pname=${1}
	case "$pname" in
		-v | --version)
			shift
			showversion=true
			;;
		-h | --help)
			shift
			showhelp=true
			;;
		-a | --all)
			shift
			showall=true
			;;
		-u | --uid)
			shift
			if [ "$1" != "" ]; then
				uid=${1}
				result=`echo "$uid" | grep '^[0123456789]*$'`
				if [ "$result" = "" ]; then
					errstr="Id has to be a number."
				fi
				shift
			else
				errstr="Please specify an id after parameter '$pname'."
			fi
			;;
		-c | --comment)
			shift
			if [ "$1" != "" ]; then
				comment=${1}
				shift
			else
				errstr="Please specify a comment after parameter '$pname'."
			fi
			;;
		-s | --shell)
			shift
			if [ "$1" != "" ]; then
				shell=${1}
				result=`filecheck -x "$shell"`
				if [ "$result" = "" ]; then
					errstr="Login shell '$shell' does not exist or is not executable."
				fi
				shift
			else
				errstr="Please specify a comment after parameter '$pname'."
			fi
			;;
		-d | --home)
			shift
			if [ "$1" != "" ]; then
				home=${1}
				shift
			else
				errstr="Please specify a directory after parameter '$pname'."
			fi
			;;
		-g | --groups)
			shift
			paramck=`echo "$1" | grep '^-'`
			if [ "$paramck" = "" ]; then
				addgroups=${1}
				shift
			else
				errstr="Please specify none (empty string), one or more groups (comma separated) after parameter '$pname'."
			fi
			;;
		*)
			shift
			paramck=`echo "$pname" | grep '^-'` # Keys don't begin with '-'
			if [ "$paramck" != "" ]; then
				errstr="Unknown option '$pname'."
			else
				if [ "$action" = "" ]; then
					action=`echo "$pname" | tolower`
				elif [ "$param1" = "" ]; then
					param1="$pname"
				elif [ "$param2" = "" ]; then
					param2="$pname"
				else
					errstr="Unknown additional parameter: '$pname'."
				fi
			fi
	esac
done

# Display help or error message
DisplayHelp

# Plausibilty check
if [ "$action" = "" ]; then
	action="show"
fi

# Main
passwdfile="/etc/passwd"
shadowfile="/etc/shadow"
gshadowfile="/etc/gshadow"
groupfile="/etc/group"
keyfile=".ssh/authorized_keys"

if [ "$USER" = "root" ]; then
	ReadConfig
	
	case "$action" in
		show)
			DisplayUsers
			;;
		check)
			if [ "$param1" = "" ]; then
				exitcode=3
				errormsg $exitcode "No username specified."
			else
				CheckUser "$param1" "$param2"
			fi
			;;
		lock)
			if [ "$param1" = "" ]; then
				exitcode=3
				errormsg $exitcode "No username specified."
			else
				LockUser "$param1"
			fi
			;;
		unlock)
			if [ "$param1" = "" ]; then
				exitcode=3
				errormsg $exitcode "No username specified."
			else
				UnlockUser "$param1"
			fi
			;;
		delete)
			if [ "$param1" = "" ]; then
				exitcode=3
				errormsg $exitcode "No username specified."
			else
				DeleteUser "$param1"
			fi
			;;
		*)
			exitcode=2
			errormsg $exitcode "Unknown action: '$action'."
	esac
fi

# Cleanup and exit
Cleanup
exit $exitcode