#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# Univention AD Connector
#  Grant List and Read access to "CN=Deleted Objects"
#
# Copyright 2014-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 sys
import time
from argparse import ArgumentParser

import ldap
import univention
import univention.connector
import univention.connector.ad
import univention.config_registry
from samba.dcerpc import security
from samba.ndr import ndr_unpack, ndr_pack
import ldap.controls
from ldap.filter import filter_format

LDAP_SERVER_SHOW_DELETED_OID = '1.2.840.113556.1.4.417'
LDAP_SERVER_SD_FLAGS_OID = '1.2.840.113556.1.4.801'


class AD_DSACL_modifier():

	''' Provides methods for modifying the nTSecurityDescriptor of CN=Deleted Objects
		NOTE: copied from univention-management-console-module-adtakeover
	'''

	def __init__(self, ucr, binddn, bindpwd):
		self.ucr = ucr
		self.ad_ldap_binddn = binddn
		self.ad_ldap_bindpwd = bindpwd
		self.ad_connect()

	def ad_connect(self):
		''' stripped down univention.connector.ad.main
			difference: pass "bindpwd" directly instead of "bindpw" filename
		'''

		poll_sleep = int(self.ucr['%s/ad/poll/sleep' % CONFIGBASENAME])
		while True:
			try:
				self.ad = univention.connector.ad.ad.main(ucr, CONFIGBASENAME, ad_ldap_binddn=self.ad_ldap_binddn, ad_ldap_bindpw=self.ad_ldap_bindpwd)
				self.ad.init_ldap_connections()
				return
			except ldap.SERVER_DOWN:
				print("Warning: Can't initialize LDAP-Connections, wait...")
				sys.stdout.flush()
				time.sleep(poll_sleep)

	def get_nTSecurityDescriptor_of_Deleted_Objects(self):
		ctrls = []
		ctrls.append(ldap.controls.LDAPControl(LDAP_SERVER_SHOW_DELETED_OID, criticality=0))

		result = self.ad.lo_ad.lo.search_ext_s("CN=Deleted Objects,%s" % (self.ad.ad_ldap_base,), ldap.SCOPE_BASE, "(objectClass=*)", attrlist=["nTSecurityDescriptor", ], serverctrls=ctrls)
		if result and len(result) > 0 and result[0] and len(result[0]) > 0 and result[0][0]:  # no referral, so we've got a valid result
			self.deleted_objects_dn = result[0][0]
			obj = result[0][1]
			desc_ndr = obj.get("nTSecurityDescriptor", [None])[0]
		else:
			print("ERROR: CN=Deleted Objects not found in AD")
			sys.exit(1)

		desc_sddl = None
		if desc_ndr:
			desc = ndr_unpack(security.descriptor, desc_ndr)
			desc_sddl = desc.as_sddl()

		return desc_sddl

	def initialize_nTSecurityDescriptor_of_Deleted_Objects(self):
		# Probably only the O:DAG:SY fields matter here, because
		# we use LDAP_SERVER_SD_FLAGS_OID with OWNER_SECURITY_INFORMATION only.
		default_desc_sddl = 'O:DAG:SYD:PAI(A;;RPLC;;;BA)(A;;RPWPCCDCLCRCWOWDSDSW;;;SY)S:AI(OU;CIIOIDSA;WP;f30e3bbe-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)(OU;CIIOIDSA;WP;f30e3bbf-9ff0-11d1-b603-0000f80367c1;bf967aa5-0de6-11d0-a285-00aa003049e2;WD)'

		desc = security.descriptor.from_sddl(default_desc_sddl, security.dom_sid(self.ad.ad_sid))
		desc_ndr = ndr_pack(desc)
		modify_ctrls = []
		modify_ctrls.append(ldap.controls.LDAPControl(LDAP_SERVER_SHOW_DELETED_OID, criticality=0))
		modify_ctrls.append(ldap.controls.LDAPControl(LDAP_SERVER_SD_FLAGS_OID, criticality=1, encodedControlValue=b'0\x03\x02\x01\x01'))  # this is a 1 encoded as ASN1 SEQUENCE { INTEGER }
		self.ad.lo_ad.lo.modify_ext_s(self.deleted_objects_dn, [(ldap.MOD_REPLACE, 'nTSecurityDescriptor', desc_ndr)], serverctrls=modify_ctrls)

	def grant_DSACL_LCRP_to_local_system(self):
		ldap_filter = filter_format("(sAMAccountName=%s$)", (self.ucr["hostname"],))
		result = self.ad.lo_ad.lo.search_ext_s(self.ad.ad_ldap_base, ldap.SCOPE_SUBTREE, ldap_filter, attrlist=["objectSid", ])
		if result and len(result) > 0 and result[0] and len(result[0]) > 0 and result[0][0]:  # no referral, so we've got a valid result
			obj = result[0][1]
			objectSid_ndr = obj.get("objectSid", [None])[0]
			machine_sid = ndr_unpack(security.dom_sid, objectSid_ndr)
		else:
			print("ERROR: sAMAccountName %s$ not found in AD" % (self.ucr["hostname"],))
			sys.exit(1)

		new_ace = '(A;;RPLC;;;%s)' % (machine_sid,)

		desc_sddl = self.get_nTSecurityDescriptor_of_Deleted_Objects()
		if not desc_sddl:
			self.initialize_nTSecurityDescriptor_of_Deleted_Objects()
			desc_sddl = self.get_nTSecurityDescriptor_of_Deleted_Objects()
			if not desc_sddl:
				print("ERROR: Failed to initialize nTSecurityDescriptor for CN=Deleted Objects in AD")
				sys.exit(1)

		if new_ace in desc_sddl:
			print("INFO: DSACL of Deleted Objects is already OK.")
			sys.exit(0)

		if desc_sddl.find("(") >= 0:
			desc_sddl = desc_sddl[:desc_sddl.index("(")] + new_ace + desc_sddl[desc_sddl.index("("):]
		else:
			desc_sddl = desc_sddl + new_ace
		desc = security.descriptor.from_sddl(desc_sddl, security.dom_sid(self.ad.ad_sid))
		desc_ndr = ndr_pack(desc)
		ctrls = []
		ctrls.append(ldap.controls.LDAPControl(LDAP_SERVER_SHOW_DELETED_OID, criticality=0))
		self.ad.lo_ad.lo.modify_ext_s(self.deleted_objects_dn, [(ldap.MOD_REPLACE, 'nTSecurityDescriptor', desc_ndr)], serverctrls=ctrls)
		print("INFO: DSACL of Deleted Objects adjusted.")


if __name__ == "__main__":
	parser = ArgumentParser()
	parser.add_argument("--configbasename", metavar="CONFIGBASENAME", default="connector")
	parser.add_argument("--binddn", metavar="BINDDN")
	parser.add_argument("--bindpwd", metavar="BINDPWD")
	parser.add_argument("--bindpwdfile", metavar="BINDPWDFILE")
	options = parser.parse_args()

	CONFIGBASENAME = options.configbasename
	if options.bindpwdfile:
		with open(options.bindpwdfile) as fd:
			options.bindpwd = fd.readline().strip()

	ucr = univention.config_registry.ConfigRegistry()
	ucr.load()

	ad = AD_DSACL_modifier(ucr, options.binddn, options.bindpwd)
	ad.grant_DSACL_LCRP_to_local_system()
