]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Adding classes LdapConnectionDict and LdapConfiguration to module pp_admintools.ldap_...
authorFrank Brehm <frank@brehm-online.com>
Tue, 17 May 2022 14:41:18 +0000 (16:41 +0200)
committerFrank Brehm <frank@brehm-online.com>
Tue, 17 May 2022 14:41:18 +0000 (16:41 +0200)
lib/pp_admintools/ldap_config.py

index 718c675356268b259f13c1e145520477f7309c31..ca2c3c611625c423300e2215eb4a7e4e861edd5c 100644 (file)
@@ -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__":