From 032a56ac5151455002093599e43e7a2ddf9b9fee Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Fri, 6 Jan 2023 16:32:02 +0100 Subject: [PATCH] completing bin/check-ldap-dn-attributes --- .../app/check_ldap_dn_attributes.py | 104 +++++++++++++++++- lib/pp_admintools/app/ldap.py | 18 ++- lib/pp_admintools/config/ldap.py | 29 ++++- 3 files changed, 142 insertions(+), 9 deletions(-) diff --git a/lib/pp_admintools/app/check_ldap_dn_attributes.py b/lib/pp_admintools/app/check_ldap_dn_attributes.py index 27e1a73..52ccbde 100644 --- a/lib/pp_admintools/app/check_ldap_dn_attributes.py +++ b/lib/pp_admintools/app/check_ldap_dn_attributes.py @@ -36,7 +36,7 @@ from .ldap import BaseLdapApplication from ..argparse_actions import NonNegativeItegerOptionAction from ..argparse_actions import LimitedFloatOptionAction -__version__ = '0.1.0' +__version__ = '0.2.0' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -61,6 +61,7 @@ class CheckLdapDnAttributesApplication(BaseLdapApplication): show_cmdline_ldap_timeout = True apply_default_ldap_instance_if_not_given = False show_force_option = False + show_assume_options = False check_attributes = ['member', 'uniqueMember', 'owner', 'seeAlso'] @@ -114,6 +115,107 @@ class CheckLdapDnAttributesApplication(BaseLdapApplication): "({url}) ...").format(inst=self.instance, url=ldap_url) LOG.debug(msg) + self.get_dns_to_check() + self.check_entries() + + # ------------------------------------------------------------------------- + def get_dns_to_check(self): + + ldap_filter = '(|' + ''.join( + map(lambda x: '({}=*)'.format(x), self.check_attributes)) + ')' + + for dn in self.get_all_entry_dns(self.instance, ldap_filter=ldap_filter): + self.all_check_dns.add(dn) + + if self.verbose: + nr = len(self.all_check_dns) + if nr: + msg = ngettext( + "Found one entry to check.", + "Found {} entries to check.".format(nr), nr) + else: + msg = _("Found no to check.") + LOG.debug(msg) + + if self.verbose > 2: + LOG.debug("Found entries to check:\n" + pp(self.all_check_dns.as_list())) + + # ------------------------------------------------------------------------- + def check_entries(self): + + for dn in self.all_check_dns: + self.check_entry(dn) + + if self.failed_entries: + nr = len(self.failed_entries) + msg = ngettext( + "Got an inconsistent entry:", "Got {} inconsistent entries:".format(nr), nr) + LOG.error(msg) + print(pp(self.failed_entries.as_dict(pure=True))) + self.exit(5) + + msg = _("Did not found any inconsistent entries.") + LOG.info(msg) + self.exit(0) + + # ------------------------------------------------------------------------- + def check_entry(self, dn): + + if self.verbose > 1: + LOG.debug(_("Checking DN-like attributes of entry {!r} ...").format(dn)) + + entry = self.get_entry(dn, self.instance, attributes=self.check_attributes) + attribs = self.normalized_attributes(entry) + if self.verbose > 2: + LOG.debug(_("Got attributes:") + '\n' + pp(attribs.as_dict())) + + for attrib in self.check_attributes: + if attrib in attribs: + for ref in attribs[attrib]: + if not self.check_ref_syntax(ref): + if dn not in self.failed_entries: + self.failed_entries[dn] = {} + if attrib not in self.failed_entries[dn]: + self.failed_entries[dn][attrib] = [] + self.failed_entries[dn][attrib].append({ + 'value': ref, + 'what': 'syntax', + }) + continue + if not self.check_ref_existence(ref, self.instance): + if dn not in self.failed_entries: + self.failed_entries[dn] = {} + if attrib not in self.failed_entries[dn]: + self.failed_entries[dn][attrib] = [] + self.failed_entries[dn][attrib].append({ + 'value': ref, + 'what': 'existence', + }) + + # ------------------------------------------------------------------------- + def check_ref_syntax(self, dn): + + if self.re_ldap_dn.match(dn): + return True + return False + + # ------------------------------------------------------------------------- + def check_ref_existence(self, dn, inst): + + if dn in self.checked_ref_dn: + return self.checked_ref_dn[dn] + + if dn == self.connect_info.admin_dn: + self.checked_ref_dn[dn] = True + return True + + if self.get_entry(dn, inst, attributes=['dn']): + self.checked_ref_dn[dn] = True + return True + + self.checked_ref_dn[dn] = False + return False + # ============================================================================= if __name__ == "__main__": diff --git a/lib/pp_admintools/app/ldap.py b/lib/pp_admintools/app/ldap.py index b682143..299909f 100644 --- a/lib/pp_admintools/app/ldap.py +++ b/lib/pp_admintools/app/ldap.py @@ -54,7 +54,7 @@ from ..config.ldap import LdapConnectionInfo, LdapConfiguration # rom ..config.ldap import DEFAULT_PORT_LDAP, DEFAULT_PORT_LDAPS from ..config.ldap import DEFAULT_TIMEOUT -__version__ = '0.10.7' +__version__ = '0.11.0' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -771,7 +771,7 @@ class BaseLdapApplication(BaseDPXApplication): del self.ldap_server[inst] # ------------------------------------------------------------------------- - def get_all_entry_dns(self, inst): + def get_all_entry_dns(self, inst, ldap_filter=None): """Get DNs of all entries in the given LDAP instance and sort them.""" connect_info = self.cfg.ldap_connection[inst] @@ -780,7 +780,11 @@ class BaseLdapApplication(BaseDPXApplication): result = [] attributes = ['dn'] - ldap_filter = '(objectClass=*)' + if not ldap_filter: + ldap_filter = '(objectClass=*)' + + if self.verbose > 1: + LOG.debug(_("Using LDAP filter: {!r}").format(ldap_filter)) req_status, req_result, req_response, req_whatever = ldap.search( search_base=base_dn, search_scope=SUBTREE, search_filter=ldap_filter, @@ -807,7 +811,7 @@ class BaseLdapApplication(BaseDPXApplication): return result # ------------------------------------------------------------------------- - def get_all_entry_dns_hash(self, inst): + def get_all_entry_dns_hash(self, inst, ldap_filter=None): """Get Object classes and DNs of all entries in the given LDAP instance.""" connect_info = self.cfg.ldap_connection[inst] @@ -816,11 +820,15 @@ class BaseLdapApplication(BaseDPXApplication): result = CIDict() attributes = ['objectClass'] - ldap_filter = '(objectClass=*)' + if not ldap_filter: + ldap_filter = '(objectClass=*)' LOG.debug(_("Getting all Entry DNs of LDAP instance {i!r} below {b!r}.").format( i=inst, b=base_dn)) + if self.verbose > 1: + LOG.debug(_("Using LDAP filter: {!r}").format(ldap_filter)) + req_status, req_result, req_response, req_whatever = ldap.search( search_base=base_dn, search_scope=SUBTREE, search_filter=ldap_filter, get_operational_attributes=False, attributes=attributes, diff --git a/lib/pp_admintools/config/ldap.py b/lib/pp_admintools/config/ldap.py index c6ec037..4f56bef 100644 --- a/lib/pp_admintools/config/ldap.py +++ b/lib/pp_admintools/config/ldap.py @@ -32,7 +32,7 @@ from . import VALID_TIERS, DEFAULT_TIER from ..xlate import XLATOR -__version__ = '0.6.2' +__version__ = '0.7.0' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -62,13 +62,16 @@ class LdapConnectionInfo(FbBaseObject): re_ldap_is_admin_key = re.compile(r'^\s*(?:is[_-]*)?admin\s*$', re.IGNORECASE) re_ldap_readonly_key = re.compile(r'^\s*read[_-]*only\s*$', re.IGNORECASE) re_ldap_sync_source_key = re.compile(r'^\s*sync[_-]*source\s*$', re.IGNORECASE) + re_ldap_admin_dn_key = re.compile(r'^\s*admin[_-]*dn\s*$', re.IGNORECASE) + + default_admin_dn = 'cn=admin' # ------------------------------------------------------------------------- def __init__( self, appname=None, verbose=0, version=__version__, base_dir=None, host=None, use_ldaps=False, port=DEFAULT_PORT_LDAP, base_dn=None, bind_dn=None, bind_pw=None, is_admin=None, readonly=None, tier=None, - sync_source=None, initialized=False): + sync_source=None, admin_dn=None, initialized=False): self._host = None self._use_ldaps = False @@ -80,6 +83,7 @@ class LdapConnectionInfo(FbBaseObject): self._readonly = True self._tier = DEFAULT_TIER self._sync_source = None + self._admin_dn = self.default_admin_dn super(LdapConnectionInfo, self).__init__( appname=appname, verbose=verbose, version=version, base_dir=base_dir, @@ -99,6 +103,7 @@ class LdapConnectionInfo(FbBaseObject): self.readonly = readonly self.tier = tier self.sync_source = sync_source + self.admin_dn = admin_dn if initialized: self.initialized = True @@ -120,6 +125,7 @@ class LdapConnectionInfo(FbBaseObject): res['base_dn'] = self.base_dn res['bind_dn'] = self.bind_dn res['bind_pw'] = None + res['admin_dn'] = self.admin_dn res['is_admin'] = self.is_admin res['host'] = self.host res['port'] = self.port @@ -291,6 +297,18 @@ class LdapConnectionInfo(FbBaseObject): return self._sync_source = str(value).strip() + # ----------------------------------------------------------- + @property + def admin_dn(self): + """The DN of the adminitrator of the LDAP instance.""" + return self._admin_dn + + @admin_dn.setter + def admin_dn(self, value): + if value is None or str(value).strip() == '': + return + self._admin_dn = str(value).strip() + # ------------------------------------------------------------------------- def __repr__(self): """Typecasting into a string for reproduction.""" @@ -309,6 +327,7 @@ class LdapConnectionInfo(FbBaseObject): fields.append("readonly={!r}".format(self.readonly)) fields.append("tier={!r}".format(self.tier)) fields.append("sync_source={!r}".format(self.sync_source)) + fields.append("admin_dn={!r}".format(self.admin_dn)) fields.append("initialized={!r}".format(self.initialized)) out += ", ".join(fields) + ")>" @@ -321,7 +340,7 @@ class LdapConnectionInfo(FbBaseObject): appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, host=self.host, use_ldaps=self.use_ldaps, port=self.port, base_dn=self.base_dn, bind_dn=self.bind_dn, bind_pw=self.bind_pw, is_admin=self.is_admin, readonly=self.readonly, tier=self.tier, - sync_source=self.sync_source, initialized=self.initialized) + sync_source=self.sync_source, admin_dn=self.admin_dn, initialized=self.initialized) return new @@ -398,6 +417,10 @@ class LdapConnectionInfo(FbBaseObject): new.sync_source = value continue + if cls.re_ldap_admin_dn_key.match(key): + new.admin_dn = value + continue + msg = _("Unknown LDAP configuration key {key} found in section {sec!r}.").format( key=key, sec=section_name) LOG.error(msg) -- 2.39.5