#!/usr/bin/bash
# This script will join a linux host to an active directory domain using kerberos (SASL)
# Originally written for CBA (AWS) Infrastructure


# !WARNING! - This script requires several dependencies to work...
# See Issue tracker Issue #52192 for more information

NGKICKSTART=/etc/sysconfig/ng-kickstart

function usage() {

        local ESC=$(printf '\e')
        local BOLD="${ESC}[1m"
        local NORM="${ESC}[0m"

        cat <<-EOD 2>&1

		${BOLD}SYNOPSIS${NORM}

		    Attempts to join "$HOSTNAME" to the target Active Directory domain using kerberos (SASL) authentication
		    This host ("$(hostname)") must be using an FQDN!


		${BOLD}USAGE${NORM}

		    $0 [options] OR --ng-kickstart [true]


		${BOLD}OPTIONS${NORM}

		    --keytab </path/to/file.keytab>
		        Full file path to the Kerberos keytab which contains the service user credentials

		    --user <username>
		        The service user which authenticates to the target Active Directory
		        (the keytab file should contain this users credentials)

		    --fqdn <ad.example.com>
		        The FQDN of the Active Directory you are attemping to join

		    --ldap-search-base <ou=foo,dc=ad,dc=example,dc=com>
		        The Active Directory LDAP search base which will be used in sssd configuration

		    --ldap-sudoers-group <linuxadmins>
		        The LDAP sudoers group which sshd will match against. Case sensitive
		        Required for anyone who needs to run the sudo command

		${BOLD}ALTERNATE OPTIONS${NORM}
		    --ng-kickstart true
		        This option takes no arguments and will OVERRIDE all other options
		        All variables will be sourced from the $NGKICKSTART file (generated by cloud-init)

	EOD
	exit 1
}

#
#
if [[ -z "$1" ]]; then
	usage
fi

while [[ "$1" =~ ^-- ]]; do
	case "$1" in
                --keytab)
			shift;
			KT="$1"
                        ;;
                --user)
			shift;
			ADUSER="$1"
                        ;;
                --fqdn)
			shift;
			FQDN="$1"
                        ;;
		--ldap-search-base)
			shift;
			LDAPSB="$1"
			;;
		--ldap-sudoers-group)
			shift;
			LDAPSUDO="$1"
			;;
		--ng-kickstart)
			shift;
			KICKSTART="$1"
			;;
		*)
			echo "Unrecognized option '$1'"
			usage
			;;
	esac
	shift;
done

# All variables are mandatory
if [ -z "${KT}${ADUSER}${FQDN}${ADUSER}${LDAPSB}${LDAPSUDO}" ] && [ -z "$KICKSTART" ]; then
        echo -e "\n\033[31;1mERROR\033[0m\n\n        Missing options" >&2
	logger -t "${0:2}" "ERROR: Missing options"
	usage;
fi

# Are we using the ng-kickstart option?
if [ "$KICKSTART" == "true" ]; then
	if [ ! -f "$NGKICKSTART" ]; then
		logger -s -t "${0:2}" "ERROR: File $NGKICKSTART not found!"
		exit 2
	fi

	# Get cloud-init configured variables if available
	source $NGKICKSTART

	# Set script variables to use cloud-init ones
	KT=$adkeytab
	ADUSER=$aduser
	FQDN=$adfqdn
	LDAPSB=$ldapsearchbase
	LDAPSUDO=$ldapsudoersgroup
fi

# Create array of LOCAL UIDs and GIDs (users and groups_ which should originate from AD instead
# Notes: The order of getent databases (e.g. passwd and group) will be different even if the results are the same
# -----> This method only supports real "users" - i.e. that the user has both a UID and GID and is not just a lone group entity
declare -A LOCALUSERS

for username in $(getent passwd | awk -F ":" '/^(oca|op|nif)(_.+|:)/ {print $1}'| sort); do
        LOCALUSERS[$username]="$username $(id -u $username) $username $(id -g $username)"
done


# Verify the Keytab file exists
if [ ! -f "$KT" ]; then
	logger -s -t "${0:2}" "ERROR: Keytab $KT does not exist"
	exit 3
fi

# Check if the current hostname is an FQDN otherwise exit
LONGHOST=$(hostname)
if [[ "$LONGHOST" != *$FQDN ]]; then
	logger -s -t "${0:2}" "ERROR: "$HOSTNAME" does not appear to be an FQDN!"
	exit 4
fi

# We (generally) want the FQDN in upper case
FQDN=${FQDN^^}

# Get Kerberos Ticket for joining the domain
kinit $ADUSER@$FQDN -k -t $KT

