#!/usr/share/ucs-test/runner bash
# shellcheck shell=bash
## desc: Check account lockout on repeated failed login attempts
## tags:
##  - replication
## roles:
##  - domaincontroller_master
## packages:
##  - univention-config
##  - univention-directory-manager-tools
##  - ldap-utils
## exposure: dangerous
## bugs: [46431, 39817]

# shellcheck source=../../lib/base.sh
. "$TESTLIBPATH/base.sh" || exit 137
# shellcheck source=../../lib/user.sh
. "$TESTLIBPATH/user.sh" || exit 137
# shellcheck source=../../lib/random.sh
. "$TESTLIBPATH/random.sh" || exit 137
# shellcheck source=../../lib/undo.sh
. "$TESTLIBPATH/undo.sh" || exit 137

if [ "$ldap_database_type" != 'mdb' ]; then
	fail_fast 138 "Ppolicy with UDM lockout only works with mdb backend"
fi

#TEST PREPARATION

deactivate_ppolicy() {
	ucr unset ldap/ppolicy/enabled; systemctl restart slapd.service
}

ucr set ldap/ppolicy/enabled=yes; systemctl restart slapd.service; undo deactivate_ppolicy

default_ppolicy_ldif=$(univention-ldapsearch -LLL -b "cn=default,cn=ppolicy,cn=univention,$ldap_base")

old_pwdFailureCountInterval=$(echo "$default_ppolicy_ldif" | VAL pwdFailureCountInterval)
old_pwdMaxFailure=$(echo "$default_ppolicy_ldif" | VAL pwdMaxFailure)
new_pwdFailureCountInterval=15
new_pwdMaxFailure=5

reset_pwdFailureCountInterval() {
	ldapmodify -x -H "ldap://$ldap_master:$ldap_master_port" -D "$tests_domainadmin_account" -y "$tests_domainadmin_pwdfile" <<-%EOR
	dn: cn=default,cn=ppolicy,cn=univention,$ldap_base
	changetype: modify
	replace: pwdMaxFailure
	pwdMaxFailure: $old_pwdMaxFailure
	-
	replace: pwdFailureCountInterval
	pwdFailureCountInterval: $old_pwdFailureCountInterval
	%EOR
}

ldapmodify -x -H "ldap://$ldap_master:$ldap_master_port" -D "$tests_domainadmin_account" -y "$tests_domainadmin_pwdfile" <<-%EOR && undo reset_pwdFailureCountInterval || fail_fast 140 "cannot modify ppolicy"
dn: cn=default,cn=ppolicy,cn=univention,$ldap_base
changetype: modify
replace: pwdMaxFailure
pwdMaxFailure: $new_pwdMaxFailure
-
replace: pwdFailureCountInterval
pwdFailureCountInterval: $new_pwdFailureCountInterval
%EOR

# create test user
test_username=$(user_randomname)
user_create "$test_username" &&
	undo user_remove "$test_username" ||
	fail_fast 140 "cannot create user $test_username"


test_userdn=$(user_dn "$test_username")

#START TEST
section "Test 1: Login with invalid password but stay below pwdMaxFailure ($new_pwdMaxFailure)"
for ((i=1; i<new_pwdMaxFailure; i++)); do
	ldapsearch -x -D "$test_userdn" -w foo >/dev/null 2>&1
done

# $(univention-ldapsearch -LLL uid=$test_username pwdFailureTime | VAL pwdFailureTime | wc -l)
sleep 1

section "Test 2: Wait for automatic reset of pwdFailureTime"
echo "Wait until pwdFailureCountInterval ($new_pwdFailureCountInterval seconds) has passed.." >&2
sleep "$new_pwdFailureCountInterval"

## Do exactly one failed attempt on each user, should not trigger.
ldapsearch -x -D "$test_userdn" -w foo >/dev/null 2>&1
if ! ldapsearch -xLLL -D "$test_userdn" -w univention "uid=$test_username" 1.1 >/dev/null; then
	fail_test 1 "Authentication failure prior to lock"
fi

sleep 1

section "Test 3: Wait for automatic reset and cleanup of pwdFailureTime"
echo "Wait until pwdFailureCountInterval ($new_pwdFailureCountInterval seconds) has passed.." >&2
sleep "$new_pwdFailureCountInterval"
if ! ldapsearch -xLLL -D "$test_userdn" -w univention "uid=$test_username" 1.1 >/dev/null; then
	fail_fast 1 "Authentication failure prior to lock"
fi
test_output=$(univention-ldapsearch -LLL -b "$test_userdn" -s base + | VAL pwdFailureTime)
if [ -n "$test_output" ]; then
	n=$(wc -l <<<"$test_output")
	echo "WARNING: $test_userdn still has $n pwdFailureTime entries, should be 0" >&2
fi

section "Test 4: Login with invalid password exceeding pwdMaxFailure ($new_pwdMaxFailure)"
if ! ldapsearch -xLLL -D "$test_userdn" -w univention "uid=$test_username" 1.1 >/dev/null; then
	fail_fast 1 "Authentication failure prior to lock"
fi

for ((i=1; i<=new_pwdMaxFailure; i++)); do
	ldapsearch -x -D "$test_userdn" -w foo >/dev/null 2>&1
done

wait_for_replication_and_postrun

section "Test 5: Check that all test account is locked now"
locked_state=$(udm-test users/user list --filter "username=$test_username" | sed -n 's/^  locked: //p')
if [ "$locked_state" != '1' ]; then
	fail_test 1 "Account not locked: $locked_state"
	univention-ldapsearch -LLL -b "$test_userdn" -s base +

	echo "DEBUG: attempting to lock manually"
	python3 -m univention.lib.account lock --dn "$test_userdn" --lock-time '20141006192950Z'

	locked_state=$(udm-test users/user list --filter "username=$test_username" | sed -n 's/^  locked: //p')
	echo "DEBUG: result: $locked_state"
fi

remove_ldif () {
	rm ldif.1 ldif.2
}
undo remove_ldif

section "Test 6: Unlock account"
univention-ldapsearch -o ldif-wrap=no -LLLb "$test_userdn" > ldif.1
udm-test users/user modify --dn "$test_userdn" --set unlock=1
univention-ldapsearch -o ldif-wrap=no -LLLb "$test_userdn" > ldif.2

wait_for_replication_and_postrun

echo "Check that all accounts are unlocked again" >&2
if ! ldapsearch -xLLL -D "$test_userdn" -w univention "uid=$test_username" 1.1 >/dev/null; then
	echo 'DEBUG: diff'
	ldiff ldif.1 ldif.2
	echo 'DEBUG: object'
	cat ldif.2
	python3 -c "import ctypes, ctypes.util, hmac, univention.uldap; lib=ctypes.CDLL(ctypes.util.find_library('crypt') or 'libcrypt.so.1'); lib.crypt.argtypes=(ctypes.c_char_p, ctypes.c_char_p); lib.crypt.restype=ctypes.c_char_p; lo=univention.uldap.getMachineConnection(); password=lo.getAttr('${test_userdn}', 'userPassword')[0].decode('ASCII'); password=password.removeprefix('{crypt}'); result=lib.crypt(b'univention', password.encode('ASCII')); print('Password valid?:', result is not None and hmac.compare_digest(result.decode('ASCII'), password))"

	fail_test 1 "Authentication failure after unlock"
fi

exit "$RETVAL"
