From caab7d371237cd50cb857af0031327292586221a Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 7 Sep 2025 19:03:56 +0200 Subject: [PATCH 01/14] LDAP auth: load SSL/TLS config unconditionally Currently it is not used by _login2(), but it does not hurt to have it available. It is a preparation for supporting encrypted connections in _login2(). --- radicale/auth/ldap.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 94640f33..0974b024 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -102,21 +102,19 @@ class Auth(auth.BaseAuth): if ldap_secret_file_path: with open(ldap_secret_file_path, 'r') as file: self._ldap_secret = file.read().rstrip('\n') - if self._ldap_module_version == 3: - self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") - self._ldap_security = configuration.get("auth", "ldap_security") - self._use_encryption = self._ldap_use_ssl or self._ldap_security in ("tls", "starttls") - if self._ldap_use_ssl and self._ldap_security == "starttls": - raise RuntimeError("Cannot set both 'ldap_use_ssl = True' and 'ldap_security' = 'starttls'") - if self._ldap_use_ssl: - logger.warning("Configuration uses soon to be deprecated 'ldap_use_ssl', use 'ldap_security' ('none', 'tls', 'starttls') instead.") - if self._use_encryption: - self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file") - tmp = configuration.get("auth", "ldap_ssl_verify_mode") - if tmp == "NONE": - self._ldap_ssl_verify_mode = ssl.CERT_NONE - elif tmp == "OPTIONAL": - self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL + self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") + self._ldap_security = configuration.get("auth", "ldap_security") + self._use_encryption = self._ldap_use_ssl or self._ldap_security in ("tls", "starttls") + if self._ldap_use_ssl and self._ldap_security == "starttls": + raise RuntimeError("Cannot set both 'ldap_use_ssl = True' and 'ldap_security' = 'starttls'") + if self._ldap_use_ssl: + logger.warning("Configuration uses soon to be deprecated 'ldap_use_ssl', use 'ldap_security' ('none', 'tls', 'starttls') instead.") + self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file") + tmp = configuration.get("auth", "ldap_ssl_verify_mode") + if tmp == "NONE": + self._ldap_ssl_verify_mode = ssl.CERT_NONE + elif tmp == "OPTIONAL": + self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL logger.info("auth.ldap_uri : %r" % self._ldap_uri) logger.info("auth.ldap_base : %r" % self._ldap_base) From 7eb0c665127580aab6ab1ee1225f86fd59eaf765 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 14 Sep 2025 09:50:46 +0200 Subject: [PATCH 02/14] LDAP auth: refactor dealing with 'ldap_use_ssl' * stop treating it as class property * refactor to consolidate logic into one big 'if' statement (for easier removal when the config option gets removed in the future) * make deprecation warning for 'ldap_use_ssl' more urgent * raise error if conflicting settings 'ldap_security' = "starttls" and 'ldap_use_ssl' = True are set together * if not set, infer 'ldap_security' = "tls" from 'ldap_use_ssl' = True, logging a warning for the admin to update the config --- radicale/auth/ldap.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 0974b024..249c3b1a 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -68,7 +68,6 @@ class Auth(auth.BaseAuth): _ldap_group_members_attr: str _ldap_module_version: int = 3 _use_encryption: bool = False - _ldap_use_ssl: bool = False _ldap_security: str = "none" _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED _ldap_ssl_ca_file: str = "" @@ -102,13 +101,16 @@ class Auth(auth.BaseAuth): if ldap_secret_file_path: with open(ldap_secret_file_path, 'r') as file: self._ldap_secret = file.read().rstrip('\n') - self._ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") self._ldap_security = configuration.get("auth", "ldap_security") - self._use_encryption = self._ldap_use_ssl or self._ldap_security in ("tls", "starttls") - if self._ldap_use_ssl and self._ldap_security == "starttls": - raise RuntimeError("Cannot set both 'ldap_use_ssl = True' and 'ldap_security' = 'starttls'") - if self._ldap_use_ssl: - logger.warning("Configuration uses soon to be deprecated 'ldap_use_ssl', use 'ldap_security' ('none', 'tls', 'starttls') instead.") + ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") + self._use_encryption = ldap_use_ssl or self._ldap_security in ("tls", "starttls") + if ldap_use_ssl: + logger.warning("Configuration uses deprecated 'ldap_use_ssl': use 'ldap_security' ('none', 'tls', 'starttls') instead.") + if self._ldap_security == "starttls": + raise RuntimeError("Deprecated config setting 'ldap_use_ssl = True' conflicts with 'ldap_security' = 'starttls'") + elif self._ldap_security != "tls": + logger.warning("Update configuration: set 'ldap_security = tls' instead of deprecated 'ldap_use_ssl = True'") + self._ldap_security = "tls" self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file") tmp = configuration.get("auth", "ldap_ssl_verify_mode") if tmp == "NONE": @@ -152,7 +154,7 @@ class Auth(auth.BaseAuth): if self._ldap_reader_dn and not self._ldap_secret: logger.error("auth.ldap_secret : (not provided)") raise RuntimeError("LDAP authentication requires ldap_secret for ldap_reader_dn") - logger.info("auth.ldap_use_ssl : %s" % self._ldap_use_ssl) + logger.info("auth.ldap_use_ssl : %s" % ldap_use_ssl) logger.info("auth.ldap_security : %s" % self._ldap_security) if self._use_encryption: logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode) @@ -269,7 +271,7 @@ class Auth(auth.BaseAuth): validate=self._ldap_ssl_verify_mode, ca_certs_file=self._ldap_ssl_ca_file ) - if self._ldap_use_ssl or self._ldap_security == "tls": + if self._ldap_security == "tls": logger.debug("_login3 using ssl (reader)") server = self.ldap3.Server(self._ldap_uri, use_ssl=True, tls=tls) else: From c58eef4bacc7f457782a174178d55b03781477bc Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 14 Sep 2025 10:04:22 +0200 Subject: [PATCH 03/14] LDAP auth: infer 'ldap_security = tls' from the URL prefix: ldaps:// => LDAPS LDAP URIs starting with the scheme 'ldaps' are - by definition - meant to use LDAPS instead of plain LDAP: infer 'ldap_security' = "tls" if it is not set. --- radicale/auth/ldap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 249c3b1a..9df25b83 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -118,6 +118,10 @@ class Auth(auth.BaseAuth): elif tmp == "OPTIONAL": self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL + if self._ldap_uri.lower().startswith("ldaps://") and self._ldap_security not in ("tls", "starttls"): + logger.info("Inferring 'ldap_security' = tls from 'ldap_uri' starting with 'ldaps://'") + self._ldap_security = "tls" + logger.info("auth.ldap_uri : %r" % self._ldap_uri) logger.info("auth.ldap_base : %r" % self._ldap_base) logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn) From 73b77defe455207162968f567fac9c112516076b Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 14 Sep 2025 10:27:26 +0200 Subject: [PATCH 04/14] LDAP auth: warn on unset ldap_ssl_ca_file when certificate verification is wanted --- radicale/auth/ldap.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 9df25b83..0783cfcf 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -122,6 +122,9 @@ class Auth(auth.BaseAuth): logger.info("Inferring 'ldap_security' = tls from 'ldap_uri' starting with 'ldaps://'") self._ldap_security = "tls" + if self._ldap_ssl_ca_file == "" and self._ldap_ssl_verify_mode != ssl.CERT_NONE and self._ldap_security in ("tls", "starttls"): + logger.warning("Certificate verification not possible: 'ldap_ssl_ca_file' not set") + logger.info("auth.ldap_uri : %r" % self._ldap_uri) logger.info("auth.ldap_base : %r" % self._ldap_base) logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn) From b21549b998565b661aa474534475ad85daadda83 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 14 Sep 2025 11:41:10 +0200 Subject: [PATCH 05/14] LDAP auth: warn if 'ldap_ssl_ca_file' is set without LDAP encryption --- radicale/auth/ldap.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 0783cfcf..bd9e851c 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -124,6 +124,8 @@ class Auth(auth.BaseAuth): if self._ldap_ssl_ca_file == "" and self._ldap_ssl_verify_mode != ssl.CERT_NONE and self._ldap_security in ("tls", "starttls"): logger.warning("Certificate verification not possible: 'ldap_ssl_ca_file' not set") + if self._ldap_ssl_ca_file and self._ldap_security not in ("tls", "starttls"): + logger.warning("Config setting 'ldap_ssl_ca_file' useless without encrypted LDAP connection") logger.info("auth.ldap_uri : %r" % self._ldap_uri) logger.info("auth.ldap_base : %r" % self._ldap_base) From f8b15eb122b1ab3ff4d06e37f95db8b6dd84739c Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 14 Sep 2025 12:22:18 +0200 Subject: [PATCH 06/14] LDAP auth: get rid of helper property '_use_encryption' Inferring 'ldap_security' in earlier commits, allows us to get rid of the helper property '_use_encryption', streamlining the code. --- radicale/auth/ldap.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index bd9e851c..8c9d5b69 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -67,7 +67,6 @@ class Auth(auth.BaseAuth): _ldap_group_filter: str _ldap_group_members_attr: str _ldap_module_version: int = 3 - _use_encryption: bool = False _ldap_security: str = "none" _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED _ldap_ssl_ca_file: str = "" @@ -103,7 +102,6 @@ class Auth(auth.BaseAuth): self._ldap_secret = file.read().rstrip('\n') self._ldap_security = configuration.get("auth", "ldap_security") ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") - self._use_encryption = ldap_use_ssl or self._ldap_security in ("tls", "starttls") if ldap_use_ssl: logger.warning("Configuration uses deprecated 'ldap_use_ssl': use 'ldap_security' ('none', 'tls', 'starttls') instead.") if self._ldap_security == "starttls": @@ -165,7 +163,7 @@ class Auth(auth.BaseAuth): raise RuntimeError("LDAP authentication requires ldap_secret for ldap_reader_dn") logger.info("auth.ldap_use_ssl : %s" % ldap_use_ssl) logger.info("auth.ldap_security : %s" % self._ldap_security) - if self._use_encryption: + if self._ldap_security in ("tls", "starttls"): logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode) if self._ldap_ssl_ca_file: logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file) @@ -272,7 +270,7 @@ class Auth(auth.BaseAuth): """Connect the server""" try: logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}") - if self._use_encryption: + if self._ldap_security in ("tls", "starttls"): logger.debug("_login3 using encryption (reader)") tls = self.ldap3.Tls(validate=self._ldap_ssl_verify_mode) if self._ldap_ssl_ca_file != "": From 2d7a9b001c1512fee5d0e023cf7802d3c7ee0490 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 14 Sep 2025 13:57:36 +0200 Subject: [PATCH 07/14] LDAP auth: support TLS & start_tls also with python-ldap Until now, every connection to the LDAP server was silently unencryptedr when using Python's ldap module instead of the ldap3 module. I.e. using Python's ldap module was inherently insecure, as there was not even a hint that the config settings for encryption were ignored. This commit changes this and brings LDAP authentication based on the ldap module feature-wise on par with the one based on the ldap3 module. --- radicale/auth/ldap.py | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 8c9d5b69..9d41e5aa 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -83,7 +83,7 @@ class Auth(auth.BaseAuth): self._ldap_module_version = 2 self.ldap = ldap except ImportError as e: - raise RuntimeError("LDAP authentication requires the ldap3 module") from e + raise RuntimeError("LDAP authentication requires the ldap3 or ldap module") from e self._ldap_ignore_attribute_create_modify_timestamp = configuration.get("auth", "ldap_ignore_attribute_create_modify_timestamp") self._ldap_uri = configuration.get("auth", "ldap_uri") @@ -183,8 +183,26 @@ class Auth(auth.BaseAuth): """Bind as reader dn""" logger.debug(f"_login2 {self._ldap_uri}, {self._ldap_reader_dn}") conn = self.ldap.initialize(self._ldap_uri) - conn.protocol_version = 3 + conn.protocol_version = self.ldap.VERSION3 conn.set_option(self.ldap.OPT_REFERRALS, 0) + + if self._ldap_security in ("tls", "starttls"): + """certificate validation mode""" + if self._ldap_ssl_verify_mode == ssl.CERT_REQUIRED: + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_DEMAND) + elif self._ldap_ssl_verify_mode == ssl.CERT_OPTIONAL: + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_ALLOW) + else: + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_NONE) + """CA file to validate certificate against""" + if self._ldap_ssl_ca_file: + conn.set_option(self.ldap.OPT_X_TLS_CACERTFILE, self._ldap_ssl_ca_file) + """create TLS context- this must be the last TLS setting""" + conn.set_option(self.ldap.OPT_X_TLS_NEWCTX, self.ldap.OPT_ON) + + if self._ldap_security == "starttls": + conn.start_tls_s() + conn.simple_bind_s(self._ldap_reader_dn, self._ldap_secret) """Search for the dn of user to authenticate""" escaped_login = self.ldap.filter.escape_filter_chars(login) @@ -234,8 +252,26 @@ class Auth(auth.BaseAuth): try: """Bind as user to authenticate""" conn = self.ldap.initialize(self._ldap_uri) - conn.protocol_version = 3 + conn.protocol_version = self.ldap.VERSION3 conn.set_option(self.ldap.OPT_REFERRALS, 0) + + if self._ldap_security in ("tls", "starttls"): + """certificate validation mode""" + if self._ldap_ssl_verify_mode == ssl.CERT_REQUIRED: + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_DEMAND) + elif self._ldap_ssl_verify_mode == ssl.CERT_OPTIONAL: + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_ALLOW) + else: + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_NONE) + """CA file to validate certificate against""" + if self._ldap_ssl_ca_file: + conn.set_option(self.ldap.OPT_X_TLS_CACERTFILE, self._ldap_ssl_ca_file) + """create TLS context- this must be the last TLS setting""" + conn.set_option(self.ldap.OPT_X_TLS_NEWCTX, self.ldap.OPT_ON) + + if self._ldap_security == "starttls": + conn.start_tls_s() + conn.simple_bind_s(user_dn, password) if self._ldap_user_attr: if user_entry[1][self._ldap_user_attr]: From 44c64d70f51c3158b866fc33c7340c860967551f Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sat, 27 Sep 2025 20:31:57 +0200 Subject: [PATCH 08/14] LDAP auth: _login2: re-bind as user within same connection Python's ldap module, which is modelled along OpenLDAP's API, allows us to keep the connection and doing a new bind as a different user, superseding the previous bind. Use this to simplify the code and avoid duplication. --- radicale/auth/ldap.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 9d41e5aa..f9041993 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -244,34 +244,11 @@ class Auth(auth.BaseAuth): for dn, entry in res: groupDNs.append(dn) - """Close LDAP connection""" - conn.unbind() except Exception as e: raise RuntimeError(f"Invalid LDAP configuration:{e}") try: """Bind as user to authenticate""" - conn = self.ldap.initialize(self._ldap_uri) - conn.protocol_version = self.ldap.VERSION3 - conn.set_option(self.ldap.OPT_REFERRALS, 0) - - if self._ldap_security in ("tls", "starttls"): - """certificate validation mode""" - if self._ldap_ssl_verify_mode == ssl.CERT_REQUIRED: - conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_DEMAND) - elif self._ldap_ssl_verify_mode == ssl.CERT_OPTIONAL: - conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_ALLOW) - else: - conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_NONE) - """CA file to validate certificate against""" - if self._ldap_ssl_ca_file: - conn.set_option(self.ldap.OPT_X_TLS_CACERTFILE, self._ldap_ssl_ca_file) - """create TLS context- this must be the last TLS setting""" - conn.set_option(self.ldap.OPT_X_TLS_NEWCTX, self.ldap.OPT_ON) - - if self._ldap_security == "starttls": - conn.start_tls_s() - conn.simple_bind_s(user_dn, password) if self._ldap_user_attr: if user_entry[1][self._ldap_user_attr]: From b6ee3b6991e1d4365a081edbcf3f756ad455ab07 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 28 Sep 2025 10:24:20 +0200 Subject: [PATCH 09/14] LDAP auth: align values when logging config options In addition, log 'ldap_ssl_verify_mode' and 'ldap_ssl_ca_file' unconditionally. --- radicale/auth/ldap.py | 51 +++++++++++++++++++++---------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index f9041993..65eb2c02 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -125,50 +125,49 @@ class Auth(auth.BaseAuth): if self._ldap_ssl_ca_file and self._ldap_security not in ("tls", "starttls"): logger.warning("Config setting 'ldap_ssl_ca_file' useless without encrypted LDAP connection") - logger.info("auth.ldap_uri : %r" % self._ldap_uri) - logger.info("auth.ldap_base : %r" % self._ldap_base) - logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn) - logger.info("auth.ldap_filter : %r" % self._ldap_filter) + logger.info("auth.ldap_uri : %r" % self._ldap_uri) + logger.info("auth.ldap_base : %r" % self._ldap_base) + logger.info("auth.ldap_reader_dn : %r" % self._ldap_reader_dn) + logger.info("auth.ldap_filter : %r" % self._ldap_filter) if self._ldap_user_attr: - logger.info("auth.ldap_user_attribute : %r" % self._ldap_user_attr) + logger.info("auth.ldap_user_attribute : %r" % self._ldap_user_attr) else: - logger.info("auth.ldap_user_attribute : (not provided)") + logger.info("auth.ldap_user_attribute : (not provided)") if self._ldap_groups_attr: - logger.info("auth.ldap_groups_attribute: %r" % self._ldap_groups_attr) + logger.info("auth.ldap_groups_attribute : %r" % self._ldap_groups_attr) else: - logger.info("auth.ldap_groups_attribute: (not provided)") + logger.info("auth.ldap_groups_attribute : (not provided)") if self._ldap_group_base: - logger.info("auth.ldap_group_base : %r" % self._ldap_group_base) + logger.info("auth.ldap_group_base : %r" % self._ldap_group_base) else: - logger.info("auth.ldap_group_base : (not provided, using ldap_base)") + logger.info("auth.ldap_group_base : (not provided, using ldap_base)") self._ldap_group_base = self._ldap_base if self._ldap_group_filter: - logger.info("auth.ldap_group_filter: %r" % self._ldap_group_filter) + logger.info("auth.ldap_group_filter : %r" % self._ldap_group_filter) else: - logger.info("auth.ldap_group_filter: (not provided)") + logger.info("auth.ldap_group_filter : (not provided)") if self._ldap_group_members_attr: logger.info("auth.ldap_group_members_attr: %r" % self._ldap_group_members_attr) else: logger.info("auth.ldap_group_members_attr: (not provided)") if ldap_secret_file_path: - logger.info("auth.ldap_secret_file_path: %r" % ldap_secret_file_path) + logger.info("auth.ldap_secret_file_path : %r" % ldap_secret_file_path) if self._ldap_secret: - logger.info("auth.ldap_secret : (from file)") + logger.info("auth.ldap_secret : (from file)") else: - logger.info("auth.ldap_secret_file_path: (not provided)") + logger.info("auth.ldap_secret_file_path : (not provided)") if self._ldap_secret: - logger.info("auth.ldap_secret : (from config)") + logger.info("auth.ldap_secret : (from config)") if self._ldap_reader_dn and not self._ldap_secret: - logger.error("auth.ldap_secret : (not provided)") + logger.error("auth.ldap_secret : (not provided)") raise RuntimeError("LDAP authentication requires ldap_secret for ldap_reader_dn") - logger.info("auth.ldap_use_ssl : %s" % ldap_use_ssl) - logger.info("auth.ldap_security : %s" % self._ldap_security) - if self._ldap_security in ("tls", "starttls"): - logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode) - if self._ldap_ssl_ca_file: - logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file) - else: - logger.info("auth.ldap_ssl_ca_file : (not provided)") + logger.info("auth.ldap_use_ssl : %s" % ldap_use_ssl) + logger.info("auth.ldap_security : %s" % self._ldap_security) + logger.info("auth.ldap_ssl_verify_mode : %s" % self._ldap_ssl_verify_mode) + if self._ldap_ssl_ca_file: + logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file) + else: + logger.info("auth.ldap_ssl_ca_file : (not provided)") if self._ldap_ignore_attribute_create_modify_timestamp: logger.info("auth.ldap_ignore_attribute_create_modify_timestamp applied (relevant for ldap3 only)") """Extend attributes to to be returned in the user query""" @@ -176,7 +175,7 @@ class Auth(auth.BaseAuth): self._ldap_attributes.append(self._ldap_groups_attr) if self._ldap_user_attr: self._ldap_attributes.append(self._ldap_user_attr) - logger.info("ldap_attributes : %r" % self._ldap_attributes) + logger.info("ldap_attributes : %r" % self._ldap_attributes) def _login2(self, login: str, password: str) -> str: try: From 7df4c070e1749beacec3321bb291c60d9d38bc54 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 28 Sep 2025 10:44:33 +0200 Subject: [PATCH 10/14] LDAP auth: fail on illegal values for config settings Thr config settings 'ldap_security' and 'ldap_ssl_verify_mode' only accept a specific set of values: fail if other values are provided. --- radicale/auth/ldap.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 65eb2c02..5fbe2684 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -101,6 +101,8 @@ class Auth(auth.BaseAuth): with open(ldap_secret_file_path, 'r') as file: self._ldap_secret = file.read().rstrip('\n') self._ldap_security = configuration.get("auth", "ldap_security") + if self._ldap_security not in ("none", "tls", "starttls"): + raise RuntimeError("Illegal value for config setting ´ldap_security'") ldap_use_ssl = configuration.get("auth", "ldap_use_ssl") if ldap_use_ssl: logger.warning("Configuration uses deprecated 'ldap_use_ssl': use 'ldap_security' ('none', 'tls', 'starttls') instead.") @@ -115,6 +117,8 @@ class Auth(auth.BaseAuth): self._ldap_ssl_verify_mode = ssl.CERT_NONE elif tmp == "OPTIONAL": self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL + elif tmp != "REQUIRED": + raise RuntimeError("Illegal value for config setting ´ldap_ssl_verify_mode'") if self._ldap_uri.lower().startswith("ldaps://") and self._ldap_security not in ("tls", "starttls"): logger.info("Inferring 'ldap_security' = tls from 'ldap_uri' starting with 'ldaps://'") From bcba53ed8dbf53da20d77466a949bd1a45305ac5 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 28 Sep 2025 12:31:10 +0200 Subject: [PATCH 11/14] LDAP auth: re-factor handling of 'ldap_ssl_verify_mode' * treat 'ldap_ssl_verify_mode' as string * perform check for accepted values; fail on illegal ones * translate to the values nbeeded by the respective LDAP module when doing the login, based on a module specific dictionary --- radicale/auth/ldap.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 5fbe2684..a627e132 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -68,7 +68,7 @@ class Auth(auth.BaseAuth): _ldap_group_members_attr: str _ldap_module_version: int = 3 _ldap_security: str = "none" - _ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED + _ldap_ssl_verify_mode: str = "REQUIRED" _ldap_ssl_ca_file: str = "" def __init__(self, configuration: config.Configuration) -> None: @@ -112,19 +112,15 @@ class Auth(auth.BaseAuth): logger.warning("Update configuration: set 'ldap_security = tls' instead of deprecated 'ldap_use_ssl = True'") self._ldap_security = "tls" self._ldap_ssl_ca_file = configuration.get("auth", "ldap_ssl_ca_file") - tmp = configuration.get("auth", "ldap_ssl_verify_mode") - if tmp == "NONE": - self._ldap_ssl_verify_mode = ssl.CERT_NONE - elif tmp == "OPTIONAL": - self._ldap_ssl_verify_mode = ssl.CERT_OPTIONAL - elif tmp != "REQUIRED": + self._ldap_ssl_verify_mode = configuration.get("auth", "ldap_ssl_verify_mode") + if self._ldap_ssl_verify_mode not in ("NONE", "OPTIONAL", "REQUIRED"): raise RuntimeError("Illegal value for config setting ´ldap_ssl_verify_mode'") if self._ldap_uri.lower().startswith("ldaps://") and self._ldap_security not in ("tls", "starttls"): logger.info("Inferring 'ldap_security' = tls from 'ldap_uri' starting with 'ldaps://'") self._ldap_security = "tls" - if self._ldap_ssl_ca_file == "" and self._ldap_ssl_verify_mode != ssl.CERT_NONE and self._ldap_security in ("tls", "starttls"): + if self._ldap_ssl_ca_file == "" and self._ldap_ssl_verify_mode != "NONE" and self._ldap_security in ("tls", "starttls"): logger.warning("Certificate verification not possible: 'ldap_ssl_ca_file' not set") if self._ldap_ssl_ca_file and self._ldap_security not in ("tls", "starttls"): logger.warning("Config setting 'ldap_ssl_ca_file' useless without encrypted LDAP connection") @@ -191,12 +187,10 @@ class Auth(auth.BaseAuth): if self._ldap_security in ("tls", "starttls"): """certificate validation mode""" - if self._ldap_ssl_verify_mode == ssl.CERT_REQUIRED: - conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_DEMAND) - elif self._ldap_ssl_verify_mode == ssl.CERT_OPTIONAL: - conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_ALLOW) - else: - conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, self.ldap.OPT_X_TLS_NONE) + verifyMode = {"NONE": self.ldap.OPT_X_TLS_NEVER, + "OPTIONAL": self.ldap.OPT_X_TLS_ALLOW, + "REQUIRED": self.ldap.OPT_X_TLS_DEMAND} + conn.set_option(self.ldap.OPT_X_TLS_REQUIRE_CERT, verifyMode[self._ldap_ssl_verify_mode]) """CA file to validate certificate against""" if self._ldap_ssl_ca_file: conn.set_option(self.ldap.OPT_X_TLS_CACERTFILE, self._ldap_ssl_ca_file) @@ -288,12 +282,12 @@ class Auth(auth.BaseAuth): logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}") if self._ldap_security in ("tls", "starttls"): logger.debug("_login3 using encryption (reader)") - tls = self.ldap3.Tls(validate=self._ldap_ssl_verify_mode) + verifyMode = {"NONE": ssl.CERT_NONE, + "OPTIONAL": ssl.CERT_OPTIONAL, + "REQUIRED": ssl.CERT_REQUIRED} + tls = self.ldap3.Tls(validate=verifyMode[self._ldap_ssl_verify_mode]) if self._ldap_ssl_ca_file != "": - tls = self.ldap3.Tls( - validate=self._ldap_ssl_verify_mode, - ca_certs_file=self._ldap_ssl_ca_file - ) + tls = self.ldap3.Tls(validate=verifyMode[self._ldap_ssl_verify_mode], ca_certs_file=self._ldap_ssl_ca_file) if self._ldap_security == "tls": logger.debug("_login3 using ssl (reader)") server = self.ldap3.Server(self._ldap_uri, use_ssl=True, tls=tls) From f0626a8dde005d351f61b93fa9f861d20a7562a2 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 28 Sep 2025 13:17:29 +0200 Subject: [PATCH 12/14] LDAP auth: change 'ldap_ssl_verify_mode' to NONE for ldapi:// For ldapi:// connections, which connect - by definition - to a local UNIX socket, lower the value of config setting 'ldap_ssl_verify_mode' to "NONE" to avoid certificate validation failures. The UNIX socket address can NEVER match any DNS name from a certificate, making the whole certificate validation moot. This is a workaround for a limitation of Python's LDAP modules, that do not consider this edge case. --- radicale/auth/ldap.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index a627e132..48634327 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -119,6 +119,9 @@ class Auth(auth.BaseAuth): if self._ldap_uri.lower().startswith("ldaps://") and self._ldap_security not in ("tls", "starttls"): logger.info("Inferring 'ldap_security' = tls from 'ldap_uri' starting with 'ldaps://'") self._ldap_security = "tls" + if self._ldap_uri.lower().startswith("ldapi://") and self._ldap_ssl_verify_mode != "NONE": + logger.info("Lowering 'ldap_'ldap_ssl_verify_mode' to NONE for 'ldap_uri' starting with 'ldapi://'") + self._ldap_ssl_verify_mode = "NONE" if self._ldap_ssl_ca_file == "" and self._ldap_ssl_verify_mode != "NONE" and self._ldap_security in ("tls", "starttls"): logger.warning("Certificate verification not possible: 'ldap_ssl_ca_file' not set") From 2d9830fb6a2e50c30fab18be9f83c6d2162fc658 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Mon, 29 Sep 2025 20:17:16 +0200 Subject: [PATCH 13/14] LDAP auth: add my Copyright to radicale/auth/ldap.py --- radicale/auth/ldap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 48634327..aadbbf64 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -1,6 +1,7 @@ # This file is part of Radicale - CalDAV and CardDAV server # Copyright © 2022-2024 Peter Varkoly # Copyright © 2024-2024 Peter Bieringer +# Copyright © 2024-2025 Peter Marschall # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From 8ae5831e9c7aafbbf719949a05ef7ae3425a5576 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Mon, 29 Sep 2025 20:21:37 +0200 Subject: [PATCH 14/14] LDAP auth: update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e96ab4..8314ad0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 3.5.8.dev +* Extend [auth]: re-factor & overhaul LDPA autrhentication, especially for Python's ldap module ## 3.5.7 * Extend: [auth] dovecot: add support for version >= 2.4