diff --git a/config b/config index cee373d3..61008501 100644 --- a/config +++ b/config @@ -93,8 +93,14 @@ # Path of the file containing 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 attribute in user entry to read the group memberships. +#ldap_groups_attribute = + +# The attribute in group entries to read the group memberships. +#ldap_group_members_attribute = + +# The base dn to find the groups. Necessary only if ldap_group_members_attribute is defined and other then ldap_base. +#ldap_groups_base = # 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})) diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 76b4a1a1..c894850c 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -27,7 +27,6 @@ from http import client from typing import Dict, Optional, cast import defusedxml.ElementTree as DefusedET - import radicale.item as radicale_item from radicale import httputils, storage, types, xmlutils from radicale.app.base import Access, ApplicationBase diff --git a/radicale/auth/htpasswd.py b/radicale/auth/htpasswd.py index dd66dfbd..0615dcbb 100644 --- a/radicale/auth/htpasswd.py +++ b/radicale/auth/htpasswd.py @@ -60,7 +60,6 @@ import time from typing import Any, Tuple from passlib.hash import apr_md5_crypt, sha256_crypt, sha512_crypt - from radicale import auth, config, logger diff --git a/radicale/auth/ldap.py b/radicale/auth/ldap.py index e45aeb29..2e6635b6 100644 --- a/radicale/auth/ldap.py +++ b/radicale/auth/ldap.py @@ -18,13 +18,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_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. 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 @@ -78,6 +79,10 @@ 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 ldap_secret_file_path = configuration.get("auth", "ldap_secret_file") if ldap_secret_file_path: with open(ldap_secret_file_path, 'r') as file: @@ -172,17 +177,25 @@ 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: - 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)) + 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})".format(self._ldap_group_members_attr,user_dn), + attrlist=self._ldap_attributes + ) + for g in gdns: + """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] @@ -261,17 +274,28 @@ 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: - 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)) + 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})".format(self._ldap_group_members_attr,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: + 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 c4f5fbe7..90b3a9d5 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -269,31 +269,39 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "type": str}), ("ldap_base", { "value": "", - "help": "LDAP base DN of the ldap server", + "help": "The 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": "The 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": "The password of the ldap_reader_dn", "type": str}), ("ldap_secret_file", { "value": "", - "help": "path of the file containing the password of the ldap_reader_dn", + "help": "Path of the file containing the password of the 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": "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", + "help": "The attribute to be used as username after authentication", "type": str}), ("ldap_groups_attribute", { "value": "", - "help": "attribute to read the group memberships from", + "help": "Attribute in the user entry to read the group memberships from.", + "type": str}), + ("ldap_group_members_attribute", { + "value": "", + "help": "Attribute in the group entries to read the group memberships from.", + "type": str}), + ("ldap_groups_base_dn", { + "value": "", + "help": "The base dn to find the groups. Necessary only if ldap_group_members_attribute is defined and other then ldap_base.", "type": str}), ("ldap_use_ssl", { "value": "False", diff --git a/radicale/hook/rabbitmq/__init__.py b/radicale/hook/rabbitmq/__init__.py index 2323ed43..5b1ec659 100644 --- a/radicale/hook/rabbitmq/__init__.py +++ b/radicale/hook/rabbitmq/__init__.py @@ -1,6 +1,5 @@ import pika from pika.exceptions import ChannelWrongStateError, StreamLostError - from radicale import hook from radicale.hook import HookNotificationItem from radicale.log import logger diff --git a/radicale/tests/__init__.py b/radicale/tests/__init__.py index e5ecb1f9..23df6d54 100644 --- a/radicale/tests/__init__.py +++ b/radicale/tests/__init__.py @@ -31,9 +31,9 @@ from io import BytesIO from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import quote -import defusedxml.ElementTree as DefusedET import vobject +import defusedxml.ElementTree as DefusedET import radicale from radicale import app, config, types, xmlutils diff --git a/radicale/tests/test_auth.py b/radicale/tests/test_auth.py index 88cd3ea4..9036c500 100644 --- a/radicale/tests/test_auth.py +++ b/radicale/tests/test_auth.py @@ -29,7 +29,6 @@ import sys from typing import Iterable, Tuple, Union import pytest - from radicale import xmlutils from radicale.tests import BaseTest diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index d2b26949..8cd3279a 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -26,9 +26,9 @@ import os import posixpath from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple -import defusedxml.ElementTree as DefusedET import vobject +import defusedxml.ElementTree as DefusedET from radicale import storage, xmlutils from radicale.tests import RESPONSES, BaseTest from radicale.tests.helpers import get_file_content diff --git a/radicale/tests/test_config.py b/radicale/tests/test_config.py index 92ece9a6..7b46a601 100644 --- a/radicale/tests/test_config.py +++ b/radicale/tests/test_config.py @@ -21,7 +21,6 @@ from configparser import RawConfigParser from typing import List, Tuple import pytest - from radicale import config, types from radicale.tests.helpers import configuration_to_dict diff --git a/radicale/tests/test_server.py b/radicale/tests/test_server.py index b344dddf..d1887b63 100644 --- a/radicale/tests/test_server.py +++ b/radicale/tests/test_server.py @@ -34,7 +34,6 @@ from urllib import request from urllib.error import HTTPError, URLError import pytest - from radicale import config, server from radicale.tests import BaseTest from radicale.tests.helpers import configuration_to_dict, get_file_path diff --git a/radicale/tests/test_storage.py b/radicale/tests/test_storage.py index 89572bca..53dc1669 100644 --- a/radicale/tests/test_storage.py +++ b/radicale/tests/test_storage.py @@ -26,7 +26,6 @@ import shutil from typing import ClassVar, cast import pytest - import radicale.tests.custom.storage_simple_sync from radicale.tests import BaseTest from radicale.tests.helpers import get_file_content