# We won't be needing THIS
echo > /etc/sssd/sssd.conf


# Create realmd configuration
# INFRA-934: Create PREDICTABLE netBIOS/ComputerObject name
# this is based on the hostname of the client joining the domain
# i.e. the same hostname will always have the same netBIOS name
PCNAME=$(ng-mk-auth-token "netbios:${HOSTNAME}" 15)
cat << EOF > /etc/realmd.conf
[service]
automatic-install = nosssd

[users]
default-home = /home/%U
default-shell = /bin/bash

[${FQDN,,}]
automatic-id-mapping = no
fully-qualified-names = no
computer-name = ${PCNAME}
EOF

# Restart the realmd service
systemctl restart realmd

# Join the realm (active directory domain)
realm join "$FQDN"

# TODO: Parse OU etc. from the FQDN
# Modify SSSD configuration
augtool --autosave --backup --noautoload << EOD
	set /augeas/load/sssd/lens "sssd.lns"
	set /augeas/load/sssd/incl "/etc/sssd/sssd.conf"
	load
	set /files/etc/sssd/sssd.conf/target[1]/services "nss, pam, ssh, sudo"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_search_base "$LDAPSB"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_user_search_base "ou=users,$LDAPSB"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_group_search_base "ou=groups,$LDAPSB"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_user_extra_attrs "altSecurityIdentities:altSecurityIdentities"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_user_ssh_public_key "altSecurityIdentities"
	set /files/etc/sssd/sssd.conf/target[2]/sudo_provider "ad"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_sudo_search_base "ou=SUDOers,$LDAPSB"
	set /files/etc/sssd/sssd.conf/target[2]/enumerate "True"
	set /files/etc/sssd/sssd.conf/target[2]/ldap_group_name "cn"
	set /files/etc/sssd/sssd.conf/target[3] "pam"
	set /files/etc/sssd/sssd.conf/target[3]/reconnection_retries "3"
	set /files/etc/sssd/sssd.conf/target[3]/offline_credentials_expiration "5"
	set /files/etc/sssd/sssd.conf/target[4] "ssh"
	set /files/etc/sssd/sssd.conf/target[5] "sudo"
EOD

# Modify sshd configuration for sudoers group match
augtool --autosave --backup --noautoload << EOD
	set /augeas/load/sshd/lens "sshd.lns"
	set /augeas/load/sshd/incl "/etc/ssh/sshd_config"
	load
	defnode cond /files/etc/ssh/sshd_config/Match[Condition/Group = "linuxadmins"]/Condition/Group "$LDAPSUDO"
	set \$cond/../../Settings/AuthenticationMethods "password publickey"
	set \$cond/../../Settings/AuthorizedKeysCommand "/usr/bin/sss_ssh_authorizedkeys"
	set \$cond/../../Settings/AuthorizedKeysCommandUser "nobody"
EOD

# Restart modified services
systemctl restart sssd sshd

# Edit nsswitch for sss and sudoers configuration
augtool --autosave --backup --noautoload <<-EOD
	set /augeas/load/nsswitch/lens "nsswitch.lns"
	set /augeas/load/nsswitch/incl "/etc/nsswitch.conf"
	load
	set /files/etc/nsswitch.conf/database[1] "passwd"
	set /files/etc/nsswitch.conf/database[1]/service[1] "files"
	set /files/etc/nsswitch.conf/database[1]/service[2] "sss"
	rm  /files/etc/nsswitch.conf/database[1]/service[3]
	set /files/etc/nsswitch.conf/database[2] "shadow"
	set /files/etc/nsswitch.conf/database[2]/service[1] "files"
	set /files/etc/nsswitch.conf/database[2]/service[2] "sss"
	rm  /files/etc/nsswitch.conf/database[2]/service[3]
	set /files/etc/nsswitch.conf/database[3] "group"
	set /files/etc/nsswitch.conf/database[3]/service[1] "files"
	set /files/etc/nsswitch.conf/database[3]/service[2] "sss"
	rm  /files/etc/nsswitch.conf/database[3]/service[3]
	set /files/etc/nsswitch.conf/database[16] "sudoers"
	set /files/etc/nsswitch.conf/database[16]/service[1] "files"
	set /files/etc/nsswitch.conf/database[16]/service[2] "sss"
EOD

# Remove users from local passwd and group files
for i in $(getent passwd | awk -F ":" '/^(oca|op|nif)(_.+|:)/ {print $1}'| sort); do
	augtool --autosave --backup --noautoload <<-EOD
		set /augeas/load/passwd/lens "passwd.lns"
		set /augeas/load/passwd/incl "/etc/passwd"
		load
		rm /files/etc/passwd/$i
	EOD

	augtool --autosave --backup --noautoload <<-EOD
		set /augeas/load/group/lens "group.lns"
		set /augeas/load/group/incl "/etc/group"
		load
		rm /files/etc/group/$i
	EOD
