diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index 2d30ac9c..81ea518d 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -17,19 +17,19 @@ """ 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 searching for users. - 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 - ldap_groups_base The baseDN of the LDAP server searching for groups. + ldap_uri LDAP URL to the server like ldap://localhost + ldap_base BaseDN of the LDAP server searching for users. + ldap_reader_dn DN of a LDAP user with read access to get the user accounts + ldap_secret Password of the ldap_reader_dn + ldap_secret_file Path of the file containing the password of the ldap_reader_dn + ldap_filter Search filter to find the user to authenticate by the username + ldap_user_attribute Attribute to be used as username after authentication + ldap_groups_attribute Attribute containing group memberships in the LDAP user entry + ldap_group_base BaseDN of the LDAP server searching for groups. 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_security Encryption mode to be used: *none*|tls|starttls + ldap_ssl_verify_mode Certificate verification mode. Works for tls and starttls. NONE, OPTIONAL, default is REQUIRED ldap_ssl_ca_file """ @@ -49,8 +49,8 @@ class Auth(auth.BaseAuth): _ldap_user_attr: str _ldap_groups_attr: str _ldap_group_members_attr: str - _ldap_groups_base: str - _ldap_groups_filter: str + _ldap_group_base: str + _ldap_group_filter: str _ldap_module_version: int = 3 _ldap_use_ssl: bool = False _ldap_security: str = "none" @@ -82,11 +82,11 @@ 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_members_attr = configuration.get("auth", "ldap_group_members_attribute") - self._ldap_groups_base = configuration.get("auth", "ldap_groups_base") - if self._ldap_groups_base == "": - self._ldap_groups_base = self._ldap_base - self._ldap_groups_filter = configuration.get("auth", "ldap_groups_filter") + self._ldap_group_members_attr = configuration.get("auth", "ldap_group_member_attribute") + self._ldap_group_base = configuration.get("auth", "ldap_group_base") + if self._ldap_group_base == "": + self._ldap_group_base = self._ldap_base + self._ldap_group_filter = configuration.get("auth", "ldap_group_filter") ldap_secret_file_path = configuration.get("auth", "ldap_secret_file") if ldap_secret_file_path: with open(ldap_secret_file_path, 'r') as file: @@ -111,7 +111,7 @@ class Auth(auth.BaseAuth): 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_groups_base : %r" % self._ldap_groups_base) + logger.info("auth.ldap_group_base : %r" % self._ldap_group_base) if self._ldap_user_attr: logger.info("auth.ldap_user_attribute : %r" % self._ldap_user_attr) else: @@ -124,10 +124,10 @@ class Auth(auth.BaseAuth): logger.info("auth.ldap_group_members_attr: %r" % self._ldap_group_members_attr) else: logger.info("auth.ldap_group_members_attr: (not provided)") - if self._ldap_groups_filter: - logger.info("auth.ldap_groups_filter: %r" % self._ldap_groups_filter) + if self._ldap_group_filter: + logger.info("auth.ldap_group_filter: %r" % self._ldap_group_filter) else: - logger.info("auth.ldap_groups_filter: (not provided)") + logger.info("auth.ldap_group_filter: (not provided)") if ldap_secret_file_path: logger.info("auth.ldap_secret_file_path: %r" % ldap_secret_file_path) if self._ldap_secret: @@ -155,6 +155,7 @@ class Auth(auth.BaseAuth): logger.info("ldap_attributes : %r" % self._ldap_attributes) def _login2(self, login: str, password: str) -> str: + gdns: list[str] = [] try: """Bind as reader dn""" logger.debug(f"_login2 {self._ldap_uri}, {self._ldap_reader_dn}") @@ -178,6 +179,16 @@ class Auth(auth.BaseAuth): user_entry = res[0] user_dn = user_entry[0] logger.debug(f"_login2 found LDAP user DN {user_dn}") + if self._ldap_group_members_attr: + 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=self._ldap_attributes + ) """Close LDAP connection""" conn.unbind() except Exception as e: @@ -190,19 +201,8 @@ class Auth(auth.BaseAuth): conn.set_option(self.ldap.OPT_REFERRALS, 0) conn.simple_bind_s(user_dn, password) tmp: list[str] = [] - gdns: list[str] = [] if self._ldap_groups_attr: gdns = user_entry[1][self._ldap_groups_attr] - elif self._ldap_group_members_attr: - res = conn.search_s( - self._ldap_groups_base, - self.ldap.SCOPE_SUBTREE, - filterstr="(&{0}({1}={2}))".format( - self._ldap_groups_filter, - self._ldap_group_members_attr, - self.ldap.filter.escape_filter_chars(user_dn)), - attrlist=self._ldap_attributes - ) for g in gdns: """Get group g's RDN's attribute value""" try: @@ -224,6 +224,7 @@ class Auth(auth.BaseAuth): return "" def _login3(self, login: str, password: str) -> str: + gdns: list[str] = [] """Connect the server""" try: logger.debug(f"_login3 {self._ldap_uri}, {self._ldap_reader_dn}") @@ -274,8 +275,20 @@ class Auth(auth.BaseAuth): return "" user_entry = conn.response[0] - conn.unbind() user_dn = user_entry['dn'] + if self._ldap_group_members_attr: + 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="dn" + ) + for group in conn.response: + gdns.append(group['dn']) + conn.unbind() logger.debug(f"_login3 found LDAP user DN {user_dn}") try: """Try to bind as the user itself""" @@ -290,22 +303,9 @@ class Auth(auth.BaseAuth): logger.debug(f"_login3 user '{login}' cannot be found") return "" tmp: list[str] = [] - gdns: list[str] = [] """Let's collect the groups of the user.""" if self._ldap_groups_attr: gdns = user_entry['attributes'][self._ldap_groups_attr] - elif self._ldap_group_members_attr: - conn.search( - search_base=self._ldap_groups_base, - search_filter="(&{0}({1}={2}))".format( - self._ldap_groups_filter, - self._ldap_group_members_attr, - self.ldap3.utils.conv.escape_filter_chars(user_dn)), - search_scope=self.ldap3.SUBTREE, - attributes="dn" - ) - for group in conn.response: - gdns.append(group['dn']) for g in gdns: """Get group g's RDN's attribute value""" try: diff --git a/radicale/config.py b/radicale/config.py index d9e7a544..73e3aa06 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -269,15 +269,15 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "type": str}), ("ldap_base", { "value": "", - "help": "The base DN of the ldap server where the user can be find.", + "help": "Base DN of the ldap server where the user can be find.", "type": str}), ("ldap_reader_dn", { "value": "", - "help": "The DN of a ldap user with read access to get the user accounts", + "help": "DN of a ldap user with read access to get the user accounts", "type": str}), ("ldap_secret", { "value": "", - "help": "The password of the ldap_reader_dn", + "help": "Password of the ldap_reader_dn", "type": str}), ("ldap_secret_file", { "value": "", @@ -285,27 +285,27 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "type": str}), ("ldap_filter", { "value": "(cn={0})", - "help": "The search filter to find the user DN to authenticate by the username", + "help": "Search filter to find the user DN to authenticate by the username", "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_groups_attribute", { "value": "", "help": "Attribute in the user entry to read the group memberships from.", "type": str}), - ("ldap_group_members_attribute", { + ("ldap_group_member_attribute", { "value": "", "help": "Attribute in the group entries to read the group members from.", "type": str}), - ("ldap_groups_base", { + ("ldap_group_base", { "value": "", - "help": "The base dn to find the groups. Necessary only if ldap_group_members_attribute is defined and different from ldap_base.", + "help": "Base DN to find the groups. Necessary only if ldap_group_member_attribute is defined and different from ldap_base.", "type": str}), - ("ldap_groups_filter", { + ("ldap_group_filter", { "value": "", - "help": "Additional filter to find the groups when ldap_group_members_attribute is defined. The following filter will be built (&{ldap_groups_filter}({ldap_group_members_attribute}={user_dn})", + "help": "Additional filter to find the groups when ldap_group_member_attribute is defined. Following filter will be built (&{ldap_group_filter}({ldap_group_member_attribute}={user_dn})", "type": str}), ("ldap_use_ssl", { "value": "False", @@ -313,15 +313,15 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "type": bool}), ("ldap_security", { "value": "none", - "help": "the encryption mode to be used: *none*|tls|starttls", + "help": "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", + "help": "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", + "help": "Path to the CA file in pem format which is used to certificate the server certificate", "type": str}), ("imap_host", { "value": "localhost",