#!/usr/share/ucs-test/runner python
## desc: Test if a domain user account is locked out and freed.
## exposure: dangerous
## packages: [univention-samba4, univention-s4-connector]
## bugs: [35898]
## roles:
## - domaincontroller_master

from sys import exit
from time import sleep
from subprocess import Popen, PIPE

import univention.testing.utils as utils
from univention.testing.codes import TestCodes
from univention.config_registry import ConfigRegistry
from univention.testing.strings import random_username

LOCKOUT_DURATION = 1  # duration of lockout in minutes
LOCKOUT_THRESHOLD = 3  # amount of auth. attempts allowed before the lock out
TEST_USER_PASS = 'Univention1'

UCR = ConfigRegistry()

admin_username = ''
admin_password = ''
test_username = ''
hostname = ''


def create_and_run_process(cmd, stdout=PIPE):
    """
    Creates a process as a Popen instance with a given 'cmd'
    and 'communicates' with it. Returns (stdout, stderr).
    """
    proc = Popen(cmd, stdout=stdout, stderr=PIPE)
    return proc.communicate()


def try_to_authenticate(password):
    """
    Tries to authenticate a 'test_username' user with a given 'password'
    using smbclient and execute an 'ls'. Returns (stdout, stderr).
    """
    print("\nTrying to authenticate a '%s' user with a password '%s'"
          % (test_username, password))

    cmd = ("smbclient", "//" + hostname + "/" + test_username,
           "-U", test_username + "%" + password, "--kerberos",
           "-t", "20",  # 20 seconds timeout per operation.
           "-c", "ls")

    return create_and_run_process(cmd)


def set_reset_lockout_settings(lock_duration, lock_threshold):
    """
    Sets the lockout settings to a given values.
    """
    print("\nSetting account lockout settings to the following values "
          "(lockout duration = %s min; attempts before lockout = %s):"
          % (lock_duration, lock_threshold))

    cmd = ("samba-tool", "domain", "passwordsettings", "set",
           "--account-lockout-duration=" + lock_duration,
           "--account-lockout-threshold=" + lock_threshold,
           "--username=" + admin_username,
           "--password=" + admin_password)

    stdout, stderr = create_and_run_process(cmd)
    if stderr:
        # just print the stderr, as ofter there are warnings occur,
        # for instance (Bug #34056)
        print("An error/warning occured while trying to set/reset "
              "account lockout settings via samba-tool command '%s':\n%r"
              % (" ".join(cmd), stderr))
    if stdout:
        print stdout


def create_delete_test_user(should_exist):
    """
    Creates or deletes the 'test_username' depending on the given argument
    via 'samba-tool'. User password is TEST_USER_PASS.
    """
    if should_exist is True:
        print("\nCreating a test user with a username '%s'" % test_username)
        cmd = ("samba-tool", "user", 'create', test_username, TEST_USER_PASS)
    elif should_exist is False:
        print("\nDeleting a test user with a username '%s'" % test_username)
        cmd = ("samba-tool", "user", 'delete', test_username)
    else:
        utils.fail("The given 'should_exist'='%s' value is not supported. Pass"
                   " 'True' to create a user or 'False' to delete."
                   % should_exist)

    cmd += ("--username=" + admin_username,
            "--password=" + admin_password)

    stdout, stderr = create_and_run_process(cmd)
    if stderr:
        # just print the stderr, as ofter there are warnings occur,
        # for instance (Bug #34056)
        print("An error/warning occured while trying to create or remove a "
              "user with a username '%s' via command: '%s'.\nSTDERR: '%s'."
              % (test_username, " ".join(cmd), stderr))
    if stdout:
        print stdout


