#!/bin/bash
#
# Univention Server
#  helper script: creates new machine password
#
# SPDX-FileCopyrightText: 2004-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only

# shellcheck disable=SC2154

MSECRET='/etc/machine.secret'
eval "$(/usr/sbin/univention-config-registry shell)"

# shellcheck source=/dev/null
. /usr/share/univention-lib/all.sh
create_logfile_if_missing /var/log/univention/server_password_change.log "root:adm" 640

tmpfile=$(mktemp /tmp/server_password_change.debug.XXXXXX)
exec 4>>$tmpfile
exec 3>>/var/log/univention/server_password_change.log
exec 3> >(tee -ia  "/var/log/univention/server_password_change.log" $tmpfile  >/dev/null 2>/dev/null)

echowithtimestamp "Starting server password change" >&3
check_exit () { #dump debug file if there is an error.
	return_code=$?

	set +o functrace
	trap - DEBUG
	exec 4<&-

	if [ -e $new_pass ]; then
		rm -f '$new_pass'
	fi

	if [ -e $old_pass ]; then
		rm -f '$old_pass'
	fi

	if [ $return_code -ne 0 ]; then
		exec 3>>/var/log/univention/server_password_change.log  # Do not write to tmpfile anymore.
		printf "#========================================================================#\n" >&3
		echowithtimestamp "ERROR during execution, see below for more detailed debug output" >&3
		cat "$tmpfile" >&3
		printf "#========================================================================#\n" >&3
	fi

	rm "$tmpfile"
	exec 3<&-

	return $return_code
}

trap check_exit EXIT

FAIL () { # log error message to log file and std-err, then fail
	msg=$(echowithtimestamp "$@")
	echo "$msg" >&3
	echo "$msg" >&2
	exit 1
}
try_ldap () { # try to connect LDAP server
	local i
	for ((i=0;i<60;i++))
	do
		sleep 1
		univention-ldapsearch \
			-D "$ldap_hostdn" \
			-y "$@" \
			-s base \
			1.1 >/dev/null 2>&3 &&
			return 0
	done
	return 1
}
change_password () {  # old-password-file new-password-file
	echowithtimestamp "Performing LDAP modification, set new password .." >&3
	/usr/sbin/univention-directory-manager "computers/$server_role" modify \
		--binddn "$ldap_hostdn" \
		--bindpwdfile "$1" \
		--dn "$ldap_hostdn" \
		--set password="$(<"$2")" >&3 2>&3
	echowithtimestamp ".. done" >&3
}
run_hooks () {
	# Never use --exit-on-error with run-parts scripts because after an exit-on-error
	# we wouldn't know which scripts have received a "prechange" and need a "nochange".
	run-parts --verbose --arg "$@" -- /usr/lib/univention-server/server_password_change.d >&3 2>&3
}
restart_udl () {
	systemctl try-restart univention-directory-listener >&3
}


# 0 -> set to true
# 1 -> set to false
# 2 -> empty
is_ucr_true server/password/change
if [ $? = 1 ]; then
	echowithtimestamp "Server password change is disabled by the UCR variable server/password/change" >&3
	exit 0
fi

[ -n "$server_role" ] ||
	FAIL "failed to change server password: empty config-registry variable server/role"
[ -n "$ldap_hostdn" ] ||
	FAIL "failed to change server password: empty config-registry variable ldap/hostdn"
[ -e "$MSECRET" ] ||
	FAIL "failed to change server password: $MSECRET not found"
[ -e "/var/lib/univention-directory-replication/failed.ldif" ] &&
	FAIL "failed to change server password: /var/lib/univention-directory-replication/failed.ldif exists"

# Allow password change only if it is scheduled.
epoch_last_change="$(stat --format %Y "$MSECRET")"
epoch="$(date +%s)"
seconds_last_change="$((epoch- epoch_last_change))"
days_last_change="$((seconds_last_change/60/60/24))"
if [ "$server_password_interval" -gt "$days_last_change" ]; then
	echowithtimestamp "No server password change scheduled for today, terminating without a change" >&3
	exit 0
fi

echowithtimestamp "Proceeding with regular server password change scheduled for today" >&3

. /usr/lib/univention-server/lib/server_password_change/debug.sh

# Try to use a trivial command just to check that LDAP server is reachable.
univention-ldapsearch -D "$ldap_hostdn" -y "$MSECRET" -s base 1.1 >/dev/null 2>&3 ||
	FAIL "failed to contact LDAP server: cannot connect with univention-ldapsearch"

