From 5582112029475aa65c33fc687700fd91d6ce5c73 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:32:07 +0200 Subject: [PATCH 01/14] add support functions for retrieving path owner/group/permissions --- radicale/pathutils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/radicale/pathutils.py b/radicale/pathutils.py index 46306b2e..31d1845f 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -23,6 +23,7 @@ Helper functions for working with the file system. import errno import os +import pathlib import posixpath import sys import threading @@ -314,3 +315,14 @@ def name_from_path(path: str, collection: "storage.BaseCollection") -> str: raise ValueError("%r is not a component in collection %r" % (name, collection.path)) return name + + +def path_permissions(path): + path = pathlib.Path(path) + return [path.owner(), path.group(), path.stat().st_mode] + + +def path_permissions_as_string(path): + pp = path_permissions(path) + s = "path=%r owner=%s group=%s mode=%o" % (path, pp[0], pp[1], pp[2]) + return s From 003235b117c4d41b5d5bee321bfbe65bbd8b091b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:33:18 +0200 Subject: [PATCH 02/14] log on startup path owner/group/permissions --- radicale/storage/multifilesystem/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 005e6af1..2aa1309a 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -165,10 +165,12 @@ class Storage( if not os.path.exists(self._filesystem_folder): logger.warning("Storage location: %r does not exist, creating now", self._filesystem_folder) self._makedirs_synced(self._filesystem_folder) + logger.info("Storage location permissions: %s", pathutils.path_permissions_as_string(self._filesystem_folder)) logger.info("Storage location subfolder: %r", self._get_collection_root_folder()) if not os.path.exists(self._get_collection_root_folder()): logger.warning("Storage location subfolder: %r does not exist, creating now", self._get_collection_root_folder()) self._makedirs_synced(self._get_collection_root_folder()) + logger.info("Storage location permissions: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder())) logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item) logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history) logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken) @@ -193,3 +195,4 @@ class Storage( if not os.path.exists(self._get_collection_cache_folder()): logger.warning("Storage cache subfolder: %r does not exist, creating now", self._get_collection_cache_folder()) self._makedirs_synced(self._get_collection_cache_folder()) + logger.info("Storage cache subfolder permissions: %s", pathutils.path_permissions_as_string(self._get_collection_cache_folder())) From 704ff23ddf2e6b726b1c56cc329ae4f332096f3b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:33:55 +0200 Subject: [PATCH 03/14] add support function to display user/group radicale is running --- radicale/utils.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/radicale/utils.py b/radicale/utils.py index 4f759f58..25ca7a58 100644 --- a/radicale/utils.py +++ b/radicale/utils.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . +import getpass +import grp +import os +import pwd import ssl import sys from importlib import import_module, metadata @@ -214,3 +218,23 @@ def ssl_get_protocols(context): if (context.minimum_version <= ssl.TLSVersion.TLSv1_3) and (context.maximum_version >= ssl.TLSVersion.TLSv1_3): protocols.append("TLSv1.3") return protocols + + +def user_groups_as_string(): + if sys.platform != "win32": + euid = os.geteuid() + egid = os.getegid() + username = pwd.getpwuid(euid)[0] + gids = os.getgrouplist(username, egid) + groups = [] + for gid in gids: + try: + gi = grp.getgrgid(gid) + groups.append("%s(%d)" % (gi.gr_name, gid)) + except Exception: + groups.append("%s(%d)" % (gid, gid)) + s = "user=%s(%d) groups=%s" % (username, euid, ','.join(groups)) + else: + username = getpass.getuser() + s = "user=%s" % (username) + return s From 9c50f154fdf6126dc6d2446050d3b23633f722c3 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:34:52 +0200 Subject: [PATCH 04/14] display user/group radicale is starting with --- radicale/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/server.py b/radicale/server.py index ac2ef284..91cee68d 100644 --- a/radicale/server.py +++ b/radicale/server.py @@ -297,7 +297,7 @@ def serve(configuration: config.Configuration, info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH") else: info = "" - logger.info("Starting Radicale %s(%s)", info, utils.packages_version()) + logger.info("Starting Radicale %s(%s) as %s", info, utils.packages_version(), utils.user_groups_as_string()) # Copy configuration before modifying configuration = configuration.copy() configuration.update({"server": {"_internal_server": "True"}}, "server", From e36d5f2c014c7cb43826d940f9395ded733ff6c4 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:35:26 +0200 Subject: [PATCH 05/14] catch permissions errors and display owner+user information --- radicale/storage/multifilesystem/__init__.py | 5 ++++- radicale/storage/multifilesystem/base.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 2aa1309a..5ef0514c 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -29,7 +29,7 @@ import sys import time from typing import ClassVar, Iterator, Optional, Type -from radicale import config +from radicale import config, pathutils, utils from radicale.log import logger from radicale.storage.multifilesystem.base import CollectionBase, StorageBase from radicale.storage.multifilesystem.cache import CollectionPartCache @@ -187,6 +187,9 @@ class Storage( logger.info("Storage item mtime resolution test result: %d %s" % (precision_unit, unit)) if self._use_mtime_and_size_for_item_cache is False: logger.info("Storage cache using mtime and size for 'item' may be an option in case of performance issues") + except PermissionError as e: + logger.error("Directory permissions: %s / Effective user: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder()), utils.user_groups_as_string()) + raise e except Exception: logger.warning("Storage item mtime resolution test result not successful") logger.debug("Storage cache action logging: %s", self._debug_cache_actions) diff --git a/radicale/storage/multifilesystem/base.py b/radicale/storage/multifilesystem/base.py index 394e89bf..f0fe3dd5 100644 --- a/radicale/storage/multifilesystem/base.py +++ b/radicale/storage/multifilesystem/base.py @@ -22,7 +22,7 @@ import sys from tempfile import TemporaryDirectory from typing import IO, AnyStr, ClassVar, Iterator, Optional, Type -from radicale import config, pathutils, storage, types +from radicale import config, logger, pathutils, storage, types, utils from radicale.storage import multifilesystem # noqa:F401 @@ -161,7 +161,13 @@ class StorageBase(storage.BaseStorage): # Create parent dirs recursively self._makedirs_synced(parent_filesystem_path) # Possible race! - os.makedirs(filesystem_path, exist_ok=True) + try: + os.makedirs(filesystem_path, exist_ok=True) + except PermissionError as e: + logger.error("Directory permissions: %s / Effective user: %s", pathutils.path_permissions_as_string(parent_filesystem_path), utils.user_groups_as_string()) + raise e + except Exception: + raise self._sync_directory(parent_filesystem_path) if sys.platform != "win32" and self._folder_umask: os.umask(oldmask) From 995eeaf2d0efc5656e717b7135ff564f351dec78 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:35:52 +0200 Subject: [PATCH 06/14] extend copyright --- radicale/pathutils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radicale/pathutils.py b/radicale/pathutils.py index 31d1845f..0902eacf 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -1,7 +1,8 @@ # This file is part of Radicale - CalDAV and CardDAV server # Copyright © 2014 Jean-Marc Martins # Copyright © 2012-2017 Guillaume Ayoub -# Copyright © 2017-2018 Unrud +# Copyright © 2017-2022 Unrud +# Copyright © 2025-2025 Peter Bieringer # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by From 070a022b90de32a54f0446bbd7d5920b4c706d99 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:48:47 +0200 Subject: [PATCH 07/14] fix windows case --- radicale/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/radicale/utils.py b/radicale/utils.py index 25ca7a58..378bb330 100644 --- a/radicale/utils.py +++ b/radicale/utils.py @@ -18,7 +18,6 @@ # along with Radicale. If not, see . import getpass -import grp import os import pwd import ssl @@ -29,6 +28,9 @@ from typing import Callable, Sequence, Tuple, Type, TypeVar, Union from radicale import config from radicale.log import logger +if sys.platform != "win32": + import grp + _T_co = TypeVar("_T_co", covariant=True) RADICALE_MODULES: Sequence[str] = ("radicale", "vobject", "passlib", "defusedxml", From d09c4e0819eec9d40781c2f0fcfb37a3939e8be5 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 08:50:20 +0200 Subject: [PATCH 08/14] next windows case --- radicale/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/utils.py b/radicale/utils.py index 378bb330..c5fa40e6 100644 --- a/radicale/utils.py +++ b/radicale/utils.py @@ -19,7 +19,6 @@ import getpass import os -import pwd import ssl import sys from importlib import import_module, metadata @@ -30,6 +29,7 @@ from radicale.log import logger if sys.platform != "win32": import grp + import pwd _T_co = TypeVar("_T_co", covariant=True) From a4698a44756644bcfc8fbcf07da837439a82ef51 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 09:35:47 +0200 Subject: [PATCH 09/14] catch next windows related issue --- radicale/pathutils.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/radicale/pathutils.py b/radicale/pathutils.py index 0902eacf..b204635a 100644 --- a/radicale/pathutils.py +++ b/radicale/pathutils.py @@ -324,6 +324,9 @@ def path_permissions(path): def path_permissions_as_string(path): - pp = path_permissions(path) - s = "path=%r owner=%s group=%s mode=%o" % (path, pp[0], pp[1], pp[2]) + try: + pp = path_permissions(path) + s = "path=%r owner=%s group=%s mode=%o" % (path, pp[0], pp[1], pp[2]) + except NotImplementedError: + s = "path=%r owner=UNKNOWN(unsupported on this system)" % (path) return s From 56e084f74cc1767afb4cb8a00f9811648852b206 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 19:53:33 +0200 Subject: [PATCH 10/14] fix for windows --- radicale/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/utils.py b/radicale/utils.py index c5fa40e6..1475fdc3 100644 --- a/radicale/utils.py +++ b/radicale/utils.py @@ -237,6 +237,6 @@ def user_groups_as_string(): groups.append("%s(%d)" % (gid, gid)) s = "user=%s(%d) groups=%s" % (username, euid, ','.join(groups)) else: - username = getpass.getuser() + username = os.getlogin() s = "user=%s" % (username) return s From e17ef37c426d0c0692b8ccef57b049e9668f4d26 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 20:00:03 +0200 Subject: [PATCH 11/14] remove no longer required import --- radicale/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/radicale/utils.py b/radicale/utils.py index 1475fdc3..b5b42694 100644 --- a/radicale/utils.py +++ b/radicale/utils.py @@ -17,7 +17,6 @@ # You should have received a copy of the GNU General Public License # along with Radicale. If not, see . -import getpass import os import ssl import sys From 8382ab7f93a56ff256a8f48814cf2d723470d065 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 20:00:18 +0200 Subject: [PATCH 12/14] display platform on startup --- radicale/server.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/radicale/server.py b/radicale/server.py index 91cee68d..e437286a 100644 --- a/radicale/server.py +++ b/radicale/server.py @@ -25,6 +25,7 @@ Built-in WSGI server. import http import os +import platform import select import socket import socketserver @@ -297,7 +298,7 @@ def serve(configuration: config.Configuration, info = "with PYTHONPATH=%r " % os.environ.get("PYTHONPATH") else: info = "" - logger.info("Starting Radicale %s(%s) as %s", info, utils.packages_version(), utils.user_groups_as_string()) + logger.info("Starting Radicale %s(%s) as %s on %s", info, utils.packages_version(), utils.user_groups_as_string(), platform.platform()) # Copy configuration before modifying configuration = configuration.copy() configuration.update({"server": {"_internal_server": "True"}}, "server", From 1aa612780daf9d5ff6fb9ff2b9d05e471bb0754b Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 21:05:18 +0200 Subject: [PATCH 13/14] extend changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5b1c05d..20e1a53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ ## 3.5.5.dev * Improve: [auth] ldap: do not read server info by bind to avoid needless network traffic * Fix: [storage] broken support of 'folder_umask' +* Improve: add details about platform and effective user on startup +* Improve: display owner+permissions on directories on startup, extend error message in case of missing permissions ## 3.5.4 * Improve: item filter enhanced for 3rd level supporting VALARM and honoring TRIGGER (offset or absolute) From 83270efeae01c08330e232bf1a688553d4c00c83 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Jun 2025 21:09:10 +0200 Subject: [PATCH 14/14] cosmetics --- radicale/storage/multifilesystem/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/storage/multifilesystem/__init__.py b/radicale/storage/multifilesystem/__init__.py index 5ef0514c..aeaba732 100644 --- a/radicale/storage/multifilesystem/__init__.py +++ b/radicale/storage/multifilesystem/__init__.py @@ -170,7 +170,7 @@ class Storage( if not os.path.exists(self._get_collection_root_folder()): logger.warning("Storage location subfolder: %r does not exist, creating now", self._get_collection_root_folder()) self._makedirs_synced(self._get_collection_root_folder()) - logger.info("Storage location permissions: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder())) + logger.info("Storage location subfolder permissions: %s", pathutils.path_permissions_as_string(self._get_collection_root_folder())) logger.info("Storage cache subfolder usage for 'item': %s", self._use_cache_subfolder_for_item) logger.info("Storage cache subfolder usage for 'history': %s", self._use_cache_subfolder_for_history) logger.info("Storage cache subfolder usage for 'sync-token': %s", self._use_cache_subfolder_for_synctoken)