Merge pull request #1669 from marschap/more-LDAPauth-patches

More LDAP auth patches
This commit is contained in:
Peter Bieringer 2025-01-04 07:33:47 +00:00 committed by GitHub
commit c853ec4a74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 48 additions and 33 deletions

View file

@ -10,6 +10,7 @@
* Improve: [auth] constant execution time for failed logins independent of external backend or by htpasswd used digest method
* Drop: support for Python 3.8
* Add: option [auth] ldap_user_attribute
* Add: option [auth] ldap_groups_attribute as a more flexible replacement of removed ldap_load_groups
## 3.3.3
* Add: display mtime_ns precision of storage folder with condition warning if too less

View file

@ -926,20 +926,26 @@ The search filter to find the user DN to authenticate by the username. User '{0}
Default: `(cn={0})`
#### ldap_user_attribute
##### ldap_user_attribute
The LDAP attribute whose value shall be used as the user name after successful authentication
Default: not set, i.e. the login name given is used directly.
##### ldap_load_groups
##### ldap_groups_attribute
Load the ldap groups of the authenticated user. These groups can be used later on to define rights. This also gives you access to the group calendars, if they exist.
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 automaticaly. This must be created manually. [Here](https://github.com/Kozea/Radicale/wiki/LDAP-authentication) you can find a script to create group calendar folders https://github.com/Kozea/Radicale/wiki/LDAP-authentication
* 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.
Default: False
Use 'memberOf' if you want to load groups on Active Directory and alikes, 'groupMembership' on Novell eDirectory, ...
Default: unset
##### ldap_use_ssl

4
config
View file

@ -86,8 +86,8 @@
# Path of the file containing password of the reader DN
#ldap_secret_file = /run/secrets/ldap_password
# If the ldap groups of the user need to be loaded
#ldap_load_groups = True
# 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
#ldap_filter = (&(objectClass=person)(uid={0}))

View file

@ -24,7 +24,7 @@ Following parameters are needed in the configuration:
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_load_groups If the groups of the authenticated users need to be loaded
ldap_groups_attribute The attribute containing group memberships in the LDAP user entry
Following parameters controls SSL connections:
ldap_use_ssl If the connection
ldap_ssl_verify_mode The certificate verification mode. NONE, OPTIONAL, default is REQUIRED
@ -43,8 +43,9 @@ class Auth(auth.BaseAuth):
_ldap_reader_dn: str
_ldap_secret: str
_ldap_filter: str
_ldap_attributes: list[str] = []
_ldap_user_attr: str
_ldap_load_groups: bool
_ldap_groups_attr: str
_ldap_module_version: int = 3
_ldap_use_ssl: bool = False
_ldap_ssl_verify_mode: int = ssl.CERT_REQUIRED
@ -65,10 +66,10 @@ class Auth(auth.BaseAuth):
self._ldap_uri = configuration.get("auth", "ldap_uri")
self._ldap_base = configuration.get("auth", "ldap_base")
self._ldap_reader_dn = configuration.get("auth", "ldap_reader_dn")
self._ldap_load_groups = configuration.get("auth", "ldap_load_groups")
self._ldap_secret = configuration.get("auth", "ldap_secret")
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")
ldap_secret_file_path = configuration.get("auth", "ldap_secret_file")
if ldap_secret_file_path:
with open(ldap_secret_file_path, 'r') as file:
@ -85,12 +86,15 @@ class Auth(auth.BaseAuth):
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_load_groups : %s" % self._ldap_load_groups)
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)
else:
logger.info("auth.ldap_user_attribute : (not provided)")
if self._ldap_groups_attr:
logger.info("auth.ldap_groups_attribute: %r" % self._ldap_groups_attr)
else:
logger.info("auth.ldap_groups_attribute: (not provided)")
if ldap_secret_file_path:
logger.info("auth.ldap_secret_file_path: %r" % ldap_secret_file_path)
if self._ldap_secret:
@ -109,6 +113,12 @@ class Auth(auth.BaseAuth):
logger.info("auth.ldap_ssl_ca_file : %r" % self._ldap_ssl_ca_file)
else:
logger.info("auth.ldap_ssl_ca_file : (not provided)")
"""Extend attributes to to be returned in the user query"""
if self._ldap_groups_attr:
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)
def _login2(self, login: str, password: str) -> str:
try:
@ -121,15 +131,11 @@ class Auth(auth.BaseAuth):
"""Search for the dn of user to authenticate"""
escaped_login = self.ldap.filter.escape_filter_chars(login)
logger.debug(f"_login2 login escaped for LDAP filters: {escaped_login}")
attrs = ['memberof']
if self._ldap_user_attr:
attrs = ['memberOf', self._ldap_user_attr]
logger.debug(f"_login2 attrs: {attrs}")
res = conn.search_s(
self._ldap_base,
self.ldap.SCOPE_SUBTREE,
filterstr=self._ldap_filter.format(escaped_login),
attrlist=attrs
attrlist=self._ldap_attributes
)
if len(res) != 1:
"""User could not be found unambiguously"""
@ -150,12 +156,15 @@ class Auth(auth.BaseAuth):
conn.set_option(self.ldap.OPT_REFERRALS, 0)
conn.simple_bind_s(user_dn, password)
tmp: list[str] = []
if self._ldap_load_groups:
if self._ldap_groups_attr:
tmp = []
for g in user_entry[1]['memberOf']:
for g in user_entry[1][self._ldap_groups_attr]:
"""Get group g's RDN's attribute value"""
g = g.decode('utf-8').split(',')[0]
tmp.append(g.partition('=')[2])
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:
@ -198,15 +207,11 @@ class Auth(auth.BaseAuth):
"""Search the user dn"""
escaped_login = self.ldap3.utils.conv.escape_filter_chars(login)
logger.debug(f"_login3 login escaped for LDAP filters: {escaped_login}")
attrs = ['memberof']
if self._ldap_user_attr:
attrs = ['memberOf', self._ldap_user_attr]
logger.debug(f"_login3 attrs: {attrs}")
conn.search(
search_base=self._ldap_base,
search_filter=self._ldap_filter.format(escaped_login),
search_scope=self.ldap3.SUBTREE,
attributes=attrs
attributes=self._ldap_attributes
)
if len(conn.entries) != 1:
"""User could not be found unambiguously"""
@ -224,12 +229,15 @@ class Auth(auth.BaseAuth):
logger.debug(f"_login3 user '{login}' cannot be found")
return ""
tmp: list[str] = []
if self._ldap_load_groups:
if self._ldap_groups_attr:
tmp = []
for g in user_entry['attributes']['memberOf']:
for g in user_entry['attributes'][self._ldap_groups_attr]:
"""Get group g's RDN's attribute value"""
g = g.split(',')[0]
tmp.append(g.partition('=')[2])
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:

View file

@ -247,10 +247,10 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"value": "",
"help": "the attribute to be used as username after authentication",
"type": str}),
("ldap_load_groups", {
"value": "False",
"help": "load the ldap groups of the authenticated user",
"type": bool}),
("ldap_groups_attribute", {
"value": "",
"help": "attribute to read the group memberships from",
"type": str}),
("ldap_use_ssl", {
"value": "False",
"help": "Use ssl on the ldap connection",