#!/usr/bin/python3
#
# Univention AD Connector
#  Grant List and Read access to "CN=Deleted Objects"
#
# SPDX-FileCopyrightText: 2014-2025 Univention GmbH
# SPDX-License-Identifier: AGPL-3.0-only


import sys
import time
from argparse import ArgumentParser

import ldap
import ldap.controls
from ldap.filter import filter_format
from samba.dcerpc import security
from samba.ndr import ndr_pack, ndr_unpack

import univention.config_registry
import univention.connector.ad


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