#!/usr/bin/env python3
#
# Author:
#  Tamas Jos (@skelsec)
#

import datetime #, timedelta, timezone
from msldap.ldap_objects.common import MSLDAP_UAC, vn
from msldap.commons.utils import bh_dt_convert

MSADUser_ATTRS = [ 	
	'accountExpires', 'badPasswordTime', 'badPwdCount', 'cn', 'codePage', 
	'countryCode', 'displayName', 'distinguishedName', 'givenName', 'initials', 
	'lastLogoff', 'lastLogon', 'lastLogonTimestamp', 'logonCount', 'name', 'description',
	'objectCategory', 'objectClass', 'objectGUID', 'objectSid', 'primaryGroupID', 
	'pwdLastSet', 'sAMAccountName', 'sAMAccountType', 'sn', 'userAccountControl', 
	'userPrincipalName', 'whenChanged', 'whenCreated','memberOf','member', 'servicePrincipalName',
	'msDS-AllowedToDelegateTo', 'adminCount', 'isDeleted', 'mail', 'title', 'homeDirectory',
	'unixUserPassword',	'userPassword', 'unicodePwd', 'scriptPath', 'sIDHistory'
]
MSADUser_TSV_ATTRS = [
	'sAMAccountName', 'userPrincipalName' ,'canLogon', 'badPasswordTime', 'description',
	'badPwdCount', 'when_pw_change', 'when_pw_expires', 'pwdLastSet', 'lastLogonTimestamp',
	'whenCreated', 'whenChanged', 'member', 'memberOf', 'servicePrincipalName', 
	'objectSid', 'cn', 'UAC_SCRIPT', 'UAC_ACCOUNTDISABLE', 'UAC_LOCKOUT', 'UAC_PASSWD_NOTREQD', 
	'UAC_PASSWD_CANT_CHANGE', 'UAC_ENCRYPTED_TEXT_PASSWORD_ALLOWED', 'UAC_DONT_EXPIRE_PASSWD', 
	'UAC_USE_DES_KEY_ONLY', 'UAC_DONT_REQUIRE_PREAUTH', 'UAC_PASSWORD_EXPIRED', 'adminCount',
	'isDeleted', 'mail', 'title', 'homeDirectory', 'userPassword', 'unixUserPassword', 'unicodePwd', 'scriptPath',
]

