Skip to content

Commit

Permalink
LDAP auth: indroduce config option 'ldap_user_attribute'
Browse files Browse the repository at this point in the history
This option gives us
- flexible authentication options where the name used for logging on
  does not have to be the account name
  e.g. use ldap_filter = (&(obhjectclass=inetOrgperson)(|(cn={0]})(mail={0})))
  to allow loginng on using the cn or the mail address
- automatically consistent / canonicalized username values
  (i.e. exactly the way the LDAP server returns them)
  • Loading branch information
marschap committed Jan 2, 2025
1 parent 0253682 commit 99f5ec3
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 9 deletions.
6 changes: 6 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,12 @@ The search filter to find the user DN to authenticate by the username. User '{0}

Default: `(cn={0})`

#### 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

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.
Expand Down
3 changes: 3 additions & 0 deletions config
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
# 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}))

# the attribute holding the value to be used as username after authentication
#ldap_user_attribute = cn

# Use ssl on the ldap connection
#ldap_use_ssl = False

Expand Down
42 changes: 33 additions & 9 deletions radicale/auth/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
"""
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_load_groups If the groups of the authenticated users need to be loaded
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_load_groups If the groups of the authenticated users need to be loaded
Following parameters controls SSL connections:
ldap_use_ssl If the connection
ldap_ssl_verify_mode The certificate verification mode. NONE, OPTIONAL, default is REQUIRED
Expand All @@ -42,6 +43,7 @@ class Auth(auth.BaseAuth):
_ldap_reader_dn: str
_ldap_secret: str
_ldap_filter: str
_ldap_user_attr: str
_ldap_load_groups: bool
_ldap_module_version: int = 3
_ldap_use_ssl: bool = False
Expand All @@ -66,6 +68,7 @@ def __init__(self, configuration: config.Configuration) -> None:
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")
ldap_secret_file_path = configuration.get("auth", "ldap_secret_file")
if ldap_secret_file_path:
with open(ldap_secret_file_path, 'r') as file:
Expand All @@ -84,6 +87,10 @@ def __init__(self, configuration: config.Configuration) -> None:
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 ldap_secret_file_path:
logger.info("auth.ldap_secret_file_path: %r" % ldap_secret_file_path)
if self._ldap_secret:
Expand Down Expand Up @@ -114,11 +121,15 @@ def _login2(self, login: str, password: str) -> str:
"""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=['memberOf']
attrlist=attrs
)
if len(res) != 1:
"""User could not be found unambiguously"""
Expand Down Expand Up @@ -147,6 +158,11 @@ def _login2(self, login: str, password: str) -> str:
tmp.append(g.partition('=')[2])
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}'")
conn.unbind()
logger.debug(f"_login2 {login} successfully authenticated")
return login
Expand Down Expand Up @@ -182,11 +198,15 @@ def _login3(self, login: str, password: str) -> str:
"""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=['memberOf']
attributes=attrs
)
if len(conn.entries) != 1:
"""User could not be found unambiguously"""
Expand All @@ -212,6 +232,10 @@ def _login3(self, login: str, password: str) -> str:
tmp.append(g.partition('=')[2])
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]:
login = user_entry['attributes'][self._ldap_user_attr][0]
logger.debug(f"_login3 user set to: '{login}'")
conn.unbind()
logger.debug(f"_login3 {login} successfully authenticated")
return login
Expand Down
4 changes: 4 additions & 0 deletions radicale/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ def json_str(value: Any) -> dict:
"value": "(cn={0})",
"help": "the 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",
"type": str}),
("ldap_load_groups", {
"value": "False",
"help": "load the ldap groups of the authenticated user",
Expand Down

0 comments on commit 99f5ec3

Please sign in to comment.