#!/usr/bin/python3
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2016-2024 Univention GmbH
#
# https://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
# <https://www.gnu.org/licenses/>.


import argparse
import datetime
import re
import subprocess
import sys


_LDBSEARCH_PATTERNS = {
    "dn": re.compile("dn: (.*)"),
    "name": re.compile("sAMAccountName: (.*)"),
    "lockout": re.compile(r"lockoutTime: (\d+)"),
}

_NT_UNIX_DELTA = (datetime.datetime(1970, 1, 1) - datetime.datetime(1601, 1, 1)).total_seconds()


def parse_arguments():
    parser = argparse.ArgumentParser()

    parser.add_argument("filter", nargs="?", default="(objectClass=User)", help="Search filter (default: %(default)r")
    parser.add_argument("--debug", action="store_true", help="Print debug univention-s4search call to stdout and exit")
    parser.add_argument("--verbose", action="store_true", help="Print remaining time per user")

    general = parser.add_argument_group("General options (as in ldbsearch)")
    general.add_argument("-H", "--url", help="database URL")
    general.add_argument("-b", "--basedn", metavar="DN", help="base DN")
    general.add_argument("-s", "--scope", help="search scope")

    auth = parser.add_argument_group("Authentication options (as in ldbsearch)")
    auth.add_argument("-U", "--user", help="Set the network username")
    auth.add_argument("-N", "--no-pass", action="store_true", help="Don't ask for a password")
    auth.add_argument("--password", metavar="STRING", help="Password")
    auth.add_argument("-A", "--authentication-file", metavar="FILE", help="Get the credentials from a file")
    auth.add_argument("-P", "--machine-pass", action="store_true", help="Use stored machine account password")
    auth.add_argument("--simple-bind-dn", metavar="STRING", help="DN to use for a simple bind")
    auth.add_argument("-k", "--kerberos", metavar="STRING", choices=["yes", "no"], help="Use Kerbos")
    auth.add_argument("--krb5-ccache", metavar="STRING", help="Credentials cache location for Kerberos")
    auth.add_argument("-S", "--sign", action="store_true", help="Sign connection to prevent modificationi in transit")
    auth.add_argument("-e", "--encrypt", action="store_true", help="Encrypt connection for privacy")

    return parser.parse_args()


def build_search_command(parsed):
    arguments = ["univention-s4search"]
    options = ("url", "basedn", "scope", "user", "password", "authentication-file", "simple-bind-dn", "kerberos", "krb5-ccache")

    for option in options:
        value = getattr(parsed, option.replace("-", "_"))
        if value:
            arguments.append(f"--{option}={value}")

    for option in ("no-pass", "machine-pass", "sign", "encrypt"):
        value = getattr(parsed, option.replace("-", "_"))
        if value:
            arguments.append(f"--{option}")

    arguments.extend([parsed.filter, "lockoutTime", "samAccountName"])
    return arguments


def get_lockout_duration():
    try:
        output = subprocess.check_output(["samba-tool", "domain", "passwordsettings", "show"]).decode('UTF-8', 'replace')
    except (OSError, subprocess.CalledProcessError) as error:
        sys.exit("Error calling samba-tool: " + str(error))

    matches = re.findall(r"^Account lockout duration \(mins\): (\d+)$", output, re.MULTILINE)
    try:
        return int(matches[0]) or 30
    except (IndexError, ValueError):
        sys.exit("Unable to retrieve lockout-duration via samba-tool")


def parse_ldbsearch_entry(entry):
    user = {}
    for line in entry.splitlines():
        for (key, pattern) in list(_LDBSEARCH_PATTERNS.items()):
            match = pattern.search(line)
            if match is None:
                continue
            value = match.group(1)
            if value is None:
                continue
            user[key] = value
    return user


def nt_to_unix(nt_timestamp):
    in_seconds = nt_timestamp // 10000000
    if in_seconds > 0:
        return in_seconds - _NT_UNIX_DELTA
    return in_seconds


def get_remaining_locktime(lockout_duration, lockout_time):
    if lockout_duration == -1:
        return -1
    if lockout_time == 0:
        return 0
    delta = datetime.datetime.utcnow() - datetime.datetime.utcfromtimestamp(lockout_time)
    locked_out_seconds = delta.total_seconds()
    remaining = int(lockout_duration * 60 - locked_out_seconds)
    return max(0, remaining)


def get_users(search_command, lockout_duration):
    try:
        output = subprocess.check_output(search_command).decode('UTF-8', 'replace')
    except (OSError, subprocess.CalledProcessError) as error:
        sys.exit("Error calling univention-s4search: " + str(error))

    users = []
    for entry in output.split("\n\n"):
        user = parse_ldbsearch_entry(entry)
        if "dn" in user and "name" in user and "lockout" in user:
            lockout = nt_to_unix(int(user["lockout"]))
            user["lockout"] = lockout
            user["remaining"] = get_remaining_locktime(lockout_duration, lockout)
            users.append(user)
    return users


def main():
    arguments = parse_arguments()
    search_command = build_search_command(arguments)

    if arguments.debug:
        print(" ".join(search_command))
        sys.exit(0)

    lockout_duration = get_lockout_duration()
    users = get_users(search_command, lockout_duration)

    lockout = [user for user in users if user["remaining"] != 0]

    template = "{} remaining: {} seconds" if arguments.verbose else "{}"
    for user in lockout:
        print(template.format(user["name"], user["remaining"]))


if __name__ == "__main__":
    main()