class MSADUser:
	def __init__(self):
		## ID
		self.sn = None #str
		self.cn = None #str
		self.distinguishedName = None #dn

		self.initials = None #str
		self.givenName = None #str
		self.displayName = None #str
		self.name = None #str
		self.description = None

		self.objectCategory = None #dn
		self.objectClass = None #str
		self.objectGUID = None #uid
		self.objectSid = None #str
		self.primaryGroupID = None #uid
		self.sAMAccountName = None #str
		self.userPrincipalName = None #str
		self.servicePrincipalName = None #str
		self.mail = None #str
		self.title = None #str
		self.homeDirectory = None #str
		self.userPassword = None #str
		self.unixUserPassword = None #str
		self.unicodePwd = None #str
		self.scriptPath = None #str
		self.sIDHistory = None #list

		## groups
		self.memberOf = None #list
		self.member = None #list

		## times
		self.accountExpires = None #datetime
		self.badPasswordTime = None #datetime
		self.lastLogoff = None #datetime
		self.lastLogon = None #datetime
		self.lastLogonTimestamp = None #datetime
		self.pwdLastSet = None #datetime
		self.whenChanged = None #datetime
		self.whenCreated = None #datetime

		## security
		self.badPwdCount = None #int
		self.logonCount = None #int
		self.sAMAccountType = None #int
		self.userAccountControl = None #UserAccountControl intflag
		self.allowedtodelegateto = None
		self.admincount = None
		self.isDeleted = None
		
		## other
		self.codePage = None #int
		self.countryCode = None #int
		
		## calculated properties
		self.when_pw_change = None #datetime
		self.when_pw_expires = None #datetime
		self.must_change_pw = None #datetime
		self.canLogon = None #bool

	# https://msdn.microsoft.com/en-us/library/cc245739.aspx
	def calc_PasswordMustChange(self, adinfo):
		# Crtieria 1
		flags = [MSLDAP_UAC.DONT_EXPIRE_PASSWD, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT]
		for flag in flags:
			if flag & self.userAccountControl:
				return datetime.datetime.max #never

		#criteria 2
		if self.pwdLastSet == datetime.timedelta():
			return datetime.datetime.min

		if adinfo.maxPwdAge == datetime.timedelta(): #empty timedelta
			return datetime.datetime.max #never

		if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum
			return datetime.datetime.max #never

		return (self.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None)


	# https://msdn.microsoft.com/en-us/library/cc223991.aspx
	def calc_CanLogon(self):
		flags = [MSLDAP_UAC.ACCOUNTDISABLE, MSLDAP_UAC.LOCKOUT, MSLDAP_UAC.SMARTCARD_REQUIRED, MSLDAP_UAC.INTERDOMAIN_TRUST_ACCOUNT, MSLDAP_UAC.WORKSTATION_TRUST_ACCOUNT, MSLDAP_UAC.SERVER_TRUST_ACCOUNT]
		for flag in flags:
			if flag & self.userAccountControl:
				return False
		
		if (not (MSLDAP_UAC.DONT_EXPIRE_PASSWD & self.userAccountControl)) and (self.accountExpires.replace(tzinfo=None) - datetime.datetime.now()).total_seconds() < 0:
			return False

		#
		# TODO: logonHours check!
		#
		
		if self.must_change_pw == datetime.datetime.min:
			#can logon, but must change the password!
			return True

		if (self.must_change_pw - datetime.datetime.now()).total_seconds() < 0:
			return False

		return True

	@staticmethod
	def from_ldap(entry, adinfo = None):
		adi = MSADUser()
		adi.sn = entry['attributes'].get('sn') 
		adi.cn = entry['attributes'].get('cn') 
		adi.distinguishedName = entry['attributes'].get('distinguishedName')
		adi.description = entry['attributes'].get('description')
		adi.initials = entry['attributes'].get('initials')
		adi.givenName = entry['attributes'].get('givenName')
		adi.displayName = entry['attributes'].get('displayName')
		adi.name = entry['attributes'].get('name')
		adi.objectCategory = entry['attributes'].get('objectCategory')
		adi.objectClass = entry['attributes'].get('objectClass')
		adi.objectGUID = entry['attributes'].get('objectGUID')
		adi.objectSid = entry['attributes'].get('objectSid')
		adi.primaryGroupID = entry['attributes'].get('primaryGroupID')
		adi.sAMAccountName = entry['attributes'].get('sAMAccountName')
		adi.userPrincipalName = entry['attributes'].get('userPrincipalName')
		adi.servicePrincipalName = entry['attributes'].get('servicePrincipalName')
		adi.memberOf = entry['attributes'].get('memberOf')
		adi.member = entry['attributes'].get('member')
		adi.accountExpires = entry['attributes'].get('accountExpires')
		adi.badPasswordTime = entry['attributes'].get('badPasswordTime')
		adi.lastLogoff = entry['attributes'].get('lastLogoff')
		adi.lastLogon = entry['attributes'].get('lastLogon')
		adi.lastLogonTimestamp = entry['attributes'].get('lastLogonTimestamp')
		adi.pwdLastSet = entry['attributes'].get('pwdLastSet')
		adi.whenChanged = entry['attributes'].get('whenChanged')
		adi.whenCreated = entry['attributes'].get('whenCreated')
		adi.badPwdCount = entry['attributes'].get('badPwdCount')
		adi.logonCount = entry['attributes'].get('logonCount')
		adi.sAMAccountType = entry['attributes'].get('sAMAccountType')
		adi.codePage = entry['attributes'].get('codePage')
		adi.countryCode = entry['attributes'].get('countryCode')
		adi.isDeleted = entry['attributes'].get('isDeleted')
		adi.mail = entry['attributes'].get('mail')
		adi.title = entry['attributes'].get('title')
		adi.userPassword = entry['attributes'].get('userPassword')
		adi.unixUserPassword = entry['attributes'].get('unixuserpassword')
		adi.unicodePwd = entry['attributes'].get('unicodePwd')
		adi.scriptPath = entry['attributes'].get('scriptPath')
		adi.sIDHistory = entry['attributes'].get('sIDHistory')
		
		adi.allowedtodelegateto = entry['attributes'].get('msDS-AllowedToDelegateTo')
		adi.admincount = entry['attributes'].get('adminCount')
		
		temp = entry['attributes'].get('userAccountControl')
		if temp:
			adi.userAccountControl = MSLDAP_UAC(temp)

			if adinfo:
				adi.when_pw_change = (adi.pwdLastSet - adinfo.minPwdAge).replace(tzinfo=None)
				if adinfo.maxPwdAge.days < -3650: #this is needed, because some ADs have mawPwdAge set for a huge number BUT not to the minimum
					adi.when_pw_expires = datetime.datetime.max
				else:
					adi.when_pw_expires = (adi.pwdLastSet - adinfo.maxPwdAge).replace(tzinfo=None) if adinfo.maxPwdAge != 0 else adi.pwdLastSet
				adi.must_change_pw = adi.calc_PasswordMustChange(adinfo) #datetime
				adi.canLogon = adi.calc_CanLogon() #bool


		return adi

	def to_dict(self):
		t = {}
		t['sn'] = vn(self.sn)
		t['cn'] = vn(self.cn)
		t['distinguishedName'] = vn(self.distinguishedName)
		t['initials'] = vn(self.initials)
		t['givenName'] = vn(self.givenName)
		t['displayName'] = vn(self.displayName)
		t['description'] = vn(self.description)
		t['name'] = vn(self.name)
		t['objectCategory'] = vn(self.objectCategory)
		t['objectClass'] = vn(self.objectClass)
		t['objectGUID'] = vn(self.objectGUID)
		t['objectSid'] = vn(self.objectSid)
		t['primaryGroupID'] = vn(self.primaryGroupID)
		t['sAMAccountName'] = vn(self.sAMAccountName)
		t['userPrincipalName'] = vn(self.userPrincipalName)
		t['servicePrincipalName'] = vn(self.servicePrincipalName)
		t['memberOf'] = vn(self.memberOf)
		t['member'] = vn(self.member)
		t['accountExpires'] = vn(self.accountExpires)
		t['badPasswordTime'] = vn(self.badPasswordTime)
		t['lastLogoff'] = vn(self.lastLogoff)
		t['lastLogon'] = vn(self.lastLogon)
		t['lastLogonTimestamp'] = vn(self.lastLogonTimestamp)
		t['pwdLastSet'] = vn(self.pwdLastSet)
		t['whenChanged'] = vn(self.whenChanged)
		t['whenCreated'] = vn(self.whenCreated)
		t['badPwdCount'] = vn(self.badPwdCount)
		t['logonCount'] = vn(self.logonCount)
		t['sAMAccountType'] = vn(self.sAMAccountType)
		t['codePage'] = vn(self.codePage)
		t['countryCode'] = vn(self.countryCode)
		t['userAccountControl'] = vn(self.userAccountControl)
		t['when_pw_change'] = vn(self.when_pw_change)
		t['when_pw_expires'] = vn(self.when_pw_expires)
		t['must_change_pw'] = vn(self.must_change_pw)
		t['admincount'] = self.admincount
		t['canLogon'] = vn(self.canLogon)
		t['isDeleted'] = vn(self.isDeleted)
		t['mail'] = vn(self.mail)
		t['title'] = vn(self.title)
		t['homeDirectory'] = vn(self.homeDirectory)
		t['userPassword'] = vn(self.userPassword)
		t['unixUserPassword'] = vn(self.unixUserPassword)
		t['unicodePwd'] = vn(self.unicodePwd)
		t['scriptPath'] = vn(self.scriptPath)
		return t

	def uac_to_textflag(self, attr_s):
		if self.userAccountControl is None or self.userAccountControl == '':
			return 'N/A'
		attr = getattr(MSLDAP_UAC, attr_s[4:])
		if self.userAccountControl & attr:
			return True
		return False

	def get_row(self, attrs):
		t = self.to_dict()
		return [str(t.get(x)) if x[:4]!='UAC_' else str(self.uac_to_textflag(x)) for x in attrs]

	def __str__(self):
		t = 'MSADUser\n'
		t += 'sn: %s\n' % self.sn 
		t += 'cn: %s\n' % self.cn 
		t += 'distinguishedName: %s\n' % self.distinguishedName 
		t += 'initials: %s\n' % self.initials 
		t += 'displayName: %s\n' % self.displayName 
		t += 'name: %s\n' % self.name 
		t += 'primaryGroupID: %s\n' % self.primaryGroupID 
		t += 'sAMAccountName: %s\n' % self.sAMAccountName 
		t += 'userPrincipalName: %s\n' % self.userPrincipalName 
		t += 'servicePrincipalName: %s\n' % self.servicePrincipalName 
		t += 'memberOf: %s\n' % self.memberOf 
		t += 'member: %s\n' % self.member 
		t += 'accountExpires: %s\n' % self.accountExpires 
		t += 'badPasswordTime: %s\n' % self.badPasswordTime 
		t += 'lastLogoff: %s\n' % self.lastLogoff 
		t += 'lastLogon: %s\n' % self.lastLogon 
		t += 'lastLogonTimestamp: %s\n' % self.lastLogonTimestamp 
		t += 'pwdLastSet: %s\n' % self.pwdLastSet
		t += 'whenChanged: %s\n' % self.whenChanged
		t += 'whenCreated: %s\n' % self.whenCreated 
		t += 'objectGUID: %s\n' % self.objectGUID 
		t += 'objectSid: %s\n' % self.objectSid 
		t += 'badPwdCount: %s\n' % self.badPwdCount
		t += 'logonCount: %s\n' % self.logonCount
		t += 'sAMAccountType: %s\n' % self.sAMAccountType
		t += 'codePage: %s\n' % self.codePage
		t += 'countryCode: %s\n' % self.countryCode
		t += 'userAccountControl: %s\n' % self.userAccountControl
		t += 'when_pw_change: %s\n' % self.when_pw_change
		t += 'when_pw_expires: %s\n' % self.when_pw_expires
		t += 'must_change_pw: %s\n' % self.must_change_pw
		t += 'admincount: %s\n' % self.admincount
		t += 'canLogon: %s\n' % self.canLogon
		t += 'isDeleted: %s\n' % self.isDeleted
		t += 'mail: %s\n' % self.mail
		t += 'title: %s\n' % self.title
		t += 'homeDirectory: %s\n' % self.homeDirectory
		t += 'userPassword: %s\n' % self.userPassword
		t += 'unixUserPassword: %s\n' % self.unixUserPassword
		t += 'unicodePwd: %s\n' % self.unicodePwd
		t += 'scriptPath: %s\n' % self.scriptPath
		
		return t 


	def to_bh(self, domain):
		uac = self.userAccountControl
		if uac is None:
			uac = MSLDAP_UAC(0)
		sidhistory = []
		if self.sIDHistory is not None:
			for sid in self.sIDHistory:
				sidhistory.append(str(sid))
		return {
			'_allowerdtodelegateto' : self.allowedtodelegateto,
			'Aces' : [], #Post processing
			'AllowedToDelegate' : [], #Post processing
			'ObjectIdentifier' : str(self.objectSid),
			'PrimaryGroupSID' : str(self.objectSid).rsplit('-',1)[0] + '-' + str(self.primaryGroupID),
			"SPNTargets": [], #TODO
			"HasSIDHistory": [], #TODO
			"IsDeleted": bool(self.isDeleted),
			"IsACLProtected": False , # Post processing
			'Properties' : {
				'name' : '%s@%s' % (self.sAMAccountName.upper(), domain.upper()),
				'domain' : domain,
				'domainsid' : str(self.objectSid).rsplit('-',1)[0] , 
				'distinguishedname' : str(self.distinguishedName).upper(), 
				'unconstraineddelegation' : self.uac_to_textflag('UAC_TRUSTED_FOR_DELEGATION'),
				'trustedtoauth' : MSLDAP_UAC.TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION in uac, 
				'passwordnotreqd' : MSLDAP_UAC.PASSWD_NOTREQD in uac, 
				'enabled' : MSLDAP_UAC.ACCOUNTDISABLE not in uac,
				'lastlogon' : bh_dt_convert(self.lastLogon),
				'lastlogontimestamp' : bh_dt_convert(self.lastLogonTimestamp),
				'pwdlastset' : bh_dt_convert(self.pwdLastSet),
				'dontreqpreauth' : MSLDAP_UAC.DONT_REQUIRE_PREAUTH in uac,
				'pwdneverexpires' : MSLDAP_UAC.DONT_EXPIRE_PASSWD in uac,
				'sensitive' : MSLDAP_UAC.NOT_DELEGATED in uac,
				'serviceprincipalnames' : self.servicePrincipalName if self.servicePrincipalName is not None else [],
				'hasspn' : bool(self.servicePrincipalName),
				'displayname' : self.displayName ,
				'email' : self.mail,
				'title' : self.title ,
				'homedirectory' : self.homeDirectory ,
				'description' : self.description ,
				'userpassword' : self.userPassword ,
				'admincount': bool(self.admincount),
				'sidhistory' : sidhistory,
				'whencreated' : bh_dt_convert(self.whenCreated),
				'unixpassword' : self.unixUserPassword ,
				'unicodepassword' : self.unicodePwd ,
				'logonscript' : self.scriptPath,
				'samaccountname' : self.sAMAccountName,
				'highvalue': False, #TODO
				'sfupassword' : None , #TODO have no idea what this is
			}
			
		}
		

		
		
		
		
		
		
		
		
		
		
		
		
		