From 1cb622e90305f5a4118b82003a9a4b20f9e3eb91 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 26 Feb 2026 08:14:49 +0100 Subject: [PATCH] sharing: add global and conditional permissions support --- DOCUMENTATION.md | 38 ++++++++++++++++++++++++++++++++++ config | 16 +++++++++++++++ radicale/config.py | 18 +++++++++++++++- radicale/rights/__init__.py | 4 ++++ radicale/sharing/__init__.py | 40 ++++++++++++++++++++++++++++++++---- 5 files changed, 111 insertions(+), 5 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 37838ed6..781f7c17 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -2102,6 +2102,40 @@ Share collection by map Default: `false` +##### permit_create_token + +_(>= 3.7.0)_ + +Permit create of token-based sharing + +Default: `false` + +* If `False` it can be explicitly granted by `permissions: t` +* If `True` it can be explicitly forbidden by `permissions: T` + +##### permit_create_map + +_(>= 3.7.0)_ + +Permit create of map-based sharing + +Default: `false` + +* If `False` it can be explicitly granted by `permissions: m` +* If `True` it can be explicitly forbidden by `permissions: M` + +##### default_permissions_create_token + +Default permissions for create token-based sharing + +Default: `r` + +##### default_permissions_create_map + +Default permissions for map-based sharing + +Default: `r` + ## Supported Clients Radicale has been tested with: @@ -2326,6 +2360,10 @@ The following `permissions` are recognized: * **d:** deny deleting a collection in case `permit_delete_collection=True` _(>= 3.3.0)_ * **O:** allow overwriting a collection in case `permit_overwrite_collection=False` _(>= 3.3.0)_ * **o:** deny overwriting a collection in case `permit_overwrite_collection=True` _(>= 3.3.0)_ +* **T:** permit create of token-based sharing of collection in case `permit_create_token=False` _(>= 3.7.0)_ +* **t:** deny create of token-based sharing of collection in case `permit_create_token=True` _(>= 3.7.0)_ +* **M:** permit create of map-based sharing of collection in case `permit_create_map= False` _(>= 3.7.0)_ +* **m:** deny create of map-based sharing of collection in case `permit_create_map=True` _(>= 3.7.0)_ ### Storage diff --git a/config b/config index 6728142f..8f89e223 100644 --- a/config +++ b/config @@ -317,6 +317,22 @@ # Share collection by map #collection_by_map = false +# Permit create of token-based sharing +# If False it can be explicitly granted by permissions: t +# If True it can be explicitly forbidden by permissions: T +#permit_create_token = false + +# Permit create of map-based sharing +# If False it can be explicitly granted by permissions: m +# If True it can be explicitly forbidden by permissions: M +#permit_create_map = false + +# Default permissions for token-based sharing +#default_permissions_create_token = r + +# Default permissions for map-based sharing +#default_permissions_create_map = r + [web] diff --git a/radicale/config.py b/radicale/config.py index 54058d32..abfa91e7 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -482,7 +482,23 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ ("collection_by_map", { "value": "false", "help": "enable sharing of collection by map", - "type": bool})])), + "type": bool}), + ("permit_create_token", { + "value": "false", + "help": "permit create of token-based sharing", + "type": bool}), + ("permit_create_map", { + "value": "false", + "help": "permit create of map-based sharing", + "type": bool}), + ("default_permissions_create_token", { + "value": "r", + "help": "default permissions for token-based sharing", + "type": rights_permission}), + ("default_permissions_create_map", { + "value": "r", + "help": "default permissions for map-based sharing", + "type": rights_permission})])), ("hook", OrderedDict([ ("type", { "value": "none", diff --git a/radicale/rights/__init__.py b/radicale/rights/__init__.py index c70f0f6e..a232598d 100644 --- a/radicale/rights/__init__.py +++ b/radicale/rights/__init__.py @@ -31,6 +31,10 @@ Permissions: - d: deny deleting a collection in case permit_delete_collection=True (>= 3.3.0) - O: allow overwriting a collection in case permit_overwrite_collection=False (>= 3.3.0) - o: deny overwriting a collection in case permit_overwrite_collection=True (>= 3.3.0) + - T: permit create of token-based sharing of collection in case permit_create_token=False (>= 3.7.0) + - t: deny create of token-based sharing of collection in case permit_create_token=True (>= 3.7.0) + - M: permit create of map-based sharing of collection in case permit_create_map= False (>= 3.7.0) + - m: deny create of map-based sharing of collection in case permit_create_map=True (>= 3.7.0) Take a look at the class ``BaseRights`` if you want to implement your own. diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 71e072b7..d96d3d7d 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -102,8 +102,16 @@ class BaseSharing: # Sharing self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map") self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token") + self.permit_create_token = configuration.get("sharing", "permit_create_token") + self.permit_create_map = configuration.get("sharing", "permit_create_map") + self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token") + self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map") logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map) logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token) + logger.info("sharing.permit_create_token: %s", self.permit_create_token) + logger.info("sharing.permit_create_map : %s", self.permit_create_map) + logger.info("sharing.default_permissions_create_token: %r", self.default_permissions_create_token) + logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map) if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False)): logger.info("sharing disabled as no feature is enabled") @@ -634,7 +642,13 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/" + api_info + ": start") if 'Permissions' not in request_data: - Permissions = "r" + if ShareType == "token": + Permissions = self.default_permissions_create_token + elif ShareType == "map": + Permissions = self.default_permissions_create_map + else: + # default + Permissions = "r" if 'Enabled' in request_data: EnabledByOwner = config._convert_to_bool(request_data['Enabled']) @@ -652,10 +666,19 @@ class BaseSharing: if ShareType == "token": # check access Permissions access = Access(self._rights, user, str(PathMapped)) # PathMapped is mandatory - if not access.check("r") and "i" not in access.permissions: + if not access.check("r"): logger.info("Add sharing-by-token: access to %r not allowed for user %r", PathMapped, user) return httputils.NOT_ALLOWED + if self.permit_create_token is False: + if "t" not in access.permissions: + logger.info("Add sharing-by-token: access to %r not allowed for user %r (permit=False but explict grant misses 't')", PathMapped, user) + return httputils.NOT_ALLOWED + else: + if "T" in access.permissions: + logger.info("Add sharing-by-token: access to %r not allowed for user %r (permit=True but denied by 'T')", PathMapped, user) + return httputils.NOT_ALLOWED + # v1: create uuid token with 2x 32 bytes = 256 bit token = "v1/" + str(base64.urlsafe_b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), 'utf-8') @@ -690,9 +713,18 @@ class BaseSharing: logger.info("Add sharing-by-map: access to path(mapped) %r not allowed for owner %r", PathMapped, Owner) return httputils.NOT_ALLOWED + if self.permit_create_map is False: + if "m" not in access.permissions: + logger.info("Add sharing-by-map: access to %r not allowed for user %r (permit=False but explicit grant misses 'm')", PathMapped, user) + return httputils.NOT_ALLOWED + else: + if "M" in access.permissions: + logger.info("Add sharing-by-map: access to %r not allowed for user %r (permit=True but denied by 'M')", PathMapped, user) + return httputils.NOT_ALLOWED + access = Access(self._rights, str(User), PathOrToken) - if not access.check("r") and "i" not in access.permissions: - logger.info("Add sharing-by-map: access to path %r not allowed for user %r", PathOrToken, user) + if not access.check("r"): + logger.info("Add sharing-by-map: access to path %r not allowed for user %r", PathOrToken, User) return httputils.NOT_ALLOWED # check whether share is already existing as real collection