#!/usr/share/ucs-test/runner python
## desc: ucs-school-reset-password-check
## roles: [domaincontroller_master, domaincontroller_slave]
## tags: [apptest,ucsschool,SKIP-UCSSCHOOL]
## exposure: dangerous
## packages: [ucs-school-umc-users]

import sys
from pprint import pprint

import univention.testing.strings as uts
import univention.testing.ucr as ucr_test
import univention.testing.ucsschool as utu
import univention.testing.utils as utils
from univention.testing.umc import Client
from univention.lib.umc import HTTPError

EXPECT_OK = 1
EXPECT_DISALLOW = 2
EXPECT_LOGIN_FAIL = 3

ucr = ucr_test.UCSTestConfigRegistry()
ucr.load()
is_domaincontroller_slave = ucr.get('server/role') == 'domaincontroller_slave'


class PasswordReset(object):
	connections = {}

	def __init__(self, host, flavor, username, password='univention'):
		self.host = host
		self.flavor = flavor
		self.username = username
		self.password = password
		if self.username not in self.connections:  # caching :)
			self.connections[self.username] = Client(self.host, self.username, self.password)
		self.connection = self.connections[self.username]

	def __repr__(self):
		return 'PasswordReset(%r, %r, %r, %r)' % (self.host, self.flavor, self.username, self.password)

	def test_login(self, user, password):
		print '%r.test_login(%r, %r)' % (self, user, password)
		try:
			client = Client(self.host)
			return client.authenticate(user, password)
		except HTTPError as exc:
			return exc.response

	def change_password(self, target_user, new_password, change_password_on_next_login):
		print '%r.change_password(%r, %r, %r)' % (self, target_user, new_password, change_password_on_next_login)
		options = {
			'userDN': target_user[1],
			'newPassword': new_password,
			'nextLogin': change_password_on_next_login,
		}
		return self.connection.umc_command('schoolusers/password/reset', options, self.flavor).result

	def assert_password_change(self, user, new_password, change_password_on_next_login):
		print '%r.assert_password_change(%r, %r)' % (self, user, change_password_on_next_login)
		try:
			response = self.change_password(user, new_password, change_password_on_next_login=change_password_on_next_login)
		except Exception as exc:
			assert False, 'Could not change password: %s' % (exc,)
		assert isinstance(response, bool) and response is True, 'Failed to reset password: %r' % (response,)

	def assert_login(self, user, old_password, new_password, change_password_on_next_login):
		print '%s.assert_login(%r, old_password=%r, new_password=%r, change_password_on_next_login=%r)' % (self, user, old_password, new_password, change_password_on_next_login)
		login = self.test_login(user, old_password)
		assert 401 == login.status, 'The user could login with the old password: status=%r' % (login.status,)

		login = self.test_login(user, new_password)
		if change_password_on_next_login:
			assert login.status == 401, 'The user could login with new password and chgPwdNextLogin=True: status=%r' % (login.status,)
			assert login.result.get('password_expired'), 'The password is not expired - as expected'
		else:
			assert login.status == 200, 'The user could not login with new password and chgPwdNextLogin=False: status=%r' % (login.status,)

	def assert_password_change_fails(self, user):
		print '%r.assert_password_change_fails(%r)' % (self, user)
		new_password = uts.random_string()
		try:
			response = self.change_password(user, new_password, change_password_on_next_login=True)
		except Exception as exc:
			err = str(exc).lower()
			assert 'forbidden' in err or 'permission denied' in err, 'Exception did not contain "forbidden": %s' % (exc,)
		else:
			assert 'permission denied' == response, 'did not fail: %r' % (response,)


class Error(Exception):
	pass


