#!/usr/bin/env python3
# -----------------------------------------------------------------------------
#                        SYMAS OPEN SOURCE LICENSE
#
# Copyright (c) 2008-2024 Symas Corporation
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
#    * Redistributions of source code must retain the above copyright
#      notice, this list of conditions and the following disclaimer.
#    * Redistributions in binary form must reproduce the above copyright
#      notice, this list of conditions and the following disclaimer in
#      the documentation and/or other materials provided with the
#      distribution.
#    * Neither the name of the Symas Corporation nor the names of its
#      contributors may be used to endorse or promote products derived
#      from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
# -----------------------------------------------------------------------------
# ==============================================================================
# cn-config-to-slapd-conf.py
# Author: gnoe
# Updated: 07/17/2024
# Purpose: Get slapd.conf from cn=config
# ==============================================================================
import re
import os
import sys
import socket
import getpass
from typing import Pattern
import ldap.dn
import argparse
import subprocess
import traceback
import os.path as p
from ldif import LDIFParser
from datetime import datetime as dt

bse: Pattern[str] = re.compile(r'\\')
numstrip: Pattern[str] = re.compile(r'^{\d+}')
cfg_root = '/opt/symas/etc/openldap'
section_sep = '########################################################################################################'
config_dir = '/opt/symas/etc/openldap/slapd.d'
schema_path = None
schema_output_dir = None
tmpfile = '/tmp/tmp-ldap-config.ldif'
errfile = '/tmp/ldap-x-error.txt'
maskpass = False
rewrite_schema = False
output_dir = ""

# Operational Attributes
OP_ATTRS = ['entryUUID', 'entryCSN', 'creatorsName', 'createTimestamp',
            'modifiersName', 'modifyTimestamp', 'structuralObjectClass',
            'objectClass', 'cn', 'olcConfigFile', 'olcConfigDir', 'contextCSN']

# Attributes to skip
SKIP_ATTRS = ['name', 'olcVariantAlternativeAttribute', 'olcVariantAlternativeEntryPattern']

# Sections to insert a newline above
SECTIONS = ['olcOverlay', 'olcModulePath', 'olcVariantEntry', 'olcDatamorphEnum', 'olcDatamorphInteger']