old_pass="$(mktemp "$MSECRET.XXXXXXXX")"
new_pass="$(mktemp "$MSECRET.XXXXXXXX")"
chown --reference="$MSECRET" "$old_pass" "$new_pass"
chmod --reference="$MSECRET" "$old_pass" "$new_pass"

ln -f "$MSECRET" "$old_pass"
# shellcheck disable=SC2015
create_machine_password >"$new_pass" &&
	[ -s "$new_pass" ] ||
	FAIL "failed to change server password: create_machine_password() returned an empty password"

if ! run_hooks prechange
then
	run_hooks nochange
	FAIL "run-parts failed during prechange, rolling back with nochange, server password unchanged"
fi

# check if we are in sync with the Primary Directory Node, if not then rollback with "nochange".
check_in_sync () {
	case "$server_role" in
	domaincontroller_backup) ;;
	domaincontroller_slave) ;;
	*) return 0 ;;
	esac
	local i lid=0 nid=1
	for ((i=0;i<120;i++))
	do
		if [ -e "/var/lib/univention-directory-listener/notifier_id" ]; then
			read -r lid </var/lib/univention-directory-listener/notifier_id
			if [ -x "/usr/share/univention-directory-listener/get_notifier_id.py" ]; then
				nid=$(/usr/share/univention-directory-listener/get_notifier_id.py 2>&3) ||
					echowithtimestamp "Could not get notifier id from Primary Directory Node!" >&3
			fi
			[ "${lid:-0}" = "${nid:-1}" ] &&
					return 0
		fi
		echowithtimestamp "Pending listener transactions (lid=$lid < nid=$nid), waiting ..." >&3
		sleep 2
	done
	run_hooks nochange
	FAIL "Pending listener transactions timeout, rolling back with nochange, server password unchanged"
}
check_in_sync

# Try to modify the server password with UDM.
if ! change_password "$old_pass" "$new_pass"
# If changing the server password with UDM failed for some unknown reason,
# then rollback the previous run-parts operation.
then
	run_hooks nochange
	FAIL "failed to change server password for $ldap_hostdn"
fi

# If the changed server password has really been set correctly, then we can already use it.
# Try to use the new password with LDAP against the Primary Directory Node.
# Repeat this several times, just in case password distribution takes some time.
if ! try_ldap "$new_pass" -H "ldap://$ldap_master:$ldap_master_port"
then
		# The server is in an inconsistent state because the new password has
		# been set with UDM but LDAP does not work with it. Do not continue with
		# changes that would only worsen the situation. Instead, try to rollback.
		# Reset the old password with UDM and give up.
		change_password "$new_pass" "$old_pass"

		run_hooks nochange
		FAIL "resetting old server password for $ldap_hostdn, because access to Primary Directory Node LDAP did not work with the new password"
fi

# Now that we are sure the new password already works with Primary Directory Node LDAP,
# we can dare to overwrite the machine password. The machine password is
# needed by the Listener who replicates the changed password to the
# local server's LDAP.
# shellcheck disable=SC2094
{
	chmod 600 "$MSECRET.old"
	printf "%s: " "$(date +'%y%m%d%H%M')"
	cat "$old_pass"
	echo
} >>"$MSECRET.old"

# change machine.secret and restart listener
ln -f "$new_pass" "$MSECRET"
restart_udl

revert_password_change() {
	change_password "$new_pass" "$old_pass"

	# Rollback /etc/machine.secret and restart listener
	ln -f "$old_pass" "$MSECRET"
	restart_udl

	run_hooks nochange
}

# The password is changed on the Primary Directory Node now, but it is not clear if
# this change has been replicated to the local host yet.
# Do the same test as above but with the local LDAP replication.
if ! try_ldap "$new_pass"
then
		# The server is in an inconsistent state because the new password has
		# been set with UDM but LDAP does not work with it. Do not continue with
		# changes that would only worsen the situation. Instead, try to rollback.
		# Reset the old password with UDM and give up.
		revert_password_change
		FAIL "Access to local LDAP did not work with the new password, machine password set back to old password for $ldap_hostdn."
fi

# change samba password locally
if ! run_hooks localchange --regex '^univention-samba4'
# if samba-tool user setpassword fails, reset the old password.
then
	revert_password_change
	FAIL "Failed to set new password in samba, machine password set back to old password for $ldap_hostdn."
fi

# At this point the server password has been changed.
# The change has gone beyond the point-of-no-return and
# we will not try to rollback any more. But all later
# operations will be logged and any failure would become
# obvious through the log file. It is essential now to
# go all the way through all the run-parts scripts with postchange.

case "$server_role" in
domaincontroller_master) ;;
domaincontroller_backup) ;;
*) restart_udl ;;
esac

run_hooks postchange

echowithtimestamp "done" >&3

exit 0