class TestPasswordReset(object):

	def __init__(self, schoolenv, school, host):
		self.schoolenv = schoolenv
		self.school = school
		self.host = host
		self.errors = []
		testdata = list(self.get_testdata())
		self.changed_password = []
		utils.wait_for_replication_and_postrun()

		# change all passwords
		for test in testdata:
			try:
				if self.test_password_changing(**test):
					self.changed_password.append(test)
			except Error as exc:
				self.errors.append(dict(test, ERROR=str(exc)))

		# wait for all new passwords to be replicated
		utils.wait_for_replication_and_postrun()

		# check if login succeeds with new password and not anymore with old password
		for test in self.changed_password:
			try:
				self.test_umc_authentication(**test)
			except Error as exc:
				self.errors.append(dict(test, ERROR=str(exc)))

		if self.errors:
			pprint(self.errors, sys.stderr, width=120)
			utils.fail('%d errors occurred.' % (len(self.errors),))

	def get_testdata(self):
		for flavor in ('teacher', 'student'):
			for change_password_on_next_login in (True, False):
				for identification, expected_result, actor, target in self.create_test_users(flavor):
					yield {
						'identification': identification,
						'old_password': 'univention',
						'new_password': uts.random_string(),
						'expected_result': expected_result,
						'actor': actor,
						'target': target,
						'change_password_on_next_login': change_password_on_next_login,
						'flavor': flavor,
					}

	def create_test_users(self, flavor):
		# expect_flavor_student_disallow = EXPECT_DISALLOW
		# expect_flavor_teacher_allow = EXPECT_OK if flavor == 'teacher' else EXPECT_DISALLOW
		# expect_flavor_teacher_disallow = EXPECT_DISALLOW
		expect_flavor_student_allow = EXPECT_OK if flavor == 'student' else EXPECT_DISALLOW
		schoolenv, school = self.schoolenv, self.school
		kw = dict(wait_for_replication=False)
		# we have the following roles:
		# global user, domain admin, school admin, teacher, staff, teacher+staff, student
		domain_admin = schoolenv.create_domain_admin(school)
		school_admin = schoolenv.create_school_admin(school, **kw)
		teacher = schoolenv.create_teacher(school, **kw)
		teacher_and_staff = schoolenv.create_teacher_and_staff(school, **kw)

		# school admins, domain admins, teachers, teachers and staff should ...
		for i, actor in enumerate([domain_admin, school_admin, teacher, teacher_and_staff]):
			# TODO: add another user at a different school but with the local school as 'schools'
			# TODO: should a teacher be allowed to reset another's teacher password?

			# ... reset password of students
			expect = expect_flavor_student_allow if actor in (teacher, teacher_and_staff) else EXPECT_OK
			yield '%d-a' % i, expect, actor, schoolenv.create_student(school, **kw)

			# school admins, domain admins should be able to reset the other roles
			# teachers, teachers and staff should not be able to reset passwords of other roles
			expect = EXPECT_OK if actor not in (teacher, teacher_and_staff) else EXPECT_DISALLOW
			yield '%s-b' % i, expect, actor, schoolenv.create_teacher(school, **kw)
			yield '%s-c' % i, expect, actor, schoolenv.create_teacher_and_staff(school, **kw)
			yield '%s-d' % i, expect, actor, schoolenv.create_staff(school, **kw)
			yield '%s-e' % i, expect, actor, schoolenv.create_school_admin(school)
			yield '%s-f' % i, expect, actor, schoolenv.create_domain_admin(school)

			# ... not be able to reset a global user
			expect = EXPECT_DISALLOW if actor != domain_admin else EXPECT_OK
			yield '%s-g' % i, expect, actor, schoolenv.create_global_user()

		# students and staff users should not be able to change passwords
		student = schoolenv.create_student(school)
		staff = schoolenv.create_staff(school)
		for i, (actor, expect) in enumerate([(student, EXPECT_DISALLOW), (staff, EXPECT_LOGIN_FAIL if is_domaincontroller_slave else EXPECT_DISALLOW)]):
			yield '%s-h' % i, expect, actor, schoolenv.create_teacher(school, **kw)
			yield '%s-i' % i, expect, actor, schoolenv.create_teacher_and_staff(school, **kw)
			yield '%s-j' % i, expect, actor, schoolenv.create_staff(school, **kw)
			yield '%s-k' % i, expect, actor, schoolenv.create_school_admin(school, **kw)
			yield '%s-l' % i, expect, actor, schoolenv.create_domain_admin(school)
			yield '%s-m' % i, expect, actor, schoolenv.create_global_user()

	def test_password_changing(self, expected_result, actor, target, change_password_on_next_login, flavor, new_password, old_password, **_):
		try:
			password_reset = PasswordReset(self.host, flavor, actor[0])
		except HTTPError as exc:
			if expected_result == EXPECT_LOGIN_FAIL:
				return False
			raise Error('Authenticating failed')

		try:
			if expected_result == EXPECT_OK:
				print 'Assert %s can change %s password' % (actor[1], target[1])
				password_reset.assert_password_change(target, new_password, change_password_on_next_login)
			elif expected_result == EXPECT_DISALLOW:
				print 'Assert %s can not change %s password' % (actor[1], target[1])
				password_reset.assert_password_change_fails(target)
		except AssertionError as exc:
			raise Error('Changing password: %s' % (exc,))
		else:
			return True

	def test_umc_authentication(self, expected_result, actor, target, change_password_on_next_login, flavor, new_password, old_password, **_):
		# test login with the new password
		password_reset = PasswordReset(self.host, flavor, actor[0])
		try:
			password_reset.assert_login(target, old_password, new_password, change_password_on_next_login)
		except AssertionError as exc:
			raise Error('Login after password change: %s' % (exc,))


def main():
	host = ucr.get('hostname')
	with utu.UCSTestSchool() as schoolenv:
		school, oudn = schoolenv.create_ou(name_edudc=host)
		TestPasswordReset(schoolenv, school, host)


if __name__ == '__main__':
	main()