# Config attribute map
attrmap = {
    'olcADremapDNmap': 'adremap-dnmap',
    'olcADremapDowncase': 'adremap-downcase',
    'olcAGattrSet': 'autogroup-attrset',
    'olcAGmemberOfAd': 'autogroup-memberof-ad',
    'olcAccess': 'access',
    'olcAccessLogBase': 'logbase',
    'olcAccessLogDB': 'logdb',
    'olcAccessLogOld': 'logold',
    'olcAccessLogOldAttr': 'logoldattr',
    'olcAccessLogOps': 'logops',
    'olcAccessLogPurge': 'logpurge',
    'olcAccessLogSuccess': 'logsuccess',
    'olcAddContentAcl': 'add_content_acl',
    'olcAliasMapping': 'alias_attribute',
    'olcAllows': 'allow',
    'olcArgsFile': 'argsfile',
    'olcAttributeOptions': 'attributeoptions',
    'olcAttributeTypes': 'attributetype',
    'olcAuditlogFile': 'auditlog',
    'olcAuditlogNoWrap': 'nowrap',
    'olcAuthIDRewrite': 'authid-rewrite',
    'olcAuthzPolicy': 'authz-policy',
    'olcAuthzRegexp': 'authz-regexp',
    'olcAutoCADays': 'caDays',
    'olcAutoCAKeyBits': 'serverKeybits',
    'olcAutoCAKeybits': 'caKeybits',
    'olcAutoCAlocalDN': 'localDN',
    'olcAutoCAserverClass': 'serverClass',
    'olcAutoCAserverDays': 'serverDays',
    'olcAutoCAserverKeybits': 'serverKeybits',
    'olcAutoCAuserClass': 'userClass',
    'olcAutoCAuserDays': 'userDays',
    'olcAutoCAuserKeybits': 'userKeybits',
    'olcAutoGroupAttrSet': 'autogroup-attrset',
    'olcAutoGroupMemberOfAd': 'autogroup-memberof-ad',
    'olcBkMdbIdlExp': 'idlexp',
    'olcChainCacheURI': 'chain-cache-uri',
    'olcChainMaxReferralDepth': 'chain-max-depth',
    'olcChainReturnError': 'chain-return-error',
    'olcChainingBehavior': 'chain-chaining',
    'olcCloakAttribute': 'cloak-attr',
    'olcCollectInfo': 'collectinfo',
    'olcConcurrency': 'concurrency',
    'olcConnMaxPending': 'conn_max_pending',
    'olcConnMaxPendingAuth': 'conn_max_pending_auth',
    'olcConstraintAttribute': 'constraint_attribute',
    'olcDDSdefaultTtl': 'dds-default-ttl',
    'olcDDSinterval': 'dds-interval',
    'olcDDSmaxDynamicObjects': 'dds-max-dynamicObjects',
    'olcDDSmaxTtl': 'dds-max-ttl',
    'olcDDSminTtl': 'dds-min-ttl',
    'olcDDSstate': 'dds-state',
    'olcDDStolerance': 'dds-tolerance',
    'olcDGAttrPair': 'attrpair',
    'olcDatabase': 'database',
    'olcDatamorphAttribute': 'datamorph_attribute',
    'olcDatamorphEnum': 'datamorph enum',
    'olcDatamorphEnumValue': 'datamorph_value',
    'olcDatamorphInteger': 'datamorph int',
    'olcDatamorphIntegerBytes': 'datamorph_size',
    'olcDatamorphIntegerLowerBound': 'datamorph_lower_bound',
    'olcDatamorphIntegerSigned': 'datamorph_signed',
    'olcDatamorphIntegerUpperBound': 'datamorph_upper_bound',
    'olcDatamorphValue': 'datamorph_value',
    'olcDbACLBind': 'acl-bind',
    'olcDbBindAllowed': 'bind',
    'olcDbBindTimeout': 'bind-timeout',
    'olcDbCancel': 'cancel',
    'olcDbChaseReferrals': 'chase-referrals',
    'olcDbCheckpoint': 'checkpoint',
    'olcDbClientPr': 'client-pr',
    'olcDbConnTtl': 'conn-ttl',
    'olcDbConnectionPoolMax': 'conn-pool-max',
    'olcDbDefaultTarget': 'default-target',
    'olcDbDirectory': 'directory',
    'olcDbDnCacheTtl': 'dncache-ttl',
    'olcDbDoSearch': 'dosearch',
    'olcDbEnvFlags': 'envflags',
    'olcDbFilter': 'filter',
    'olcDbHost': 'dbhost',
    'olcDbIDAssertAuthzFrom': 'idassert-authzFrom',
    'olcDbIDAssertBind': 'idassert-bind',
    'olcDbIDAssertPassThru': 'idassert-passThru',
    'olcDbIdleTimeout': 'idle-timeout',
    'olcDbIndex': 'index',
    'olcDbKeepalive': 'keepalive',
    'olcDbMap': 'map',
    'olcDbMaxEntrySize': 'maxentrysize',
    'olcDbMaxPendingOps': 'max-pending-ops',
    'olcDbMaxReaders': 'maxreaders',
    'olcDbMaxSize': 'maxsize',
    'olcDbMaxTargetConns': 'max-target-conns',
    'olcDbMaxTimeoutOps': 'max-timeout-ops',
    'olcDbMode': 'mode',
    'olcDbMultival': 'multival',
    'olcDbMultivalHi': 'multival_hi',
    'olcDbMultivalLo': 'multival_lo',
    'olcDbName': 'dbname',
    'olcDbNetworkTimeout': 'network-timeout',
    'olcDbNoRefs': 'norefs',
    'olcDbNoSync': 'dbnosync',
    'olcDbNoUndefFilter': 'noundeffilter',
    'olcDbNretries': 'nretries',
    'olcDbOnErr': 'onerr',
    'olcDbPass': 'dbpasswd',
    'olcDbProtocolVersion': 'protocol-version',
    'olcDbProxyWhoAmI': 'proxy-whoami',
    'olcDbPseudoRootBindDefer': 'pseudoroot-bind-defer',
    'olcDbQuarantine': 'quarantine',
    'olcDbRebindAsUser': 'rebind-as-user',
    'olcDbRemoveUnknownSchema': 'omit-unknown-schema',
    'olcDbRewrite': 'rewrite',
    'olcDbRtxnSize': 'rtxnsize',
    'olcDbSearchStack': 'searchstack',
    'olcDbSessionTrackingRequest': 'session-tracking-request',
    'olcDbSingleConn': 'single-conn',
    'olcDbSocketExtensions': 'extensions',
    'olcDbSocketPath': 'socketpath',
    'olcDbStartTLS': 'tls',
    'olcDbSubtreeExclude': 'subtree-exclude',
    'olcDbSubtreeInclude': 'subtree-include',
    'olcDbSuffixMassage': 'suffixmassage',
    'olcDbTFSupport': 't-f-support',
    'olcDbTcpUserTimeout': 'tcp-user-timeout',
    'olcDbTimeout': 'timeout',
    'olcDbURI': 'uri',
    'olcDbUseTemporaryConn': 'use-temporary-conn',
    'olcDbUser': 'dbuser',
    'olcDefaultSearchBase': 'defaultsearchbase',
    'olcDisabled': 'disabled',
    'olcDisallows': 'disallow',
    'olcDitContentRules': 'ditcontentrule',
    'olcDlAttrSet': 'dynlist-attrset',
    'olcDynGroupAttrPair': 'attrpair',
    'olcDynListAttrSet': 'dynlist-attrset',
    'olcDynListSimple': 'dynlist-simple',
    'olcExtraAttrs': 'extra_attrs',
    'olcGentleHUP': 'gentlehup',
    'olcHidden': 'hidden',
    'olcHomedirArchivePath': 'homedir-archive-path',
    'olcHomedirDeleteStyle': 'homedir-delete-style',
    'olcHomedirRegexp': 'homedir-regexp',
    'olcIDLcache': 'idlcache',
    'olcIdleTimeout': 'idletimeout',
    'olcInclude': 'include',
    'olcIndexHash64': 'index_hash64',
    'olcIndexIntLen': 'index_intlen',
    'olcIndexSubstrAnyLen': 'index_substr_any_len',
    'olcIndexSubstrAnyStep': 'index_substr_any_step',
    'olcIndexSubstrIfMaxLen': 'index_substr_if_maxlen',
    'olcIndexSubstrIfMinLen': 'index_substr_if_minlen',
    'olcLastBind': 'lastbind',
    'olcLastBindForwardUpdates': 'lastbind_forward_updates',
    'olcLastBindPrecision': 'lastbind-precision',
    'olcLastMod': 'lastmod',
    'olcLdapSyntaxes': 'ldapsyntax',
    'olcLimits': 'limits',
    'olcListenerThreads': 'listener-threads',
    'olcLocalSSF': 'localSSF',
    'olcLogFile': 'logfile',
    'olcLogFileFormat': 'logfile-format',
    'olcLogFileOnly': 'logfile-only',
    'olcLogFileRotate': 'logfile-rotate',
    'olcLogLevel': 'loglevel',
    'olcMaxDerefDepth': 'maxderefdepth',
    'olcMaxFilterDepth': 'maxfilterdepth',
    'olcMemberOfAddCheck': 'memberof-addcheck',
    'olcMemberOfDN': 'memberof-dn',
    'olcMemberOfDangling': 'memberof-dangling',
    'olcMemberOfDanglingError': 'memberof-dangling-error',
    'olcMemberOfGroupOC': 'memberof-group-oc',
    'olcMemberOfMemberAD': 'memberof-member-ad',
    'olcMemberOfMemberOfAD': 'memberof-memberof-ad',
    'olcMemberOfRefInt': 'memberof-refint',
    'olcMemberOfReverse': 'memberof-reverse',
    'olcMinimumUidNumber': 'homedir-min-uidnumber',
    'olcMirrorMode': 'mirrormode',
    'olcModuleLoad': 'moduleload',
    'olcModulePath': 'modulepath',
    'olcMonitoring': 'monitoring',
    'olcMultiProvider': 'multiprovider',
    'olcNestGroupBase': 'nestgroup-base',
    'olcNestGroupFlags': 'nestgroup-flags',
    'olcNestGroupMember': 'nestgroup-member',
    'olcNestGroupMemberOf': 'nestgroup-memberof',
    'olcNssMap': 'nssov-map',
    'olcNssPam': 'nssoc-pam',
    'olcNssPamDefHost': 'nssov-pam-defhost',
    'olcNssPamGroupAD': 'nssov-pam-group-ad',
    'olcNssPamGroupDN': 'nssov-pam-group-dn',
    'olcNssPamMaxUid': 'nssov-pam-max-uid',
    'olcNssPamMinUid': 'nssov-pam-min-uid',
    'olcNssPamPwdProhibitMsg': 'nssov-pam-password-prohibit-msg',
    'olcNssPamSession': 'nssov-pam-session',
    'olcNssPamTemplate': 'nssov-pam-template',
    'olcNssPamTemplateAD': 'nssov-pam-template-ad',
    'olcNssSsd': 'nssov-ssd',
    'olcObjectClasses': 'objectclass',
    'olcObjectIdentifier': 'objectidentifier',
    'olcOvSocketDNpat': 'socdnpat',
    'olcOvSocketOps': 'sockops',
    'olcOvSocketResps': 'sockresps',
    'olcOverlay': 'overlay',
    'olcPPolicyDefault': 'ppolicy_default',
    'olcPPolicyDisableWrite': 'ppolicy_disable_write',
    'olcPPolicyForwardUpdates': 'ppolicy_forward_updates',
    'olcPPolicyHashCleartext': 'ppolicy_hash_cleartext',
    'olcPPolicySendNetscapeControls': 'ppolicy_send_netscape_controls',
    'olcPPolicyUseLockout': 'ppolicy_use_lockout',
    'olcPamPwdmgrDn': 'nssov-pam-pwdmgr-dn',
    'olcPamPwdmgrPwd': 'nssov-pam-pwdmgr-pwd',
    'olcPasswdFile': 'file',
    'olcPasswordCryptSaltFormat': 'password-crypt-salt-format',
    'olcPasswordHash': 'password-hash',
    'olcPcache': 'pcache',
    'olcPcacheAttrset': 'pcacheAttrset',
    'olcPcacheBind': 'pcacheBind',
    'olcPcacheMaxQueries': 'pcacheMaxQueries',
    'olcPcacheOffline': 'pcacheOffline',
    'olcPcachePersist': 'pcachePersist',
    'olcPcachePosition': 'pcachePosition',
    'olcPcacheTemplate': 'pcacheTemplate',
    'olcPcacheValidate': 'pcacheValidate',
    'olcPerlFilterSearchResults': 'filterSearchResults',
    'olcPerlModule': 'perlModule',
    'olcPerlModuleConfig': 'perlModuleConfig',
    'olcPerlModulePath': 'perlModulePath',
    'olcPidFile': 'pidfile',
    'olcPluginLogFile': 'pluginlog',
    'olcProxyAttrset': 'pcacheAttrSet',
    'olcProxyCache': 'pcache',
    'olcProxyCacheQueries': 'pcacheMaxQueries',
    'olcProxyCacheTemplate': 'pcacheTemplate',
    'olcProxyCheckCacheability': 'pcacheValidate',
    'olcProxySaveQueries': 'pcachePersist',
    'olcRBACAdminDn': 'rbac-admin',
    'olcRBACAdminPwd': 'rbac-pwd',
    'olcRBACDefaultAuditBaseDn': 'rbac-default-audit-base-dn',
    'olcRBACDefaultPermissionsBaseDn': 'rbac-default-permissions-base-dn',
    'olcRBACDefaultRolesBaseDn': 'rbac-default-roles-base-dn',
    'olcRBACDefaultSessionsBaseDn': 'rbac-default-sessions-base-dn',
    'olcRBACDefaultTenantId': 'rbac-default-tenant-id',
    'olcRBACDefaultUsersBaseDn': 'rbac-default-users-base-dn',
    'olcRBACSessionAdminDn': 'rbac-session-admin',
    'olcRBACSessionAdminPwd': 'rbac-session-admin-pwd',
    'olcRBACTenant': 'rbac-tenant',
    'olcReadOnly': 'readonly',
    'olcReferral': 'referral',
    'olcRefintAttribute': 'refint_attributes',
    'olcRefintModifiersName': 'refint_modifiersname',
    'olcRefintNothing': 'refint_nothing',
    'olcRelay': 'relay',
    'olcRemoteAuthDNAttribute': 'remoteauth_dn_attribute',
    'olcRemoteAuthDefaultDomain': 'remoteauth_default_domain',
    'olcRemoteAuthDefaultRealm': 'remoteauth_default_realm',
    'olcRemoteAuthDomainAttribute': 'remoteauth_domain_attribute',
    'olcRemoteAuthMapping': 'remoteauth_mapping',
    'olcRemoteAuthRetryCount': 'remoteauth_retry_count',
    'olcRemoteAuthStore': 'remoteauth_store',
    'olcRemoteAuthTLS': 'remoteauth_tls',
    'olcRemoteAuthTLSPeerkeyHash': 'remoteauth_tls_peerkey_hash',
    'olcReplica': 'replica',
    'olcReplicaArgsFile': 'replica-argsfile',
    'olcReplicaPidFile': 'replica-pidfile',
    'olcReplicationInterval': 'replicationInterval',
    'olcReplogFile': 'replogfile',
    'olcRequires': 'require',
    'olcRestrict': 'restrict',
    'olcRetcodeInDir': 'retcode-indir',
    'olcRetcodeItem': 'retcode-item',
    'olcRetcodeParent': 'retcode-parent',
    'olcRetcodeSleep': 'retcode-sleep',
    'olcReverseLookup': 'reverse-lookup',
    'olcRootDN': 'rootdn',
    'olcRootDSE': 'rootDSE',
    'olcRootPW': 'rootpw',
    'olcRwmDropUnrequested': 'rwm-drop-unrequested-attrs',
    'olcRwmMap': 'rwm-map',
    'olcRwmNormalizeMapped': 'rwm-normalize-mapped-attrs',
    'olcRwmRewrite': 'rwm-rewrite',
    'olcRwmTFSupport': 'rwm-t-f-support',
    'olcSaslAuxprops': 'sasl-auxprops',
    'olcSaslAuxpropsDontUseCopy': 'sasl-auxprops-dontusecopy',
    'olcSaslAuxpropsDontUseCopyIgnore': 'sasl-auxprops-dontusecopy-ignore',
    'olcSaslCBinding': 'sasl-cbinding',
    'olcSaslHost': 'sasl-host',
    'olcSaslRealm': 'sasl-realm',
    'olcSaslSecProps': 'sasl-secprops',
    'olcSchemaDN': 'schemadn',
    'olcSecurity': 'security',
    'olcServerID': 'serverID',
    'olcSizeLimit': 'sizelimit',
    'olcSkeletonPath': 'homedir-skeleton-path',
    'olcSmbK5PwdCanChange': 'smbk5pwd-can-change',
    'olcSmbK5PwdEnable': 'smbk5pwd-enable',
    'olcSmbK5PwdMustChange': 'smbk5pwd-must-change',
    'olcSockbufMaxIncoming': 'sockbuf_max_incoming',
    'olcSockbufMaxIncomingAuth': 'sockbuf_max_incoming_auth',
    'olcSortVals': 'sortvals',
    'olcSpCheckpoint': 'syncprov-checkpoint',
    'olcSpNoPresent': 'syncprov-nopresent',
    'olcSpReloadHint': 'syncprov-reloadhint',
    'olcSpSessionlog': 'syncprov-sessionlog',
    'olcSpSessionlogSource': 'syncprov-sessionlog-source',
    'olcSqlAliasingKeyword': 'aliasing_keyword',
    'olcSqlAliasingQuote': 'aliasing_quote',
    'olcSqlAllowOrphans': 'allow_orphans',
    'olcSqlAtQuery': 'at_query',
    'olcSqlAutocommit': 'autocommit',
    'olcSqlBaseObject': 'baseObject',
    'olcSqlCheckSchema': 'check_schema',
    'olcSqlChildrenCond': 'children_cond',
    'olcSqlConcatPattern': 'concat_pattern',
    'olcSqlCreateNeedsSelect': 'create_needs_select',
    'olcSqlDelEntryStmt': 'delentry_stmt',
    'olcSqlDelObjclassesStmt': 'delobjectclasses_stmt',
    'olcSqlDnMatchCond': 'dn_match_cond',
    'olcSqlFailIfNoMapping': 'fail_if_no_mapping',
    'olcSqlFetchAllAttrs': 'fetch_all_attrs',
    'olcSqlFetchAttrs': 'fetch_attrs',
    'olcSqlHasLDAPinfoDnRu': 'has_ldapinfo_dn_ru',
    'olcSqlIdQuery': 'id_query',
    'olcSqlInsEntryStmt': 'isentry_stmt',
    'olcSqlLayer': 'sqllayer',
    'olcSqlOcQuery': 'oc_query',
    'olcSqlRenEntryStmt': 'renentry_stmt',
    'olcSqlStrcastFunc': 'strcast_func',
    'olcSqlSubtreeCond': 'subtree_cond',
    'olcSqlUpperFunc': 'upper_func',
    'olcSqlUpperNeedsCast': 'upper_needs_cast',
    'olcSqlUseSubtreeShortcut': 'use_subtree_shortcut',
    'olcSssVlvMax': 'sssvlv-max',
    'olcSssVlvMaxKeys': 'sssvlv-maxkeys',
    'olcSssVlvMaxPerConn': 'sssvlv-maxperconn',
    'olcSubordinate': 'subordinate',
    'olcSuffix': 'suffix',
    'olcSyncUseSubentry': 'sync_use_subentry',
    'olcSyncrepl': 'syncrepl',
    'olcTCPBuffer': 'tcp-buffer',
    'olcTLSCACertificateFile': 'TLSCACertificateFile',
    'olcTLSCACertificatePath': 'TLSCACertificatePath',
    'olcTLSCRLCheck': 'TLSCRLCheck',
    'olcTLSCRLFile': 'TLSCRLFile',
    'olcTLSCertificateFile': 'TLSCertificateFile',
    'olcTLSCertificateKeyFile': 'TLSCertificateKeyFIle',
    'olcTLSCipherSuite': 'TLSCipherSuite',
    'olcTLSDHParamFile': 'TLSDHParamFile',
    'olcTLSECName': 'TLSECName',
    'olcTLSProtocolMin': 'TLSProtocolMin',
    'olcTLSRandFile': 'TLSRandFile',
    'olcTLSVerifyClient': 'TLSVerifyClient',
    'olcThreadQueues': 'threadqueues',
    'olcThreads': 'threads',
    'olcTimeLimit': 'timelimit',
    'olcToolThreads': 'tool-threads',
    'olcTranslucentBindLocal': 'translucent_bind_local',
    'olcTranslucentLocal': 'translucent_local',
    'olcTranslucentNoGlue': 'translucent_no_glue',
    'olcTranslucentPwModLocal': 'translucent_pwmod_local',
    'olcTranslucentRemote': 'translucent_remote',
    'olcTranslucentStrict': 'translucent_strict',
    'olcUniqueAttribute': 'unique_attributes',
    'olcUniqueBase': 'unique_base',
    'olcUniqueIgnore': 'unique_ignore',
    'olcUniqueStrict': 'unique_strict',
    'olcUniqueURI': 'unique-uri',
    'olcUpdateDN': 'updatedn',
    'olcUpdateRef': 'updateref',
    'olcValSortAttr': 'valsort-attr',
    'olcVariantEntry': 'variantDN',
    'olcVariantPassReplication': 'passReplication',
    'olcVariantRegex': 'variantRegex',
    'olcWriteTimeout': 'writetimeout',
    'olcVariantVariantAttribute': ''
}