done


# Story time
# Now that the local OCA user has now been removed - update any files which still have the local uid/gid of the OCA user
# to be owned by the uid/gid of the Active Directory OCA user
#
# Note: We don't want to change ownership on the gluster (client) mounts of the web headend servers
# Those changes should propogate directlry from the backend servers (gluster server)
#
# If we have a mountpoint which contains the word gluster, but isn't a filesystem type of fuse.glusterfs
# Then we assume this is a gluster brick and therefore a gluster server
# And that our permission changes should be applied to all glusterfs mountpoints

# Function to take an expanded array of local user information and mountpoints 
# and change permissions from the OLD (locally) created OCA uid/gid to new Active Directory based uid/gid

perm_rewrite () {
	# username=$1 uid=$2 groupname=$3 gid=$4
	#
	# Mount points to process (first four positional params are reserved)
	WHERE=( "${@:5}" )

	# Fix symlink owners
	find "${WHERE[@]}" -xdev -type l -uid $2 -print0 | xargs --null --no-run-if-empty chown $1

	# Fix symlink groups
	find "${WHERE[@]}" -xdev -type l -gid $4 -print0 | xargs --null --no-run-if-empty chgrp $3

	# Find any normal files to fix....
	# Match and print the file record (ORS=) if it contains the old uid or gid with awk
	# rewrite the old uid/gid to the symbolic username with sed
	# apply to the file(s) with setfacl --restore
	#
	# NOTE: For reasons unknown, awk cannot match the end of line ($) for the group entry in the output of getfacl
	# BUT it can match this for the owner line. As a workaround we just match on the newline (\n) instead (which works for both)
	find "${WHERE[@]}" -xdev -not -type l -print0 \
	 | xargs --null --no-run-if-empty getfacl -p  \
	 | awk -vRS= -vORS='\n\n' "/(owner: $2\n|group: $4\n|user:$2:|group:$4:)/" \
	 | sed -re "s/owner: $2\$/owner: $1/g"  \
	        -e "s/group: $4\$/group: $3/g"  \
	        -e "s/user:$2:/user:$1:/g"      \
	        -e "s/group:$4:/group:$3:/g"    \
	 | setfacl --restore=/dev/stdin
}

# Get glusterfs mountpoints, excluding bindmounts
GLUSTERMNTS=$(findmnt --noheadings --df --type=fuse.glusterfs 2>/dev/null | awk '($1 !~ ".*]$") {printf "%s ",$7}')
# Get any mounts which are not of glusterfs or devtmpfs filesystem type, excluding bindmounts
REGMNTS=$(findmnt --noheadings --df 2>/dev/null | awk '($1 !~ ".*]$" && $2 !~ "fuse.glusterfs|devtmpfs") {printf "%s ",$7}')

# For glusterfs mountpoints, to avoid contention issues between gluster servers and clients
# We first verify we can get an flock on the mountpoint before doing anything
# Each mountpoint is processed individually
PROCESSFILE=$(basename "$0").process
for mnt in $GLUSTERMNTS; do
	# Lock on file descriptor of subshell
	(
	flock --exclusive --nonblock 9
	# If the flock succeded
	if [[ $? -eq 0 ]]; then
		# And if we have not already changed permissions on this mountpoint
		if [[ ! -f $mnt/$PROCESSFILE ]]; then
		
			# Call our function to update all permissions of mountpoint
			for i in "${!LOCALUSERS[@]}"; do
				perm_rewrite "${LOCALUSERS[$i]}" $mnt
			done
			
			# Changes are finished - Create process file to indicate we have applied permissions changes
			cat <<-EOD > $mnt/$PROCESSFILE
				###############################################################################################
				# This file was automatically generated by
				# by $(readlink -f "$0") on $(date)
				# This file exists to prevent the above script from running more than once
				# against the $mnt mountpoint
				###############################################################################################
			EOD
			chmod 444 $mnt/$PROCESSFILE
		fi
	fi
	# We must write the flock to a file because it doesn't work on the root of the mountpoint
	) 9>$mnt/$PROCESSFILE
	rm -f $mnt/$PROCESSFILE
done

# Regular mounts are per host and are processsed concurrently
for i in "${!LOCALUSERS[@]}"; do
	perm_rewrite ${LOCALUSERS[$i]} $REGMNTS
done

# TODO: Remove Keytab after verifying we can ID a user from the domain
# Remove keytab from disk
rm -f "$KT"
