#!/bin/bash
#
# Univention Server
#  helper script: creates new machine password
#
# Copyright 2004-2014 Univention GmbH
#
# http://www.univention.de/
#
# All rights reserved.
#
# The source code of this program is made available
# under the terms of the GNU Affero General Public License version 3
# (GNU AGPL V3) as published by the Free Software Foundation.
#
# Binary versions of this program provided by Univention to you as
# well as other copyrighted, protected or trademarked materials like
# Logos, graphics, fonts, specific documentations and configurations,
# cryptographic keys etc. are subject to a license agreement between
# you and Univention and not subject to the GNU AGPL V3.
#
# In the case you use this program under the terms of the GNU AGPL V3,
# the program is provided in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License with the Debian GNU/Linux or Univention distribution in file
# /usr/share/common-licenses/AGPL-3; if not, see
# <http://www.gnu.org/licenses/>.

. /usr/share/univention-lib/all.sh
create_logfile_if_missing /var/log/univention/server_password_change.log "root:adm" 640

exec 3>>/var/log/univention/server_password_change.log
echo "Starting server password change ($(date))" >&3
FAIL () { # log error message to log file and std-err, then fail
	echo "$@" >&3
	echo "$@" >&2
	exit 1
}

eval "$(/usr/sbin/univention-config-registry shell)"

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

if [ -z "$server_role" ]; then
	FAIL "failed to change server password: empty config-registry variable server/role"
fi
if [ -z "$ldap_hostdn" ]; then
	FAIL "failed to change server password: empty config-registry variable ldap/hostdn"
fi

if [ ! -e "/etc/machine.secret" ]; then
	FAIL "failed to change server password: /etc/machine.secret not found"
fi

if [ -e "/var/lib/univention-directory-replication/failed.ldif" ]; then
	FAIL "failed to change server password: /var/lib/univention-directory-replication/failed.ldif exists"
fi

# Allow password change only if it is scheduled.
epoch_last_change="$(stat --format %Y /etc/machine.secret)"
epoch="$(date +%s)"
seconds_last_change="$(($epoch - $epoch_last_change))"
days_last_change="$(($seconds_last_change/60/60/24))"
if (("$server_password_interval" > "$days_last_change")); then
	echo "No server password change scheduled for today, terminating without a change" >&3
	exit 0
else
	echo "Proceeding with regular server password change scheduled for today" >&3
fi

# Try to use a trivial command just to check that LDAP server is reachable.
univention-ldapsearch -D "$ldap_hostdn" -w "$(cat /etc/machine.secret)" -s base > /dev/null 2>&3
if [ $? -ne 0 ]; then
	FAIL "failed to contact LDAP server: cannot connect with univention-ldapsearch"
fi

new_password="$(create_machine_password)"
old_password="$(cat /etc/machine.secret)"

if [ -z "$new_password" ]; then
	FAIL "failed to change server password: create_machine_password() returned an empty password"
fi

# Try to run hook scripts for "prechange" (which are named '^[A-Za-z0-9_-]+$')
# 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 prechange -- /usr/lib/univention-server/server_password_change.d >&3 2>&3
# If ANY of the scripts fails while doing "prechange", then rollback with "nochange".
if [ $? != 0 ]; then
	# Use run-parts without --exit-on-error; go through all scripts.
	run-parts --verbose --arg nochange -- /usr/lib/univention-server/server_password_change.d >&3 2>&3
	FAIL "run-parts failed during prechange, rolling back with nochange, server password unchanged"
fi

# Try to modify the server password with UDM.
/usr/sbin/univention-directory-manager "computers/$server_role" modify --binddn "$ldap_hostdn" --bindpwd "$old_password" --dn "$ldap_hostdn" --set password="$new_password" >&3 2>&3
# If changing the server password with UDM failed for some unknown reason,
# then rollback the previous run-parts operation. 
if [ $? != 0 ]; then
	# run hook scripts for "nochange" (which are named '^[A-Za-z0-9_-]+$')
	run-parts --verbose --arg nochange -- /usr/lib/univention-server/server_password_change.d >&3 2>&3
	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 MASTER.