# Names of schemas built into Symas OpenLDAP
builtin_schema = ['collective', 'connexitor', 'corba', 'core', 'cosine', 'dsee', 'duaconf', 'dyngroup', 'edupermission',
                  'eduperson', 'inetorgperson', 'java', 'krb5-kdc', 'misc', 'msuser', 'namedobject', 'nis', 'openldap',
                  'pmi', 'qmail', 'rfc2307bis', 'samba', 'samba3']


# Add backslashes in authz-regexp
def get_authzregexp(authz_str):
    authzstr = clean_value(authz_str)
    return 'authz-regexp {}'.format(bse.sub(r'\\\\', authzstr))


# Exclude frontend config and add newline separator for databases
def get_database_name(db_name):
    dbname = clean_value(db_name)
    if dbname != '{-1}frontend':
        return '\n{}\ndatabase {}'.format(section_sep, dbname)
    else:
        return '\n{}\n# Frontend Config'.format(section_sep)


# Encode values and remove attribute numbering
def clean_value(attr_value):
    val = attr_value.decode('UTF-8')
    return numstrip.sub('', val)


# Get config using ldapsearch
def get_from_ldapsearch(uri, binddn, userpw, srchcmd):
    tmp_ldif = open(tmpfile, 'w')
    tmp_err = open(errfile, 'w')
    proc = subprocess.Popen([srchcmd, '-xLLL',
        '-H', uri, '-D', binddn, '-w', userpw, '-b', 'cn=config',
        '-o', 'ldif-wrap=no'], stdout=tmp_ldif, stderr=tmp_err)
    proc.communicate()
    tmp_ldif.close()
    tmp_err.close()
    if proc.returncode != 0:
        tmp_err = open(errfile, 'r')
        print("ERROR: ldapsearch for cn=config failed:")
        for ln in tmp_err.readlines():
            print('\t' + ln)
        print('\n')
        tmp_err.close()
        if p.exists(errfile):
            os.remove(errfile)
        if p.exists(tmpfile):
            os.remove(tmpfile)
        exit(1)


