#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Like what you see? Join us!
# https://www.univention.com/about-us/careers/vacancies/
#
# Copyright 2016-2022 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/>.

from __future__ import print_function

import re
import datetime
import argparse
import subprocess

_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("--{}={}".format(option, value))

	for option in ("no-pass", "machine-pass", "sign", "encrypt"):
		value = getattr(parsed, option.replace("-", "_"))
		if value:
			arguments.append("--{}".format(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:
		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):
		exit("Unable to retrieve lockout-duration via samba-tool")


def parse_ldbsearch_entry(entry):
	user = dict()
	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:
		exit("Error calling univention-s4search: " + str(error))

	users = list()
	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))
		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()
