From 9945a9f65a649148e6b9a64f55222fe28bf4e296 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dipl=2E=20Ing=2E=20P=C3=A9ter=20Varkoly?= Date: Sat, 21 Sep 2024 18:37:04 +0200 Subject: [PATCH 1/6] Enhance comments. Remove duplicate entry --- config | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config b/config index f1037a81..0751fa75 100644 --- a/config +++ b/config @@ -59,7 +59,7 @@ # URI to the LDAP server #ldap_uri = ldap://localhost -# The base DN of the LDAP server +# The base DN where the user accounts have to be searched #ldap_base = ##BASE_DN## # The reader DN of the LDAP server @@ -71,8 +71,8 @@ # If the ldap groups of the user need to be loaded #ldap_load_groups = True -# Value: none | htpasswd | remote_user | http_x_remote_user | denyall -#type = none +# The filter to find the DN of the user. This filter must contain a python-style placeholder for the login +#ldap_filter = (&(objectClass=person)(cn={0})) # Htpasswd filename #htpasswd_filename = /etc/radicale/users From 98c5ffdc87db3893130f430229808e61bee096aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dipl=2E=20Ing=2E=20P=C3=A9ter=20Varkoly?= Date: Sat, 21 Sep 2024 18:39:39 +0200 Subject: [PATCH 2/6] Increase performace: open and parse rigts file only by starting. Hanlde right sections without user. --- radicale/rights/from_file.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/radicale/rights/from_file.py b/radicale/rights/from_file.py index 03c1799c..0b5e9093 100644 --- a/radicale/rights/from_file.py +++ b/radicale/rights/from_file.py @@ -49,33 +49,36 @@ class Rights(rights.BaseRights): super().__init__(configuration) self._filename = configuration.get("rights", "file") self._log_rights_rule_doesnt_match_on_debug = configuration.get("logging", "rights_rule_doesnt_match_on_debug") + self._rights_config = configparser.ConfigParser() + try: + with open(self._filename, "r") as f: + self._rights_config.read_file(f) + logger.debug("Read rights file") + except Exception as e: + raise RuntimeError("Failed to load rights file %r: %s" % + (self._filename, e)) from e def authorization(self, user: str, path: str) -> str: user = user or "" sane_path = pathutils.strip_path(path) # Prevent "regex injection" escaped_user = re.escape(user) - rights_config = configparser.ConfigParser() - try: - with open(self._filename, "r") as f: - rights_config.read_file(f) - except Exception as e: - raise RuntimeError("Failed to load rights file %r: %s" % - (self._filename, e)) from e if not self._log_rights_rule_doesnt_match_on_debug: logger.debug("logging of rules which doesn't match suppressed by config/option [logging] rights_rule_doesnt_match_on_debug") - for section in rights_config.sections(): + for section in self._rights_config.sections(): group_match = False + user_match = False try: - user_pattern = rights_config.get(section, "user") - collection_pattern = rights_config.get(section, "collection") - allowed_groups = rights_config.get(section, "groups", fallback="").split(",") + user_pattern = self._rights_config.get(section, "user", fallback="") + collection_pattern = self._rights_config.get(section, "collection") + allowed_groups = self._rights_config.get(section, "groups", fallback="").split(",") try: group_match = len(self._user_groups.intersection(allowed_groups)) > 0 except Exception: pass # Use empty format() for harmonized handling of curly braces - user_match = re.fullmatch(user_pattern.format(), user) + if user_pattern != "": + user_match = re.fullmatch(user_pattern.format(), user) user_collection_match = user_match and re.fullmatch( collection_pattern.format( *(re.escape(s) for s in user_match.groups()), @@ -85,13 +88,13 @@ class Rights(rights.BaseRights): raise RuntimeError("Error in section %r of rights file %r: " "%s" % (section, self._filename, e)) from e if user_match and user_collection_match: - permission = rights_config.get(section, "permissions") + permission = self._rights_config.get(section, "permissions") logger.debug("Rule %r:%r matches %r:%r from section %r permission %r", user, sane_path, user_pattern, collection_pattern, section, permission) return permission if group_match and group_collection_match: - permission = rights_config.get(section, "permissions") + permission = self._rights_config.get(section, "permissions") logger.debug("Rule %r:%r matches %r:%r from section %r permission %r by group membership", user, sane_path, user_pattern, collection_pattern, section, permission) From a272d3039e8fd28764c922687db3a4b997067f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dipl=2E=20Ing=2E=20P=C3=A9ter=20Varkoly?= Date: Sun, 22 Sep 2024 16:56:53 +0200 Subject: [PATCH 3/6] Implement using group calenders. Based on the ldap groups the user is member of group calender usage is implemented. The group calenders must be placed in the GROUPS directory based under collection_root_folder. The name of the group calender directory is the base64 encoded name of the group to avoid trouble with spaces and special characters in name. If the directory does not exist the group will be ignored. --- radicale/app/propfind.py | 3 ++- radicale/storage/multifilesystem/discover.py | 14 +++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 009c61dc..6a3cea6d 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -392,7 +392,8 @@ class ApplicationPartPropfind(ApplicationBase): return httputils.REQUEST_TIMEOUT with self._storage.acquire_lock("r", user): items_iter = iter(self._storage.discover( - path, environ.get("HTTP_DEPTH", "0"))) + path, environ.get("HTTP_DEPTH", "0"), + None, self._rights._user_groups)) # take root item for rights checking item = next(items_iter, None) if not item: diff --git a/radicale/storage/multifilesystem/discover.py b/radicale/storage/multifilesystem/discover.py index 00316141..9a951764 100644 --- a/radicale/storage/multifilesystem/discover.py +++ b/radicale/storage/multifilesystem/discover.py @@ -18,6 +18,7 @@ import os import posixpath +import base64 from typing import Callable, ContextManager, Iterator, Optional, cast from radicale import pathutils, types @@ -36,7 +37,8 @@ class StoragePartDiscover(StorageBase): def discover( self, path: str, depth: str = "0", child_context_manager: Optional[ - Callable[[str, Optional[str]], ContextManager[None]]] = None + Callable[[str, Optional[str]], ContextManager[None]]] = None, + user_groups: Set[str] = set([]) ) -> Iterator[types.CollectionOrItem]: # assert isinstance(self, multifilesystem.Storage) if child_context_manager is None: @@ -102,3 +104,13 @@ class StoragePartDiscover(StorageBase): with child_context_manager(sane_child_path, None): yield self._collection_class( cast(multifilesystem.Storage, self), child_path) + for group in user_groups: + href = base64.b64encode(group.encode('utf-8')).decode('ascii') + logger.debug(f"searching for group calendar {group} {href}") + sane_child_path = f"GROUPS/{href}" + if not os.path.isdir(pathutils.path_to_filesystem(folder, sane_child_path)): + continue + child_path = f"/GROUPS/{href}/" + with child_context_manager(sane_child_path, None): + yield self._collection_class( + cast(multifilesystem.Storage, self), child_path) From d1ceb620e4d416250c5218bfd0d2bb6de72af11e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dipl=2E=20Ing=2E=20P=C3=A9ter=20Varkoly?= Date: Sun, 22 Sep 2024 18:38:21 +0200 Subject: [PATCH 4/6] Adapt function template discovery to the implementation --- radicale/rights/from_file.py | 4 ++-- radicale/storage/__init__.py | 8 ++++++-- radicale/storage/multifilesystem/discover.py | 9 +++++---- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/radicale/rights/from_file.py b/radicale/rights/from_file.py index 0b5e9093..20928a64 100644 --- a/radicale/rights/from_file.py +++ b/radicale/rights/from_file.py @@ -66,8 +66,8 @@ class Rights(rights.BaseRights): if not self._log_rights_rule_doesnt_match_on_debug: logger.debug("logging of rules which doesn't match suppressed by config/option [logging] rights_rule_doesnt_match_on_debug") for section in self._rights_config.sections(): - group_match = False - user_match = False + group_match = None + user_match = None try: user_pattern = self._rights_config.get(section, "user", fallback="") collection_pattern = self._rights_config.get(section, "collection") diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py index 6946f59b..f034b337 100644 --- a/radicale/storage/__init__.py +++ b/radicale/storage/__init__.py @@ -24,6 +24,7 @@ Take a look at the class ``BaseCollection`` if you want to implement your own. """ import json +from typing import Callable, ContextManager, Iterator, Optional, Set, cast import xml.etree.ElementTree as ET from hashlib import sha256 from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set, @@ -282,8 +283,11 @@ class BaseStorage: """ self.configuration = configuration - def discover(self, path: str, depth: str = "0") -> Iterable[ - "types.CollectionOrItem"]: + def discover( + self, path: str, depth: str = "0", + child_context_manager: Optional[ + Callable[[str, Optional[str]], ContextManager[None]]] = None, + user_groups: Set[str] = set([])) -> Iterable["types.CollectionOrItem"]: """Discover a list of collections under the given ``path``. ``path`` is sanitized. diff --git a/radicale/storage/multifilesystem/discover.py b/radicale/storage/multifilesystem/discover.py index 9a951764..97d31930 100644 --- a/radicale/storage/multifilesystem/discover.py +++ b/radicale/storage/multifilesystem/discover.py @@ -19,7 +19,7 @@ import os import posixpath import base64 -from typing import Callable, ContextManager, Iterator, Optional, cast +from typing import Callable, ContextManager, Iterator, Optional, Set, cast from radicale import pathutils, types from radicale.log import logger @@ -36,9 +36,10 @@ def _null_child_context_manager(path: str, class StoragePartDiscover(StorageBase): def discover( - self, path: str, depth: str = "0", child_context_manager: Optional[ - Callable[[str, Optional[str]], ContextManager[None]]] = None, - user_groups: Set[str] = set([]) + self, path: str, depth: str = "0", + child_context_manager: Optional[ + Callable[[str, Optional[str]], ContextManager[None]]] = None, + user_groups: Set[str] = set([]) ) -> Iterator[types.CollectionOrItem]: # assert isinstance(self, multifilesystem.Storage) if child_context_manager is None: From 97479190e85cfad64e97679f088ee996ce93823c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dipl=2E=20Ing=2E=20P=C3=A9ter=20Varkoly?= Date: Sun, 22 Sep 2024 18:57:48 +0200 Subject: [PATCH 5/6] Adapt imports. --- radicale/storage/__init__.py | 5 ++--- radicale/storage/multifilesystem/discover.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py index f034b337..4e2e95dd 100644 --- a/radicale/storage/__init__.py +++ b/radicale/storage/__init__.py @@ -24,11 +24,10 @@ Take a look at the class ``BaseCollection`` if you want to implement your own. """ import json -from typing import Callable, ContextManager, Iterator, Optional, Set, cast import xml.etree.ElementTree as ET from hashlib import sha256 -from typing import (Iterable, Iterator, Mapping, Optional, Sequence, Set, - Tuple, Union, overload) +from typing import (Callable, ContextManager, Iterable, Iterator, Mapping, + Optional, Sequence, Set, Tuple, Union, cast, overload) import vobject diff --git a/radicale/storage/multifilesystem/discover.py b/radicale/storage/multifilesystem/discover.py index 97d31930..5cff9789 100644 --- a/radicale/storage/multifilesystem/discover.py +++ b/radicale/storage/multifilesystem/discover.py @@ -16,9 +16,9 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import base64 import os import posixpath -import base64 from typing import Callable, ContextManager, Iterator, Optional, Set, cast from radicale import pathutils, types From ccb59444c3bd168c890eb7e44a45e7f8daadb50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dipl=2E=20Ing=2E=20P=C3=A9ter=20Varkoly?= Date: Sun, 22 Sep 2024 19:01:09 +0200 Subject: [PATCH 6/6] Remove trailing whitespaces and unsused import. --- radicale/storage/__init__.py | 6 +++--- radicale/storage/multifilesystem/discover.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/radicale/storage/__init__.py b/radicale/storage/__init__.py index 4e2e95dd..73cf77b9 100644 --- a/radicale/storage/__init__.py +++ b/radicale/storage/__init__.py @@ -27,7 +27,7 @@ import json import xml.etree.ElementTree as ET from hashlib import sha256 from typing import (Callable, ContextManager, Iterable, Iterator, Mapping, - Optional, Sequence, Set, Tuple, Union, cast, overload) + Optional, Sequence, Set, Tuple, Union, overload) import vobject @@ -283,9 +283,9 @@ class BaseStorage: self.configuration = configuration def discover( - self, path: str, depth: str = "0", + self, path: str, depth: str = "0", child_context_manager: Optional[ - Callable[[str, Optional[str]], ContextManager[None]]] = None, + Callable[[str, Optional[str]], ContextManager[None]]] = None, user_groups: Set[str] = set([])) -> Iterable["types.CollectionOrItem"]: """Discover a list of collections under the given ``path``. diff --git a/radicale/storage/multifilesystem/discover.py b/radicale/storage/multifilesystem/discover.py index 5cff9789..a635906a 100644 --- a/radicale/storage/multifilesystem/discover.py +++ b/radicale/storage/multifilesystem/discover.py @@ -36,9 +36,9 @@ def _null_child_context_manager(path: str, class StoragePartDiscover(StorageBase): def discover( - self, path: str, depth: str = "0", + self, path: str, depth: str = "0", child_context_manager: Optional[ - Callable[[str, Optional[str]], ContextManager[None]]] = None, + Callable[[str, Optional[str]], ContextManager[None]]] = None, user_groups: Set[str] = set([]) ) -> Iterator[types.CollectionOrItem]: # assert isinstance(self, multifilesystem.Storage)