# Get config using slapcat
def get_from_slapcat(slapcatpath, configpath):
    tmp_ldif = open(tmpfile, 'w')
    tmp_err = open(errfile, 'w')
    proc = subprocess.Popen([slapcatpath, '-n', '0', '-F', configpath, '-l', tmpfile, '-o', 'ldif-wrap=no'],
        stdout=tmp_ldif, stderr=tmp_err)
    proc.communicate()
    tmp_ldif.close()
    tmp_err.close()
    if proc.returncode != 0:
        tmp_err = open(errfile, 'r')
        print("ERROR: ldapsearch for cn=config failed:")
        for ln in tmp_err.readlines():
            print('\t' + ln, file=sys.stderr)
        print('\n', file=sys.stderr)
        tmp_err.close()
        if p.exists(errfile):
            os.remove(errfile)
        if p.exists(tmpfile):
            os.remove(tmpfile)
        exit(1)

    print()


# Parser class
class MyLDIF(LDIFParser):

    def __init__(self, inputfile, output, outputfile):
        LDIFParser.__init__(self, inputfile)
        self.writer = output
        self.outputfile = outputfile

    def handle(self, dn, entry):

        if b'olcSchemaConfig' in entry['objectClass']:
            entrycn = clean_value(entry['cn'][0])
            if entrycn != 'schema':
                sch_file_name = p.join(schema_path, entrycn + '.schema')
                if entrycn in builtin_schema and not rewrite_schema:
                    if p.exists(sch_file_name):
                        self.outputfile.writelines('include {}\n'.format(sch_file_name))
                    else:
                        raise ValueError(
                            '\nERROR: Unable to create include line for the builtin {} schema!\n       The schema file "{}" does not exist.\n       Conversion failed.\n'.format(entrycn, sch_file_name))
                else:
                    self.outputfile.writelines('include {}\n'.format(sch_file_name))
                    #if not p.isdir(schema_path):
                    #    os.makedirs(schema_path)
                    sch_file_name = p.join(schema_output_dir, entrycn + '.schema')
                    with open(sch_file_name, 'w') as sch_file:
                        for attr in entry:
                            if attr in ['olcAttributeTypes', 'olcObjectClasses']:
                                for val in entry[attr]:
                                    sch_file.writelines('{} {}\n'.format(attrmap[attr], clean_value(val)))

        elif b'olcModuleList' in entry['objectClass']:
            self.outputfile.writelines('\n')
            for attr in entry:
                if attr in ['olcModulePath', 'olcModuleLoad']:
                    for val in entry[attr]:
                        self.outputfile.writelines('{} {}'.format(attrmap[attr], clean_value(val)) + '\n')
            self.outputfile.writelines('\n')

        elif b'olcVariantRegex' in entry['objectClass']:
            self.outputfile.writelines(
                '\nvariantRegex {}\n'.format(
                    clean_value(entry['olcVariantEntryRegex'][0])
                )
            )
            
        elif b'olcVariantAttribute' in entry['objectClass']:
            self.outputfile.writelines(
                '\nvariantSpec {} {} {}\n'.format(
                    clean_value(entry['olcVariantVariantAttribute'][0]), clean_value(entry['olcVariantAlternativeAttribute'][0]), clean_value(entry['olcVariantAlternativeEntry'][0])
                )
            )

        elif b'olcVariantAttributePattern' in entry['objectClass']:
            self.outputfile.writelines(
                'variantRegexSpec {} {} {}\n'.format(
                    clean_value(entry['olcVariantVariantAttribute'][0]), clean_value(entry['olcVariantAlternativeAttribute'][0]), clean_value(entry['olcVariantAlternativeEntryPattern'][0])
                )
            )
            
        elif b'olcDatamorphEnumValue' in entry['objectClass']:
            self.outputfile.writelines(
                'datamorph_value {} {}\n'.format(
                    clean_value(entry['olcDatamorphIndex'][0]), clean_value(entry['olcDatamorphValue'][0])
                )
            )

        else:
            for attr in entry:
                if attr not in OP_ATTRS and attr not in SKIP_ATTRS:
                    for val in entry[attr]:
                        if attr == 'olcAuthzRegexp':
                            self.outputfile.writelines(get_authzregexp(val) + '\n')
                        elif attr == 'olcDatabase':
                            self.outputfile.writelines(get_database_name(val) + '\n')
                        elif attr in ['olcRootPW', 'olcPamPwdmgrPwd', 'olcRBACAdminPwd', 'olcRBACSessionAdminPwd'] and maskpass:
                            self.outputfile.writelines(attrmap[attr] + ' ###############\n')
                        elif attr == 'olcSyncrepl' and maskpass:
                            self.outputfile.writelines(attrmap[attr] + ' ' + re.sub(r"credentials=[\'\"]?.+[\'\"]? ", "credentials=##### ", clean_value(val)) + '\n')
                        elif attr == 'olcDatamorphAttribute':
                            self.outputfile.writelines('\ndatamorph {} {}\n'.format(
                                'enum' if clean_value(entry['objectClass'][0]) == 'olcDatamorphEnum' else 'int',
                                clean_value(val))
                            )
                        elif attr == 'olcAccess':
                            self.outputfile.writelines(attrmap[attr] + ' ' + re.sub(r" by ", "\n  by ", clean_value(val)) + '\n')
                        else:
                            self.outputfile.writelines('{}{} {}'.format(
                                '\n' if attr in SECTIONS else '',
                                attrmap[attr],
                                clean_value(val) + '\n'))


