#!/usr/bin/python
# coding: utf-8
#
# Copyright 2004-2016 Univention GmbH
#
# http://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
# <http://www.gnu.org/licenses/>.

from __future__ import print_function

import re
import datetime
import argparse
import subprocess

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

	parser.add_argument("filter", nargs="?", default="(objectClass=User)",
						help="Search filter (default: \"(objectClass=User)\")")
	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"]

	for option in ("url", "basedn", "scope", "user", "password",
				   "authentication-file", "simple-bind-dn", "kerberos",
				   "krb5-ccache"):
		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"])
	except (OSError, subprocess.CalledProcessError) as error:
		exit("Error calling samba-tool: " + str(error))

	matches = re.findall("^Account lockout duration \(mins\): (\d+)$",
						 output, re.MULTILINE)

	try:
		return int(matches[0])
	except IndexError, ValueError:
		exit("Unable to retrieve lockout-duration via samba-tool")

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

def parse_ldbsearch_entry(entry):
	user = dict()
	for line in entry.splitlines():
		for (key, pattern) in _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

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

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)
	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()