def check_no_errors_present_in_output(stdout, stderr):
    """
    Fails the test if there are signs of errors found in the given
    'stdout' or 'stderr'.
    """
    complete_output = (stdout + stderr)

    if 'NT_STATUS_ACCOUNT_LOCKED_OUT' in complete_output:
        utils.fail("\nThe 'NT_STATUS_ACCOUNT_LOCKED_OUT' error was found in "
                   "the output.\nSTDOUT: '%s'. STDERR: '%s'."
                   % (stdout, stderr))

    elif 'NT_STATUS_LOGON_FAILURE' in complete_output:
        utils.fail("\nThe 'NT_STATUS_LOGON_FAILURE' error was found in "
                   "the output.\nSTDOUT: '%s'. STDERR: '%s'."
                   % (stdout, stderr))

    elif 'NT_STATUS_OK' in complete_output:
        # the (only one possible) success status was found
        # (http://msdn.microsoft.com/en-us/library/ee441884.aspx)
        return

    elif 'NT_STATUS_' in complete_output:
        # all the rest status options are signs of errors
        utils.fail("\nAn error occured. \nSTDOUT: '%s'. STDERR: '%s'"
                   % (stdout, stderr))


def check_error_present_in_output(stdout, stderr):
    """
    Fails the test if there is no 'NT_STATUS_ACCOUNT_LOCKED_OUT' error in
    the given stdout or stderr.
    """
    if 'NT_STATUS_ACCOUNT_LOCKED_OUT' not in (stdout + stderr):
        utils.fail("The 'NT_STATUS_ACCOUNT_LOCKED_OUT' error could "
                   "not be found in the STDOUT: '%s' or STDERR: '%s'. "
                   "The account lockout may not work."
                   % (stdout, stderr))


if __name__ == '__main__':
    print "\nObtaining settings for the test from the UCR"
    UCR.load()

    admin_username = UCR.get('tests/domainadmin/account')
    admin_password = UCR.get('tests/domainadmin/pwd')
    hostname = UCR.get('ldap/server/name')

    if not all((admin_username, admin_password, hostname)):
        print("Failed to obtain Administrator credentials or a hostname "
              "for the test from UCR. Skipping the test.")
        exit(TestCodes.REASON_INSTALL)

    test_username = 'ucs_test_samba4_user_' + random_username(4)
    try:
        # create a user for the test with 'test_username'
        create_delete_test_user(True)

        # change lockout settings to the test values
        set_reset_lockout_settings(str(LOCKOUT_DURATION),
                                   str(LOCKOUT_THRESHOLD))

        sleep(30)  # wait a bit

        # try to authenticate the test user with a valid password
        print("\nTrying to authenticate '%s' user with a correct password '%s'"
              % (test_username, TEST_USER_PASS))
        stdout, stderr = try_to_authenticate(TEST_USER_PASS)
        check_no_errors_present_in_output(stdout, stderr)

        # try to lock the test user out with a random password
        print("\nTrying to lock the '%s' user out attempting to authenticate "
              "with a random password %d times:"
              % (test_username, LOCKOUT_THRESHOLD + 1))
        for attempt in range(LOCKOUT_THRESHOLD + 1):
            stdout, stderr = try_to_authenticate("Foo" + random_username() + "123")
        check_error_present_in_output(stdout, stderr)

        # check that user is locked and that even a correct password won't work
        print("\nTrying to authenticate '%s' user with a correct password '%s'"
              " on a locked out account:" % (test_username, TEST_USER_PASS))
        stdout, stderr = try_to_authenticate(TEST_USER_PASS)
        check_error_present_in_output(stdout, stderr)

        # wait for unlocking
        print("\nWaiting for '%s' user account to be unlocked after the "
              "lock out timeout (%d min) expires:"
              % (test_username, LOCKOUT_DURATION))
        sleep(LOCKOUT_DURATION * 60 + 30)  # convert to seconds + some extra

        # try to authenticate again (should be no errors)
        print("\nTrying to authenticate '%s' user with a correct password '%s'"
              " after the lock time has expired"
              % (test_username, TEST_USER_PASS))
        stdout, stderr = try_to_authenticate(TEST_USER_PASS)
        check_no_errors_present_in_output(stdout, stderr)
    finally:
        # reset the lockout settings to default values
        set_reset_lockout_settings('default', 'default')

        # remove the test user
        create_delete_test_user(False)