def show_arg_help(pv, fmtstr, *args):
    print('\n' + fmtstr + '\n', *args, file=sys.stderr)
    pv.print_help(file=sys.stderr)


def cleanup():
    if p.exists(tmpfile):
        os.remove(tmpfile)
    if p.exists(errfile):
        os.remove(errfile)


if __name__ == '__main__':
    v = sys.version_info
    if v.major < 3 or v.minor < 6:
        print(
            "\n#### This script is being run with Python {:d}.{:d}. ####\n#### The minimum Python version to run this script is 3.6. ####\n".format(
                v.major, v.minor))
        sys.exit(1)
    infile = None
    outfile = None
    bind_dn = None
    tmppass = None

    parser = argparse.ArgumentParser()
    parser.description = "This utility will converts an LDIF formatted slapd configuration (cn=config)\ninto a static slapd configuration (slapd.conf)."
    inputgroup = parser.add_mutually_exclusive_group(required=True)
    inputgroup.required = True
    inputgroup.add_argument('-s', '--use-slapcat', action='store_true', help='Use slapcat to convert from cn=config to slapd.conf.')
    inputgroup.add_argument('-l', '--use-ldif', action='store_true', help='Use an existing config ldif to convert from cn=config to slapd.conf')
    inputgroup.add_argument('-S', '--use-ldapsearch', action='store_true', help='Use ldapsearch to convert from cn=config to slapd.con.')
    parser.add_argument('-f', '--ldif-file', required=False, metavar='ldif-file', type=str, help='The cn=config ldif file to convert.')
    parser.add_argument('-F', '--config-dir', required=False, metavar='config-dir', type=str, default='/opt/symas/etc/openldap/slapd.d', help='Path to an alternate slapd configuration "cn=config" directory.')
    parser.add_argument('-R', '--config-root', required=False, metavar='config-root', type=str, help='Path to the OpenLDAP configuration root directory. Defaults to /opt/symas/etc/openldap.')
    parser.add_argument('-o', '--output-dir', required=False, metavar='output-dir', type=str, help='The directory to write the file to. Defaults to the current working directory.', default='./')
    parser.add_argument('-O', '--schema-output-dir', required=False, metavar='schema-output-dir', type=str, help='The directory to write the schema files to. Defaults to the package schema directory.')
    parser.add_argument('-p', '--slapcat-path', required=False, metavar='slapcat-path', type=str, default='/opt/symas/sbin/slapcat', help='An alternate path to the slapcat utility. Defaults to /opt/symas/bin/slapcat or /opt/symas/sbin/slapcat.')
    parser.add_argument('-c', '--ldapsearch-cmd', required=False, metavar='ldapsearch-cmd', type=str, default='/opt/symas/bin/ldapsearch', help='An alternate path to the ldapsearch command. Defaults to /opt/symas/bin.')
    credentialgroup = parser.add_mutually_exclusive_group()
    credentialgroup.add_argument('-w', '--bind-password', required=False, metavar='bind-password', type=str, help='A bind password to use when using the -S (ldapsearch) option.')
    credentialgroup.add_argument('-W', '--bind-password-prompt', required=False, metavar='bind-password-prompt',  help='A bind password to use when using the -S (ldapsearch) option.')
    parser.add_argument('-D', '--bind-dn', required=False, metavar='bind-dn', type=str, default='', help='A user DN to use when using the -S (ldapsearch) option.')
    parser.add_argument('-H', '--ldap-host', required=False, metavar='ldap-host', type=str, default='ldap:///', help='The host URI to use when using the -s (ldapsearch) option. Defaults to ldap:///')
    parser.add_argument('-M', '--mask-passwd', action='store_true', help='Mask passwords in olcRootPW and olcSyncrepl.')
    parser.add_argument('-r', '--rewrite-schema', required=False, action='store_true',  default=False, help='Rewrite builtin schema files.')
    al = parser.parse_args()

    maskpass = al.mask_passwd

    # -R --config-root
    if al.config_root:
        if p.exists(al.config_root):
            cfg_root = al.config_root
        else:
            show_arg_help(parser, 'ERROR: The configuration root directory "{}" in -D (--config-root) does not exist.', al.config_root)
    else:
        if not p.exists(cfg_root):
            show_arg_help(parser, 'ERROR: The default configuration root directory "{}" does not exist.', cfg_root)

    # -S --use-ldapsearch
    if al.use_ldapsearch:
        if not p.exists(al.ldapsearch_cmd):
            show_arg_help(parser, 'ERROR: Invalid path "{}" to the ldapsearch command.', al.ldapsearch_cmd)
        if not al.ldap_host or not al.bind_dn:
            arghelp = None
            if al.ldap_host == '':
                arghelp = 'ERROR: -H or --ldap-host required for -S option.'
            if al.bind_dn == '':
                arghelp = 'ERROR: -D or --bind-dn required for -S option.'
            show_arg_help(parser, arghelp)
        else:
            if ldap.dn.is_dn(al.bind_dn):
                bind_dn = al.bind_dn
            else:
                show_arg_help(parser, 'ERROR: The DN "{}" in -D (--bind-dn) is invalid.'.format(al.bind_dn))
        if al.bind_password:
            tmppass = al.bind_password
        elif al.bind_password_prompt:
            tmppass = getpass.getpass("Bind Password: ")
        else:
            show_arg_help(parser, 'Error: "-w", "--bind-passwd", "-W" or "--bind-passwd-prompt" is required')
        get_from_ldapsearch(al.ldap_host, bind_dn, tmppass, al.ldapsearch_cmd)
        infile = tmpfile

    # -l --use-ldif
    elif al.use_ldif:
        if not p.exists(al.ldif_file):
            show_arg_help(parser, 'ERROR: The cn=config ldif file "{}" (-l | --input-ldif) does not exist.', al.ldif_file)
        infile = al.ldif_file

    # -S --use-slapcat
    elif al.use_slapcat:

        # -p --slapcat-path
        if not p.exists(al.slapcat_path):
            if p.exists('/opt/symas/bin/slapcat'):
                al.slapcat_path = '/opt/symas/bin/slapcat'
            elif p.exists('/opt/symas/sbin/slapcat'):
                al.slapcat_path = '/opt/symas/sbin/slapcat'
            else:
                show_arg_help(parser, 'ERROR: Path to slapcat "{}" is invalid (-p | --slapcat-path).', al.slapcat_path)

        # -F --config-dir
        if not p.isdir(al.config_dir):
            show_arg_help(parser, 'ERROR: Path to cn=config directory "{}" is invalid (-F | --config-dir).', al.config_dir)

        if not os.access(al.config_dir, os.R_OK):
            show_arg_help(parser, 'ERROR: You are not allowed to read the cn=config directory "{}" (-F | --config-dir).', al.config_dir)

        get_from_slapcat(al.slapcat_path, al.config_dir)
        infile = tmpfile
    else:
        parser.print_help()

    # -w --rewrite-schema
    rewrite_schema = al.rewrite_schema

    # Set schema path
    schema_path = p.join(cfg_root, 'schema')
    if not p.exists(schema_path):
        print("OpenLDAP not installed")
        exit(1)

    # Set schema output path
    if not al.schema_output_dir:
        schema_output_dir = schema_path
    else:
        schema_output_dir = al.schema_output_dir
        if not p.exists(schema_output_dir):
            os.makedirs(schema_output_dir, exist_ok=True)

    stampdate = dt.now()
    outfile = p.join(al.output_dir, stampdate.strftime('slapd-config-conversion-%Y%m%d-%H%M%S.conf'))
    with open(outfile, 'w') as outf:
        outf.writelines('\n'.join([section_sep, '# Host: ' + socket.gethostname(), '# Created: ' + stampdate.strftime('%Y-%m-%d %H:%M:%S'), section_sep, '\n']))

    try:
        parser = MyLDIF(open(infile, 'rb'), sys.stdout, open(outfile, 'a'))
        parser.parse()
    except Exception as ex:
        print("Exception: {} -- {}".format(type(ex), ex))
        traceback.print_exception(ex)

    cleanup()

