Browse Source

Merge pull request #1814 from pbiering/improve-writeability-check-on-startup

Improve writeability check on startup
Peter Bieringer 8 months ago
parent
commit
1c97345fc9

+ 2 - 0
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)

+ 17 - 1
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 <unrud@outlook.com>
+# Copyright © 2017-2022 Unrud <unrud@outlook.com>
+# Copyright © 2025-2025 Peter Bieringer <pb@bieringer.de>
 #
 # 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
@@ -23,6 +24,7 @@ Helper functions for working with the file system.
 
 import errno
 import os
+import pathlib
 import posixpath
 import sys
 import threading
@@ -314,3 +316,17 @@ 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):
+    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

+ 2 - 1
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)", info, utils.packages_version())
+    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",

+ 7 - 1
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
@@ -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 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)
@@ -185,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)
@@ -193,3 +198,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()))

+ 8 - 2
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)

+ 25 - 0
radicale/utils.py

@@ -17,6 +17,7 @@
 # You should have received a copy of the GNU General Public License
 # along with Radicale.  If not, see <http://www.gnu.org/licenses/>.
 
+import os
 import ssl
 import sys
 from importlib import import_module, metadata
@@ -25,6 +26,10 @@ from typing import Callable, Sequence, Tuple, Type, TypeVar, Union
 from radicale import config
 from radicale.log import logger
 
+if sys.platform != "win32":
+    import grp
+    import pwd
+
 _T_co = TypeVar("_T_co", covariant=True)
 
 RADICALE_MODULES: Sequence[str] = ("radicale", "vobject", "passlib", "defusedxml",
@@ -214,3 +219,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 = os.getlogin()
+        s = "user=%s" % (username)
+    return s