# Repeat this several times, just in case password distribution takes some time.
trial_counter=60
while sleep 1
do
	# Try to use a trivial command just to check that the new password works.
	univention-ldapsearch -D "$ldap_hostdn" -w "$new_password" -h "$ldap_master" -p "$ldap_master_port" -s base > /dev/null 2>&3
	if [ $? -eq 0 ]; then
		# OK, password worked against master, go on with the script.
		break
	fi
	# If the new password failed for a long time, give up.
	if [ $trial_counter -eq 0 ]; then
		# The server is in an inconsistent state because the new password has
		# been set with UDM but LDAP does't 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.
		/usr/sbin/univention-directory-manager "computers/$server_role" modify --binddn "$ldap_hostdn" --bindpwd "$new_password" --dn "$ldap_hostdn" --set password="$old_password" >&3 2>&3

		# run hook scripts for "nochange" (which are named '^[A-Za-z0-9_-]+$')
		run-parts --verbose --arg nochange -- /usr/lib/univention-server/server_password_change.d >&3 2>&3
		FAIL "resetting old server password for $ldap_hostdn, because access to LDAP master did not work with the new password"
	fi
	trial_counter=$(( trial_counter - 1))
done

# Now that we are sure the new password already works with LDAP master,
# 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.
echo "$(date +"%y%m%d%H%M"): $old_password" >>/etc/machine.secret.old
chmod 600 /etc/machine.secret.old

# change machine.secret and restart listener
echo -n "$new_password" >/etc/machine.secret
chmod 600 /etc/machine.secret
[ -e /etc/init.d/univention-directory-listener ] && invoke-rc.d univention-directory-listener restart >&3

# The password is changed on the master 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.
trial_counter=60
while sleep 1
do
	# Try to use a trivial command just to check that the new password works.
	univention-ldapsearch -D "$ldap_hostdn" -w "$new_password" -s base > /dev/null 2>&3
	if [ $? -eq 0 ]; then
		# OK, password worked, go on with the script.
		break
	fi
	# If the new password failed for a long time, give up.
	if [ $trial_counter -eq 0 ]; then
		# The server is in an inconsistent state because the new password has
		# been set with UDM but LDAP does't 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.

		/usr/sbin/univention-directory-manager "computers/$server_role" modify --binddn "$ldap_hostdn" --bindpwd "$new_password" --dn "$ldap_hostdn" --set password="$old_password" >&3 2>&3

		# Rollback /etc/machine.secret and restart listener
		awk '{pwd=$2}; END {printf("%s", pwd)}' /etc/machine.secret.old > /etc/machine.secret
		chmod 600 /etc/machine.secret
		[ -e /etc/init.d/univention-directory-listener ] && invoke-rc.d univention-directory-listener restart >&3

		# run hook scripts for "nochange" (which are named '^[A-Za-z0-9_-]+$')
		run-parts --verbose --arg nochange -- /usr/lib/univention-server/server_password_change.d >&3 2>&3
		FAIL "resetting old server password for $ldap_hostdn, because access to local LDAP did not work with the new password"
	fi
	trial_counter=$(( trial_counter - 1))
done

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

if [ "$server_role" != "domaincontroller_master" ] && [ "$server_role" != "domaincontroller_backup" ]; then
	if [ -x /etc/init.d/univention-directory-listener ]; then
		invoke-rc.d univention-directory-listener crestart >&3 2>&3
	fi
fi

# run hook scripts for "postchange" (which are named '^[A-Za-z0-9_-]+$')
# Use run-parts without --exit-on-error; go through all scripts.
run-parts --verbose --arg postchange -- /usr/lib/univention-server/server_password_change.d >&3 2>&3

echo "done ($(date))" >&3
exec 3<&-

exit 0
