From c9a0064df6ec1dde6eef3179945dbd41f6d41323 Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Tue, 17 May 2022 16:41:18 +0200 Subject: [PATCH] Adding classes LdapConnectionDict and LdapConfiguration to module pp_admintools.ldap_config --- lib/pp_admintools/ldap_config.py | 243 ++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 4 deletions(-) diff --git a/lib/pp_admintools/ldap_config.py b/lib/pp_admintools/ldap_config.py index 718c675..ca2c3c6 100644 --- a/lib/pp_admintools/ldap_config.py +++ b/lib/pp_admintools/ldap_config.py @@ -30,7 +30,7 @@ from . import MAX_PORT_NUMBER, DEFAULT_CONFIG_DIR from .xlate import XLATOR -__version__ = '0.1.0' +__version__ = '0.2.0' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -38,6 +38,7 @@ _ = XLATOR.gettext DEFAULT_PORT_LDAP = 389 DEFAULT_PORT_LDAPS = 636 DEFAULT_TIMEOUT = 20 +MAX_TIMEOUT = 3600 # ============================================================================= class LdapConfigError(MultiConfigError): @@ -62,6 +63,7 @@ class LdapConnectionInfo(FbBaseObject): self._port = DEFAULT_PORT_LDAP self._base_dn = None self._bind_dn = None + self._bind_pw = None super(LdapConnectionInfo, self).__init__( appname=appname, verbose=verbose, version=version, base_dir=base_dir, @@ -71,7 +73,8 @@ class LdapConnectionInfo(FbBaseObject): self.use_ldaps = use_ldaps self.port = port self.base_dn = base_dn - self.base_pw = base_pw + self.bind_dn = bind_dn + self.bind_pw = bind_pw if initialized: self.initialized = True @@ -94,10 +97,17 @@ class LdapConnectionInfo(FbBaseObject): res['use_ldaps'] = self.use_ldaps res['port'] = self.port res['base_dn'] = self.base_dn - res['base_pw'] = self.base_pw + res['bind_dn'] = self.bind_dn + res['bind_pw'] = None res['schema'] = self.schema res['url'] = self.url + if self.bind_pw: + if self.verbose > 4: + res['bind_pw'] = self.bind_pw + else: + res['bind_pw'] = '******' + return res # ----------------------------------------------------------- @@ -136,6 +146,20 @@ class LdapConnectionInfo(FbBaseObject): raise LdapConfigError(_("Invalid port {!r} for LDAP server given.").format(value)) self._port = v + # ----------------------------------------------------------- + @property + def base_dn(self): + """The DN used to connect to the LDAP server, anonymous bind is used, if + this DN is empty or None.""" + return self._base_dn + + @base_dn.setter + def base_dn(self, value): + if value is None or str(value).strip() == '': + msg = _("An empty Base DN for LDAP serches is not allowed.") + raise LdapConfigError(msg) + self._base_dn = str(value).strip() + # ----------------------------------------------------------- @property def bind_dn(self): @@ -200,12 +224,223 @@ class LdapConnectionInfo(FbBaseObject): fields.append("use_ldaps={!r}".format(self.use_ldaps)) fields.append("port={!r}".format(self.port)) fields.append("base_dn={!r}".format(self.base_dn)) - fields.append("base_pw={!r}".format(self.base_pw)) + fields.append("bind_dn={!r}".format(self.bind_dn)) + fields.append("bind_pw={!r}".format(self.bind_pw)) fields.append("initialized={!r}".format(self.initialized)) out += ", ".join(fields) + ")>" return out + # ------------------------------------------------------------------------- + def __copy__(self): + + new = self.__class__( + appname=appname, verbose=verbose, base_dir=base_dir, host=host, use_ldaps=use_ldaps, + port=port, base_dn=base_dn, bind_dn=bind_dn, bind_pw=bind_pw, + initialized=initialized) + + return new + + +# ============================================================================= +class LdapConnectionDict(dict, FbGenericBaseObject): + """A dictionary containing LdapConnectionInfo as values and their names as keys.""" + + # ------------------------------------------------------------------------- + def as_dict(self, short=True): + """ + Transforms the elements of the object into a dict + + @param short: don't include local properties in resulting dict. + @type short: bool + + @return: structure as dict + @rtype: dict + """ + + res = super(LdapConnectionDict, self).as_dict(short=short) + + for key in self.keys(): + res[key] = self[key].as_dict(short=short) + + return res + + # ------------------------------------------------------------------------- + def __copy__(self): + + new = self.__class__() + + for key in self.keys(): + new[key] = copy.copy(self.[key]) + + return new + + +# ============================================================================= +class LdapConfiguration(BaseMultiConfig): + """ + A class for providing a configuration for an arbitrary Application working + with one or more LDAP connections, and methods to read it from configuration files. + """ + + default_ldap_server = 'prd-ds.pixelpark.com' + use_ssl_on_default = True + default_ldap_port = DEFAULT_PORT_LDAPS + default_base_dn = 'o=isp' + default_bind_dn = 'uid=readonly,ou=People,o=isp' + + re_ldap_section_w_name = re.compile(r'^\s*ldap\s*:\s*(\S+)') + + re_ldap_host_key = re.compile(r'^\s*(?:host|server)\s*$', re.IGNORECASE) + re_ldap_ldaps_key = re.compile(r'^\s*(?:use[_-]?)?(?:ldaps|ssl)\s*$', re.IGNORECASE) + re_ldap_port_key = re.compile(r'^\s*port\s*$', re.IGNORECASE) + re_ldap_base_dn_key = re.compile(r'^\s*base[_-]*dn\s*$', re.IGNORECASE) + re_ldap_bind_dn_key = re.compile(r'^\s*bind[_-]*dn\s*$', re.IGNORECASE) + re_ldap_bind_pw_key = re.compile(r'^\s*bind[_-]*pw\s*$', re.IGNORECASE) + + # ------------------------------------------------------------------------- + def __init__( + self, appname=None, verbose=0, version=__version__, base_dir=None, + append_appname_to_stems=True, additional_stems=None, config_dir=DEFAULT_CONFIG_DIR, + additional_config_file=None, additional_cfgdirs=None, encoding=DEFAULT_ENCODING, + ensure_privacy=False, use_chardet=True, initialized=False): + + add_stems = [] + if additional_stems: + if is_sequence(additional_stems): + for stem in additional_stems: + add_stems.append(stem) + else: + add_stems.append(additional_stems) + + if 'ldap' not in add_stems: + add_stems.append('ldap') + + self.timeout = DEFAULT_TIMEOUT + + super(LdapConfiguration, self).__init__( + appname=appname, verbose=verbose, version=version, base_dir=base_dir, + append_appname_to_stems=append_appname_to_stems, config_dir=config_dir, + additional_stems=add_stems, additional_config_file=additional_config_file, + additional_cfgdirs=additional_cfgdirs, encoding=encoding, use_chardet=use_chardet, + ensure_privacy=ensure_privacy, initialized=False, + ) + + self.connection = LdapConnectionDict() + + default_connection = LdapConnectionInfo( + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, + host=self.default_ldap_server, use_ldaps=self.use_ssl_on_default, + port=self.default_ldap_port, base_dn=self.default_base_dn, + bind_dn=self.default_bind_dn, initialized=False) + + self.connection['default'] = default_connection + + # ------------------------------------------------------------------------- + def eval_section(self, section_name): + + super(LdapConfiguration, self).eval_section(section_name) + + sn = section_name.lower() + section = self.cfg[section_name] + + if sn == 'ldap': + for key in section.keys(): + sub = section[key] + if key.lower.strip() == 'timeout': + self._eval_timeout(sub) + continue + self._eval_ldap_connection(key, sub) + return + + match = self.re_ldap_section_w_name.match(sn) + if match: + connection_name = match.group(1) + self._eval_ldap_connection(connection_name, section) + + # ------------------------------------------------------------------------- + def _eval_timeout(self, value): + + timeout = DEFAULT_TIMEOUT + + try: + timeout = int(value) + except (ValueError, TypeError) as e: + msg = _("Value {!r} for a timeout is invalid:").format(value) + msg += ' ' + str(e) + LOG.error(msg) + continue + if timeout <= 0 or timeout > MAX_TIMEOUT: + msg = _("Value {!r} for a timeout is invalid:").format(value) + msg += ' ' + str(e) + LOG.error(msg) + continue + + self.timeout = timeout + + # ------------------------------------------------------------------------- + def _eval_ldap_connection(self, connection_name, section): + + connection = LdapConnectionInfo( + appname=self.appname, verbose=self.verbose, base_dir=self.base_dir, + initialized=False) + + section_name = "ldap:" + connection_name + msg_invalid = _("Invalid value {val!r} in section {sec!r} for a LDAP {what}.") + + for key in section.keys(): + + value = section[key] + + if self.re_ldap_host_key.match(key) + if value.strip(): + connection.host = value + else: + msg = msg_invalid.format(val=value, sec=section_name, what='host') + LOG.error(msg) + continue + + if self.re_ldap_ldaps_key.match(key): + connection.use_ldaps = value + continue + + if self.re_ldap_port_key.match(key): + port = DEFAULT_PORT_LDAP + try: + port = int(value) + except (ValueError, TypeError) as e: + msg = msg_invalid.format(val=value, sec=section_name, what='port') + msg += ' ' + str(e) + LOG.error(msg) + continue + if port <= 0 or port > MAX_PORT_NUMBER: + msg = msg_invalid.format(val=value, sec=section_name, what='port') + msg += ' ' + str(e) + LOG.error(msg) + continue + connection.port = port + + if self.re_ldap_base_dn_key.match(key): + if value.strip(): + connection.base_dn = value + else: + msg = msg_invalid.format(val=value, sec=section_name, what='base_dn') + LOG.error(msg) + continue + + if self.re_ldap_bind_dn_key.match(key): + connection.bind_dn = value + continue + + if self.re_ldap_bind_pw.match(key): + connection.bind_pw = value + continue + + msg = _("Unknown LDAP configuration key found in section {!r}.").format(section_name) + LOG.error(msg) + + self.connection[connection_name] = connection + # ============================================================================= if __name__ == "__main__": -- 2.39.5