From 8821612fa83a1a10f53f3b5aeff3eb2401899331 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Mon, 21 Jul 2025 21:11:32 +0200 Subject: [PATCH 1/8] LDAP auth: allow finding groups based on separate search Instead of searching for the membership attribute on the user side (usually AD: memberOf, Novell eDirectory: groupMembership) to determine the groups the user loging on is a member of, allow performing a separate search for the groups having the user as member and use the found groups' DNs. The group search is performed in the context of 'ldap_reader_dn', after the user DN has been found in the directory, but before the authentication has been performed by doing an LDAP bind in the user's context. Although this may - in the case of unsuccessful login attempts - double the number of queries to the LDAP server, it has been done this way to keep the number of LDAP contexts minimal. Doing the group search in the context of the user logging on is no viable option, because there are known implementations where regular users do not have the necessary permissions to query the groups they are a member in. --- radicale/auth/ldap.py | 128 ++++++++++++++++++++++++++++++++++-------- radicale/config.py | 12 ++++ 2 files changed, 116 insertions(+), 24 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 2c4d63c3..fa2a4891 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -30,6 +30,10 @@ Following parameters controls SSL connections: ldap_security The encryption mode to be used: *none*|tls|starttls ldap_ssl_verify_mode The certificate verification mode. Works for tls and starttls. NONE, OPTIONAL, default is REQUIRED ldap_ssl_ca_file + The following parameters are optional: + ldap_group_base Base DN to search for groups. Only if it differs from ldap_base and if ldap_group_members_attribute is set + ldap_group_filter Search filter to search for groups having the user as member. Only if ldap_group_members_attribute is set + ldap_group_members_attribute Attribute in the group entries to read the group's members from """ import ssl @@ -47,6 +51,9 @@ class Auth(auth.BaseAuth): _ldap_attributes: list[str] = [] _ldap_user_attr: str _ldap_groups_attr: str + _ldap_group_base: str + _ldap_group_filter: str + _ldap_group_members_attr: str _ldap_module_version: int = 3 _ldap_use_ssl: bool = False _ldap_security: str = "none" @@ -78,6 +85,9 @@ class Auth(auth.BaseAuth): self._ldap_filter = configuration.get("auth", "ldap_filter") self._ldap_user_attr = configuration.get("auth", "ldap_user_attribute") self._ldap_groups_attr = configuration.get("auth", "ldap_groups_attribute") + self._ldap_group_base = configuration.get("auth", "ldap_group_base") + self._ldap_group_filter = configuration.get("auth", "ldap_group_filter") + self._ldap_group_members_attr = configuration.get("auth", "ldap_group_members_attribute") ldap_secret_file_path = configuration.get("auth", "ldap_secret_file") if ldap_secret_file_path: with open(ldap_secret_file_path, 'r') as file: @@ -110,6 +120,19 @@ class Auth(auth.BaseAuth): logger.info("auth.ldap_groups_attribute: %r" % self._ldap_groups_attr) else: logger.info("auth.ldap_groups_attribute: (not provided)") + if 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)") + self._ldap_group_base = self._ldap_base + if self._ldap_group_filter: + logger.info("auth.ldap_group_filter: %r" % self._ldap_group_filter) + else: + 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) if self._ldap_secret: @@ -160,6 +183,30 @@ class Auth(auth.BaseAuth): user_entry = res[0] user_dn = user_entry[0] logger.debug(f"_login2 found LDAP user DN {user_dn}") + + """Let's collect the groups of the user.""" + groupDNs: list[str] = [] + if self._ldap_groups_attr: + groupDNs = user_entry[1][self._ldap_groups_attr] + + """Search for all groups having the user_dn found as member.""" + if self._ldap_group_members_attr: + groupDNs = [] + res = conn.search_s( + self._ldap_group_base, + self.ldap.SCOPE_SUBTREE, + filterstr="(&{0}({1}={2}))".format( + self._ldap_group_filter, + self._ldap_group_members_attr, + self.ldap.filter.escape_filter_chars(user_dn)), + attrlist=['1.1'] + ) + """Fill groupDNs with DNs of groups found""" + if len(res) > 0: + groupDNs = [] + for dn,entry in res: + groupDNs.append(dn) + """Close LDAP connection""" conn.unbind() except Exception as e: @@ -171,23 +218,23 @@ class Auth(auth.BaseAuth): conn.protocol_version = 3 conn.set_option(self.ldap.OPT_REFERRALS, 0) conn.simple_bind_s(user_dn, password) - tmp: list[str] = [] - if self._ldap_groups_attr: - tmp = [] - for g in user_entry[1][self._ldap_groups_attr]: - """Get group g's RDN's attribute value""" - try: - rdns = self.ldap.dn.explode_dn(g, notypes=True) - tmp.append(rdns[0]) - except Exception: - tmp.append(g.decode('utf8')) - self._ldap_groups = set(tmp) - logger.debug("_login2 LDAP groups of user: %s", ",".join(self._ldap_groups)) if self._ldap_user_attr: if user_entry[1][self._ldap_user_attr]: tmplogin = user_entry[1][self._ldap_user_attr][0] login = tmplogin.decode('utf-8') logger.debug(f"_login2 user set to: '{login}'") + + """Get RDNs of groups' DNs""" + tmp: list[str] = [] + for g in groupDNs: + try: + rdns = self.ldap.dn.explode_dn(g, notypes=True) + tmp.append(rdns[0]) + except Exception: + tmp.append(g.decode('utf8')) + self._ldap_groups = set(tmp) + logger.debug("_login2 LDAP groups of user: %s", ",".join(self._ldap_groups)) + conn.unbind() logger.debug(f"_login2 {login} successfully authenticated") return login @@ -249,9 +296,42 @@ class Auth(auth.BaseAuth): return "" user_entry = conn.response[0] - conn.unbind() user_dn = user_entry['dn'] logger.debug(f"_login3 found LDAP user DN {user_dn}") + + """Let's collect the groups of the user.""" + groupDNs: list[str] = [] + if self._ldap_groups_attr: + if user_entry['attributes'][self._ldap_groups_attr]: + if isinstance(user_entry['attributes'][self._ldap_groups_attr], list): + groupDNs = user_entry['attributes'][self._ldap_groups_attr] + else: + groupDNs.append(user_entry['attributes'][self._ldap_groups_attr]) + + """Search for all groups having the user_dn found as member.""" + if self._ldap_group_members_attr: + try: + conn.search( + search_base=self._ldap_group_base, + search_filter="(&{0}({1}={2}))".format( + self._ldap_group_filter, + self._ldap_group_members_attr, + self.ldap3.utils.conv.escape_filter_chars(user_dn)), + search_scope=self.ldap3.SUBTREE, + attributes=['1.1'] + ) + except Exception as e: + """LDAP search failed: consider it as non-fatal - only groups missing""" + logger.debug(f"_ldap3: LDAP group search failed: {e}") + else: + """Fill groupDNs with DNs of groups found""" + groupDNs = [] + for group in conn.response: + groupDNs.append(group['dn']) + + """Close LDAP connection""" + conn.unbind() + try: """Try to bind as the user itself""" try: @@ -264,18 +344,18 @@ class Auth(auth.BaseAuth): if not conn.bind(read_server_info=False): logger.debug(f"_login3 user '{login}' cannot be found") return "" + + """Get RDNs of groups' DNs""" tmp: list[str] = [] - if self._ldap_groups_attr: - tmp = [] - for g in user_entry['attributes'][self._ldap_groups_attr]: - """Get group g's RDN's attribute value""" - try: - rdns = self.ldap3.utils.dn.parse_dn(g) - tmp.append(rdns[0][1]) - except Exception: - tmp.append(g) - self._ldap_groups = set(tmp) - logger.debug("_login3 LDAP groups of user: %s", ",".join(self._ldap_groups)) + for g in groupDNs: + try: + rdns = self.ldap3.utils.dn.parse_dn(g) + tmp.append(rdns[0][1]) + except Exception: + tmp.append(g) + self._ldap_groups = set(tmp) + logger.debug("_login3 LDAP groups of user: %s", ",".join(self._ldap_groups)) + if self._ldap_user_attr: if user_entry['attributes'][self._ldap_user_attr]: if isinstance(user_entry['attributes'][self._ldap_user_attr], list): diff --git a/radicale/config.py b/radicale/config.py index c6d93f41..77fcb04e 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -297,6 +297,18 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "", "help": "attribute to read the group memberships from", "type": str}), + ("ldap_group_members_attribute", { + "value": "", + "help": "Attribute in the group entries to read the group's members from", + "type": str}), + ("ldap_group_base", { + "value": "", + "help": "Base DN to search for groups. Only if it differs from ldap_base and if ldap_group_members_attribute is set", + "type": str}), + ("ldap_group_filter", { + "value": "", + "help": "Search filter to search for groups having the user as member. Only if ldap_group_members_attribute is set", + "type": str}), ("ldap_use_ssl", { "value": "False", "help": "Use ssl on the ldap connection. Soon to be deprecated, use ldap_security instead", From 5f677fc77ed9c4c0cfb2a5d462b6a21f68049d2c Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 31 Aug 2025 17:51:23 +0200 Subject: [PATCH 2/8] LDAP auth: document all paramters at the top of the file --- radicale/auth/ldap.py | 46 +++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index fa2a4891..15bf89ea 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -16,24 +16,36 @@ # along with Radicale. If not, see . """ Authentication backend that checks credentials with a LDAP server. -Following parameters are needed in the configuration: - ldap_uri The LDAP URL to the server like ldap://localhost - ldap_base The baseDN of the LDAP server - ldap_reader_dn The DN of a LDAP user with read access to get the user accounts - ldap_secret The password of the ldap_reader_dn - ldap_secret_file The path of the file containing the password of the ldap_reader_dn - ldap_filter The search filter to find the user to authenticate by the username - ldap_user_attribute The attribute to be used as username after authentication - ldap_groups_attribute The attribute containing group memberships in the LDAP user entry -Following parameters controls SSL connections: - ldap_use_ssl If ssl encryption should be used (to be deprecated) - ldap_security The encryption mode to be used: *none*|tls|starttls - ldap_ssl_verify_mode The certificate verification mode. Works for tls and starttls. NONE, OPTIONAL, default is REQUIRED - ldap_ssl_ca_file + The following parameters are needed in the configuration: + ldap_uri URI to the LDAP server + ldap_base Base DN of the LDAP server + ldap_reader_dn DN of an LDAP user with read access to get the user accounts + ldap_secret Password of the 'ldap_reader_dn' + Better: use 'ldap_secret_file'! + ldap_secret_file Path of the file containing the password of the 'ldap_reader_dn' + ldap_filter Search filter to find the user DN to authenticate + The following parameters control TLS connections: + ldap_use_ssl Use ssl on the ldap connection. + Deprecated, use 'ldap_security' instead! + ldap_security Encryption mode to be used, + one of: *none* | tls | starttls + ldap_ssl_verify_mode Certificate verification mode for tls and starttls; + one of: *REQUIRED* | OPTIONAL | NONE + ldap_ssl_ca_file Path to the CA file in PEM format to certify the server certificate The following parameters are optional: - ldap_group_base Base DN to search for groups. Only if it differs from ldap_base and if ldap_group_members_attribute is set - ldap_group_filter Search filter to search for groups having the user as member. Only if ldap_group_members_attribute is set - ldap_group_members_attribute Attribute in the group entries to read the group's members from + ldap_user_attribute Attribute to be used as username after authentication, e.g. cn; + if not given, the name used to logon is used. + ldap_groups_attribute Attribute in the user entry to read the user's group memberships from, + e.g. memberof, groupMememberShip. This may even be a non-DN attribute! + ldap_group_base Base DN to search for groups; + only if it differs from 'ldap_base' and if 'ldap_group_members_attribute' is set + ldap_group_filter Search filter to search for groups having the user DN found as member; + only if 'ldap_group_members_attribute' is set + ldap_group_members_attribute Attribute in the group entries to read the group's members from, + e.g. member. + The following parameters are for LDAP servers with oddities + ldap_ignore_attribute_create_modify_timestamp + Ignore modifyTimestamp and createTimestamp attributes. Needed for Authentik LDAP server """ import ssl From 5a183e3c2b3504ccd59f4f74300e8c05055fbfc5 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 31 Aug 2025 20:43:10 +0200 Subject: [PATCH 3/8] LDAP auth: make flake8 happy "fix" small lint to keep flake8 happy. --- radicale/auth/ldap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 15bf89ea..54f29e08 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -216,7 +216,7 @@ class Auth(auth.BaseAuth): """Fill groupDNs with DNs of groups found""" if len(res) > 0: groupDNs = [] - for dn,entry in res: + for dn, entry in res: groupDNs.append(dn) """Close LDAP connection""" From 5c4a0578b02953dfbeb55677fbaf48cf60d14f5f Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sat, 6 Sep 2025 10:46:35 +0200 Subject: [PATCH 4/8] LDAP auth: fix _login2() by importing ldap.filter --- radicale/auth/ldap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 54f29e08..a1c0ec6c 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -80,6 +80,7 @@ class Auth(auth.BaseAuth): except ImportError: try: import ldap + import ldap.filter self._ldap_module_version = 2 self.ldap = ldap except ImportError as e: From 9b216a9f2408a51e6b4b6ae98bcea8edf4475bf5 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sat, 6 Sep 2025 10:58:23 +0200 Subject: [PATCH 5/8] LDAP auth: define fallback value for _use_encryption --- radicale/auth/ldap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index a1c0ec6c..2b17257c 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -67,6 +67,7 @@ class Auth(auth.BaseAuth): _ldap_group_filter: str _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 From cde4c5f2e831a4f41ee6e3d0e8c44b805d60bae1 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 7 Sep 2025 14:35:58 +0200 Subject: [PATCH 6/8] LDAP auth: stop giving type hints for local list variables --- radicale/auth/ldap.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 2b17257c..39b1593c 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -199,7 +199,7 @@ class Auth(auth.BaseAuth): logger.debug(f"_login2 found LDAP user DN {user_dn}") """Let's collect the groups of the user.""" - groupDNs: list[str] = [] + groupDNs = [] if self._ldap_groups_attr: groupDNs = user_entry[1][self._ldap_groups_attr] @@ -239,7 +239,7 @@ class Auth(auth.BaseAuth): logger.debug(f"_login2 user set to: '{login}'") """Get RDNs of groups' DNs""" - tmp: list[str] = [] + tmp = [] for g in groupDNs: try: rdns = self.ldap.dn.explode_dn(g, notypes=True) @@ -314,7 +314,7 @@ class Auth(auth.BaseAuth): logger.debug(f"_login3 found LDAP user DN {user_dn}") """Let's collect the groups of the user.""" - groupDNs: list[str] = [] + groupDNs = [] if self._ldap_groups_attr: if user_entry['attributes'][self._ldap_groups_attr]: if isinstance(user_entry['attributes'][self._ldap_groups_attr], list): @@ -360,7 +360,7 @@ class Auth(auth.BaseAuth): return "" """Get RDNs of groups' DNs""" - tmp: list[str] = [] + tmp = [] for g in groupDNs: try: rdns = self.ldap3.utils.dn.parse_dn(g) From 9eb955653660b86391a3f23c8a543ff8b9c34475 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Sun, 7 Sep 2025 14:38:56 +0200 Subject: [PATCH 7/8] LDAP auth: decode UTF-8 byte sequences to strings only if necessary --- radicale/auth/ldap.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 39b1593c..84dcee0b 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -234,8 +234,9 @@ class Auth(auth.BaseAuth): conn.simple_bind_s(user_dn, password) if self._ldap_user_attr: if user_entry[1][self._ldap_user_attr]: - tmplogin = user_entry[1][self._ldap_user_attr][0] - login = tmplogin.decode('utf-8') + login = user_entry[1][self._ldap_user_attr][0] + if isinstance(login, bytes): + login = login.decode('utf-8') logger.debug(f"_login2 user set to: '{login}'") """Get RDNs of groups' DNs""" @@ -245,7 +246,9 @@ class Auth(auth.BaseAuth): rdns = self.ldap.dn.explode_dn(g, notypes=True) tmp.append(rdns[0]) except Exception: - tmp.append(g.decode('utf8')) + if isinstance(g, bytes): + g = g.decode('utf-8') + tmp.append(g) self._ldap_groups = set(tmp) logger.debug("_login2 LDAP groups of user: %s", ",".join(self._ldap_groups)) From 57a4d8d47d1f8cc476a146ce1e92d5f212eeac17 Mon Sep 17 00:00:00 2001 From: Peter Marschall Date: Mon, 8 Sep 2025 21:59:29 +0200 Subject: [PATCH 8/8] LDAP auth: update, consolidate & extend documentation --- DOCUMENTATION.md | 143 ++++++++++++++++++++++++++++++++------------- config | 42 +++++++------ radicale/config.py | 54 ++++++++--------- 3 files changed, 154 insertions(+), 85 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index ec41848a..632c477a 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -987,7 +987,8 @@ Default: `Radicale - Password Required` _(>= 3.3.0)_ -The URI to the ldap server +URI to the LDAP server. +Mandatory for auth type `ldap`. Default: `ldap://localhost` @@ -995,39 +996,44 @@ Default: `ldap://localhost` _(>= 3.3.0)_ -LDAP base DN of the ldap server. This parameter must be provided if auth type is ldap. +Base DN of the LDAP server. +Mandatory for auth type `ldap`. -Default: +Default: (unset) ##### ldap_reader_dn _(>= 3.3.0)_ -The DN of a ldap user with read access to get the user accounts. This parameter must be provided if auth type is ldap. +DN of a LDAP user with read access users and - if defined - groups. +Mandatory for auth type `ldap`. -Default: +Default: (unset) ##### ldap_secret _(>= 3.3.0)_ -The password of the ldap_reader_dn. Either this parameter or `ldap_secret_file` must be provided if auth type is ldap. +Password of `ldap_reader_dn`. +Mandatory for auth type `ldap` unless `ldap_secret_file` is given. -Default: +Default: (unset) ##### ldap_secret_file _(>= 3.3.0)_ -Path of the file containing the password of the ldap_reader_dn. Either this parameter or `ldap_secret` must be provided if auth type is ldap. +Path to the file containing the password of `ldap_reader_dn`. +Mandatory for auth type `ldap` unless `ldap_secret` is given. -Default: +Default: (unset) ##### ldap_filter _(>= 3.3.0)_ -The search filter to find the user DN to authenticate by the username. User '{0}' as placeholder for the user name. +Filter to search for the LDAP entry of the user to authenticate. +It must contain '{0}' as placeholder for the login name. Default: `(cn={0})` @@ -1035,66 +1041,117 @@ Default: `(cn={0})` _(>= 3.4.0)_ -The LDAP attribute whose value shall be used as the user name after successful authentication +LDAP attribute whose value shall be used as the username after successful authentication. -Default: not set, i.e. the login name given is used directly. +If set, you can use flexible logins in `ldap_filter` and still have consolidated usernames, +e.g. to allow login in using mail addresses as an alternative to cn, simply set +``` +ldap_filter = (&(objectclass=inetOrgPerson)(|(cn={0})(mail={0}))) +ldap_user_attribute = cn +``` +Even for simple filter setups, it is recommended to set it in order to get usernames exactly +as they are stored in LDAP and to avoid inconsistencies in the upper-/lower-case spelling of the +login names. -##### ldap_groups_attribute - -_(>= 3.4.0)_ - -The LDAP attribute to read the group memberships from in the authenticated user's LDAP entry. - -If set, load the LDAP group memberships from the attribute given -These memberships can be used later on to define rights. -This also gives you access to the group calendars, if they exist. -* The group calendar will be placed under collection_root_folder/GROUPS -* The name of the calendar directory is the base64 encoded group name. -* The group calendar folders will not be created automatically. This must be done manually. In the [LDAP-authentication section of Radicale's wiki](https://github.com/Kozea/Radicale/wiki/LDAP-authentication) you can find a script to create a group calendar. - -Use 'memberOf' if you want to load groups on Active Directory and alikes, 'groupMembership' on Novell eDirectory, ... - -Default: (unset) +Default: (unset, in which case the login name is directly used as the username) ##### ldap_use_ssl _(>= 3.3.0)_ -Use ssl on the ldap connection (soon to be deprecated, use ldap_security instead) +Use ssl on the LDAP connection. **Deprecated**, use `ldap_security` instead**!** ##### ldap_security _(>= 3.5.2)_ -Use encryption on the ldap connection. none, tls, starttls +Use encryption on the LDAP connection. One of `none`, `tls`, `starttls`. -Default: none +Default: `none` ##### ldap_ssl_verify_mode _(>= 3.3.0)_ -The certificate verification mode. Works for tls and starttls. NONE, OPTIONAL or REQUIRED +Certificate verification mode for tls and starttls. One of `NONE`, `OPTIONAL`, `REQUIRED`. -Default: REQUIRED +Default: `REQUIRED` ##### ldap_ssl_ca_file _(>= 3.3.0)_ -The path to the CA file in pem format which is used to certificate the server certificate +Path to the CA file in PEM format which is used to certify the server certificate -Default: +Default: (unset) + +##### ldap_groups_attribute + +_(>= 3.4.0)_ + +LDAP attribute in the authenticated user's LDAP entry to read the group memberships from. + +E.g. `memberOf` to get groups on Active Directory and alikes, `groupMembership` on Novell eDirectory, ... + +If set, get the user's LDAP groups from the attribute given. + +For DN-valued attributes, the value of the RDN is used to determine the group names. +The implementation also supports non-DN-valued attributes: their values are taken directly. + +The user's group names can be used later on to define rights. +They also give you access to the group calendars, if those exist. +* Group calendars are placed directly under *collection_root_folder*`/GROUPS/` + with the base64-encoded group name as the calendar folder name. +* Group calendar folders are not created automatically. + This must be done manually. In the [LDAP-authentication section of Radicale's wiki](https://github.com/Kozea/Radicale/wiki/LDAP-authentication) you can find a script to create a group calendar. + +Default: (unset) + +##### ldap_group_members_attribute + +_(>= 3.5.6)_ + +Attribute in the group entries to read the group's members from. + +E.g. `member` for groups with objectclass `groupOfNames`. + +Using `ldap_group_members_attribute`, `ldap_group_base` and `ldap_group_filter` is an alternative +approach to getting the user's groups. Instead of reading them from `ldap_groups_attribute` +in the user's entry, an additional query is performed to seach for those groups beneath `ldap_group_base`, +that have the user's DN in their `ldap_group_members_attribute` and additionally fulfil `ldap_group_filter`. + +As with DN-valued `ldap_groups_attribute`, the value of the RDN is used to determine the group names. + +Default: (unset) + +##### ldap_group_base + +_(>= 3.5.6)_ + +Base DN to search for groups. +Only necessary if `ldap_group_members_attribute` is set, and if the base DN for groups differs from `ldap_base`. + +Default: (unset, in which case `ldap_base` is used as fallback) + +##### ldap_group_filter + +_(>= 3.5.6)_ + +Search filter to search for groups having the user DN found as member. +Only necessary `ldap_group_members_attribute` is set, and you want the groups returned to be restricted +instead of all groups the user's DN is in. + +Default: (unset) ##### ldap_ignore_attribute_create_modify_timestamp _(>= 3.5.1)_ -Add modifyTimestamp and createTimestamp to the exclusion list of internal ldap3 client -so that these schema attributes are not checked. This is needed at least for Authentik -LDAP server as not providing these both attributes. +Quirks for Authentik LDAP server, which violates the LDAP RFCs: +add modifyTimestamp and createTimestamp to the exclusion list of internal ldap3 client +so that these schema attributes are not checked. -Default: false +Default: `false` ##### dovecot_connection_type = AF_UNIX @@ -1177,7 +1234,9 @@ providers like ldap, kerberos Default: `False` -Note: cannot be enabled together with `uc_username` +Notes: +* `lc_username` and `uc_username` are mutually exclusive +* for auth type `ldap` the use of `ldap_user_attribute` is preferred ##### uc_username @@ -1188,7 +1247,9 @@ providers like ldap, kerberos Default: `False` -Note: cannot be enabled together with `lc_username` +Notes: +* `uc_username` and `lc_username` are mutually exclusive +* for auth type `ldap` the use of `ldap_user_attribute` is preferred ##### strip_domain diff --git a/config b/config index 0e08659b..b51c5dfc 100644 --- a/config +++ b/config @@ -75,46 +75,54 @@ ## Expiration time of caching failed logins in seconds #cache_failed_logins_expiry = 90 -# Ignore modifyTimestamp and createTimestamp attributes. Required e.g. for Authentik LDAP server -#ldap_ignore_attribute_create_modify_timestamp = false - # URI to the LDAP server #ldap_uri = ldap://localhost -# The base DN where the user accounts have to be searched +# Base DN of the LDAP server to search for user accounts #ldap_base = ##BASE_DN## -# The reader DN of the LDAP server +# Reader DN of the LDAP server; (needs read access to users and - if defined - groups) #ldap_reader_dn = CN=ldapreader,CN=Users,##BASE_DN## -# Password of the reader DN +# Password of the reader DN (better: use 'ldap_secret_file'!) #ldap_secret = ldapreader-secret -# Path of the file containing password of the reader DN +# Path to the file containing the password of the reader DN #ldap_secret_file = /run/secrets/ldap_password -# the attribute to read the group memberships from in the user's LDAP entry (default: not set) -#ldap_groups_attribute = memberOf - -# The filter to find the DN of the user. This filter must contain a python-style placeholder for the login +# Filter to search for the LDAP entry of the user to authenticate. It must contain '{0}' as placeholder for the login name. #ldap_filter = (&(objectClass=person)(uid={0})) -# the attribute holding the value to be used as username after authentication +# Attribute holding the value to be used as username after authentication #ldap_user_attribute = cn -# Use ssl on the ldap connection -# Soon to be deprecated, use ldap_security instead +# Use ssl on the LDAP connection (DEPRECATED - use 'ldap_security'!) #ldap_use_ssl = False -# the encryption mode to be used: tls, starttls, default is none +# Encryption mode to be used. Default: none; one of: none, tls, starttls #ldap_security = none -# The certificate verification mode. Works for ssl and starttls. NONE, OPTIONAL, default is REQUIRED +# Certificate verification mode for tls & starttls. Default: REQUIRED; one of NONE, OPTIONAL, REQUIRED #ldap_ssl_verify_mode = REQUIRED -# The path to the CA file in pem format which is used to certificate the server certificate +# Path to the CA file in PEM format to certify the server certificate #ldap_ssl_ca_file = +# Attribute in the user's LDAP entry to read the group memberships from; default: not set +#ldap_groups_attribute = memberOf + +# Attribute in the group entries to read the group's members from, e.g. member; default: not set +#ldap_group_members_attribute = member + +# Base DN to search for groups; only if it differs from 'ldap_base' and if 'ldap_group_members_attribute' is set +#ldap_group_base = ##GROUP_BASE_DN## + +# Search filter to search for groups having the user DN found as member; only if 'ldap_group_members_attribute' is set +#ldap_group_filter = (objectclass=groupOfNames) + +# Quirks for Authentik LDAP server: ignore modifyTimestamp and createTimestamp attributes +#ldap_ignore_attribute_create_modify_timestamp = false + # Connection type for dovecot authentication (AF_UNIX|AF_INET|AF_INET6) # Note: credentials are transmitted in cleartext #dovecot_connection_type = AF_UNIX diff --git a/radicale/config.py b/radicale/config.py index 77fcb04e..adab9567 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -261,41 +261,53 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "1", "help": "incorrect authentication delay", "type": positive_float}), - ("ldap_ignore_attribute_create_modify_timestamp", { - "value": "false", - "help": "Ignore modifyTimestamp and createTimestamp attributes. Need if Authentik LDAP server is used.", - "type": bool}), ("ldap_uri", { "value": "ldap://localhost", - "help": "URI to the ldap server", + "help": "URI to the LDAP server", "type": str}), ("ldap_base", { "value": "", - "help": "LDAP base DN of the ldap server", + "help": "Base DN of the LDAP server", "type": str}), ("ldap_reader_dn", { "value": "", - "help": "the DN of a ldap user with read access to get the user accounts", + "help": "DN of an LDAP user with read access to users anmd - if defined - groups", "type": str}), ("ldap_secret", { "value": "", - "help": "the password of the ldap_reader_dn", + "help": "Password of ldap_reader_dn (better: use ldap_secret_file)", "type": str}), ("ldap_secret_file", { "value": "", - "help": "path of the file containing the password of the ldap_reader_dn", + "help": "Path to the file containing the password of ldap_reader_dn", "type": str}), ("ldap_filter", { "value": "(cn={0})", - "help": "the search filter to find the user DN to authenticate by the username", + "help": "Filter to search for the LDAP entry of the user to authenticate", "type": str}), ("ldap_user_attribute", { "value": "", - "help": "the attribute to be used as username after authentication", + "help": "Attribute to be used as username after authentication", + "type": str}), + ("ldap_use_ssl", { + "value": "False", + "help": "Use ssl on the LDAP connection. Deprecated, use ldap_security instead!", + "type": bool}), + ("ldap_security", { + "value": "none", + "help": "Encryption mode to be used: *none*|tls|starttls", + "type": str}), + ("ldap_ssl_verify_mode", { + "value": "REQUIRED", + "help": "Certificate verification mode for tls and starttls. NONE, OPTIONAL, default is REQUIRED", + "type": str}), + ("ldap_ssl_ca_file", { + "value": "", + "help": "Path to the CA file in PEM format which is used to certify the server certificate", "type": str}), ("ldap_groups_attribute", { "value": "", - "help": "attribute to read the group memberships from", + "help": "Attribute in the user's LDAP entry to read the group memberships from", "type": str}), ("ldap_group_members_attribute", { "value": "", @@ -309,22 +321,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "", "help": "Search filter to search for groups having the user as member. Only if ldap_group_members_attribute is set", "type": str}), - ("ldap_use_ssl", { - "value": "False", - "help": "Use ssl on the ldap connection. Soon to be deprecated, use ldap_security instead", + ("ldap_ignore_attribute_create_modify_timestamp", { + "value": "false", + "help": "Quirk for Authentik LDAP server: ignore modifyTimestamp and createTimestamp attributes.", "type": bool}), - ("ldap_security", { - "value": "none", - "help": "the encryption mode to be used: *none*|tls|starttls", - "type": str}), - ("ldap_ssl_verify_mode", { - "value": "REQUIRED", - "help": "The certificate verification mode. Works for tls and starttls. NONE, OPTIONAL, default is REQUIRED", - "type": str}), - ("ldap_ssl_ca_file", { - "value": "", - "help": "The path to the CA file in pem format which is used to certificate the server certificate", - "type": str}), ("imap_host", { "value": "localhost", "help": "IMAP server hostname: address|address:port|[address]:port|*localhost*",