From b6c1612bf1acf334a65d81056b862009a1859c8c Mon Sep 17 00:00:00 2001 From: Max Berger Date: Thu, 2 Apr 2026 23:05:56 +0200 Subject: [PATCH 01/11] share extended permissions in current-user-privilege-set --- radicale/app/propfind.py | 80 +++++++++++++++++++++++++--------- radicale/tests/test_sharing.py | 31 +++++++++++++ 2 files changed, 90 insertions(+), 21 deletions(-) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 1e5dc005..20b96637 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -36,7 +36,7 @@ from radicale.log import logger def xml_propfind(base_prefix: str, path: str, xml_request: Optional[ET.Element], - allowed_items: Iterable[Tuple[types.CollectionOrItem, str, str]], + allowed_items: Iterable[Tuple[types.CollectionOrItem, str, str, str]], user: str, encoding: str, max_resource_size: int, shares: dict = {}) -> Optional[ET.Element]: """Read and answer PROPFIND requests. @@ -73,19 +73,44 @@ def xml_propfind(base_prefix: str, path: str, if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/PROPFIND/xml_propfind: shares=%r", shares) - for item, permission, conversion in allowed_items: + for item, permission, raw_permissions, conversion in allowed_items: write = permission == "w" - multistatus.append(xml_propfind_response( - base_prefix, path, item, props, user, encoding, write=write, - allprop=allprop, propname=propname, max_resource_size=max_resource_size, shares=shares, conversion=conversion)) + multistatus.append( + xml_propfind_response( + base_prefix, + path, + item, + props, + user, + encoding, + write=write, + allprop=allprop, + propname=propname, + max_resource_size=max_resource_size, + shares=shares, + conversion=conversion, + raw_permissions=raw_permissions, + ) + ) return multistatus def xml_propfind_response( - base_prefix: str, path: str, item: types.CollectionOrItem, - props: Sequence[str], user: str, encoding: str, max_resource_size: int, write: bool = False, - propname: bool = False, allprop: bool = False, shares: dict = {}, conversion: Union[str, None] = None) -> ET.Element: + base_prefix: str, + path: str, + item: types.CollectionOrItem, + props: Sequence[str], + user: str, + encoding: str, + max_resource_size: int, + write: bool = False, + propname: bool = False, + allprop: bool = False, + shares: dict = {}, + conversion: Union[str, None] = None, + raw_permissions: str = "", +) -> ET.Element: """Build and return a PROPFIND response.""" if propname and allprop or (props and (propname or allprop)): raise ValueError("Only use one of props, propname and allprops") @@ -285,6 +310,20 @@ def xml_propfind_response( privileges.append("D:write") privileges.append("D:write-properties") privileges.append("D:write-content") + + if "P" in raw_permissions and "D:write-properties" not in privileges: + privileges.append("D:write-properties") + if "p" in raw_permissions: + privileges.append("RADICALE:no-write-properties") + if "T" in raw_permissions: + privileges.append("RADICALE:share-token") + if "t" in raw_permissions: + privileges.append("RADICALE:no-share-token") + if "M" in raw_permissions: + privileges.append("RADICALE:share-map") + if "m" in raw_permissions: + privileges.append("RADICALE:no-share-map") + for human_tag in privileges: privilege = ET.Element(xmlutils.make_clark("D:privilege")) privilege.append(ET.Element( @@ -470,26 +509,25 @@ class ApplicationPartPropfind(ApplicationBase): def _collect_allowed_items( self, items: Iterable[types.CollectionOrItem], user: str - ) -> Iterator[Tuple[types.CollectionOrItem, str]]: + ) -> Iterator[Tuple[types.CollectionOrItem, str, str]]: """Get items from request that user is allowed to access.""" for item in items: if isinstance(item, storage.BaseCollection): path = pathutils.unstrip_path(item.path, True) + raw_permissions = self._rights.authorization(user, path) if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/PROPFIND/_collect_allowed_items/BaseCollection: path=%r user=%r", path, user) if item.tag: - permissions = rights.intersect( - self._rights.authorization(user, path), "rw") + permissions = rights.intersect(raw_permissions, "rw") target = "collection with tag %r" % item.path else: - permissions = rights.intersect( - self._rights.authorization(user, path), "RW") + permissions = rights.intersect(raw_permissions, "RW") target = "collection %r" % item.path else: assert item.collection is not None path = pathutils.unstrip_path(item.collection.path, True) - permissions = rights.intersect( - self._rights.authorization(user, path), "rw") + raw_permissions = self._rights.authorization(user, path) + permissions = rights.intersect(raw_permissions, "rw") target = "item %r from %r" % (item.href, item.collection.path) if rights.intersect(permissions, "Ww"): permission = "w" @@ -504,7 +542,7 @@ class ApplicationPartPropfind(ApplicationBase): "%s has %s access to %s", repr(user) if user else "anonymous user", status, target) if permission: - yield item, permission + yield item, permission, raw_permissions def do_PROPFIND(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: @@ -548,14 +586,14 @@ class ApplicationPartPropfind(ApplicationBase): return httputils.NOT_ALLOWED # put item back items_iter = itertools.chain([item], items_iter) - for item, permission in list(self._collect_allowed_items(items_iter, user)): + for item, permission, raw_permissions in list(self._collect_allowed_items(items_iter, user)): if self._sharing._enabled and share: if share['Conversion'] == "bday" and not isinstance(item, storage.BaseCollection): if not item.convert_vcf_to_ics(): continue - allowed_items.append((item, permission, share['Conversion'])) + allowed_items.append((item, permission, raw_permissions, share['Conversion'])) else: - allowed_items.append((item, permission, None)) + allowed_items.append((item, permission, raw_permissions, None)) if self._sharing._enabled: if http_depth == "1": if logger.isEnabledFor(logging.DEBUG): @@ -580,8 +618,8 @@ class ApplicationPartPropfind(ApplicationBase): with self._storage.acquire_lock("r", c_user): c_items_iter = iter(self._storage.discover(c_path, "0")) c_allowed_items = list(self._collect_allowed_items(c_items_iter, c_user)) - for item, permission in c_allowed_items: - allowed_items.append((item, permission, share['Conversion'])) + for item, permission, raw_permissions in c_allowed_items: + allowed_items.append((item, permission, raw_permissions, share['Conversion'])) shares[c_share] = share headers = {"DAV": httputils.DAV_HEADERS, diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index e3581f77..1b30b392 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -2976,6 +2976,14 @@ permissions: RrWwM user: owner1 collection: {user}/cal-m-lc(/.*)? permissions: RrWwm +[owner1-P] +user: owner1 +collection: {user}/cal-P-uc(/.*)? +permissions: RrWwP +[owner1-p] +user: owner1 +collection: {user}/cal-p-lc(/.*)? +permissions: RrWwp [default] user: .+ collection: {user}(/.*)? @@ -3004,12 +3012,16 @@ permissions: RrWw""") path_owner1_t = "/owner1/cal-t-lc/" path_owner1_M = "/owner1/cal-M-uc/" path_owner1_m = "/owner1/cal-m-lc/" + path_owner1_P = "/owner1/cal-P-uc/" + path_owner1_p = "/owner1/cal-p-lc/" logging.info("\n*** prepare") self.mkcalendar(path_owner1_T, login="owner1:owner1pw") self.mkcalendar(path_owner1_t, login="owner1:owner1pw") self.mkcalendar(path_owner1_M, login="owner1:owner1pw") self.mkcalendar(path_owner1_m, login="owner1:owner1pw") + self.mkcalendar(path_owner1_P, login="owner1:owner1pw") + self.mkcalendar(path_owner1_p, login="owner1:owner1pw") for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): logging.info("\n*** test: %s", db_type) @@ -3112,6 +3124,25 @@ permissions: RrWw""") json_dict['PathMapped'] = path_owner1_t _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + logging.info("\n*** check PROPFIND priviledges list on collections directly") + priviledges_T = self._propfind_priviledges(path_owner1_T, login="owner1:owner1pw") + assert "RADICALE:share-token" in priviledges_T + + priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:no-share-token" in priviledges_t + + priviledges_M = self._propfind_priviledges(path_owner1_M, login="owner1:owner1pw") + assert "RADICALE:share-map" in priviledges_M + + priviledges_m = self._propfind_priviledges(path_owner1_m, login="owner1:owner1pw") + assert "RADICALE:no-share-map" in priviledges_m + + priviledges_P = self._propfind_priviledges(path_owner1_P, login="owner1:owner1pw") + assert "D:write-properties" in priviledges_P + + priviledges_p = self._propfind_priviledges(path_owner1_p, login="owner1:owner1pw") + assert "RADICALE:no-write-properties" in priviledges_p + def test_sharing_api_permissions_default(self) -> None: """sharing API usage tests related to global permissions.""" self.configure({"auth": {"type": "htpasswd", From a6ddf4c9e9c00aac8483a5a86786e3ece75d67e0 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:07:14 +0200 Subject: [PATCH 02/11] sharing/config: check for invalid permission combination --- radicale/config.py | 6 +++++- radicale/sharing/__init__.py | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/radicale/config.py b/radicale/config.py index 9df81561..594bee29 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -69,7 +69,11 @@ def positive_float(value: Any) -> float: def rights_permission(value: Any) -> str: for permission in value: if permission not in rights.INTERNAL_PERMISSIONS: - raise ValueError("unsupported permssion %r found in: %r" % (permission, value)) + raise ValueError("unsupported permission %r found in: %r" % (permission, value)) + if "p" in value and "P" in value: + raise ValueError("invalid combination of permissions (P+p) found in %r", value) + if "e" in value and "E" in value: + raise ValueError("invalid combination of permissions (E+e) found in %r", value) return value diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 791b19f5..812f0144 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -749,6 +749,10 @@ class BaseSharing: for permission in request_data[key]: if permission not in rights.INTERNAL_PERMISSIONS: return httputils.bad_request("Invalid value for Permissions") + if "p" in request_data[key] and "P" in request_data[key]: + return httputils.bad_request("Invalid combination of Permissions (P+p)") + if "e" in request_data[key] and "E" in request_data[key]: + return httputils.bad_request("Invalid combination of Permissions (E+e)") elif key == "PathOrToken": if ShareType == "token": if not re.search('^/.token/' + TOKEN_PATTERN_V1 + '/$', request_data[key]): From 4a8bede12030111ca23137bc04af003a15a519a6 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:09:34 +0200 Subject: [PATCH 03/11] sharing/propfind: adjust priviledge handling --- radicale/app/propfind.py | 45 +++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 20b96637..29bd28d4 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -34,10 +34,16 @@ from radicale.app.base import Access, ApplicationBase from radicale.log import logger -def xml_propfind(base_prefix: str, path: str, - xml_request: Optional[ET.Element], - allowed_items: Iterable[Tuple[types.CollectionOrItem, str, str, str]], - user: str, encoding: str, max_resource_size: int, shares: dict = {}) -> Optional[ET.Element]: +def xml_propfind( + self, + base_prefix: str, + path: str, + xml_request: Optional[ET.Element], + allowed_items: Iterable[Tuple[types.CollectionOrItem, str, str, str]], + user: str, encoding: str, + max_resource_size: int, + shares: dict = {}, + ) -> Optional[ET.Element]: """Read and answer PROPFIND requests. Read rfc4918-9.1 for info. @@ -77,6 +83,7 @@ def xml_propfind(base_prefix: str, path: str, write = permission == "w" multistatus.append( xml_propfind_response( + self, base_prefix, path, item, @@ -97,6 +104,7 @@ def xml_propfind(base_prefix: str, path: str, def xml_propfind_response( + self, base_prefix: str, path: str, item: types.CollectionOrItem, @@ -299,30 +307,33 @@ def xml_propfind_response( elif tag == xmlutils.make_clark("D:current-user-privilege-set"): privileges = ["D:read"] if share: + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/PROPFIND/xml_propfind_response/current-user-privilege-set: raw_permissions=%r share[Permissions]=%r permit_properties_overlay=%s", raw_permissions, share['Permissions'], self._sharing.permit_properties_overlay) if write: - if "P" in share['Permissions']: - privileges.append("D:write-properties") if "w" in share['Permissions']: if not share_bday_automap: privileges.append("D:write-content") + # priority share->rights->global + if ("P" in share['Permissions'] or + ("P" in raw_permissions and "p" not in share['Permissions']) or + (self._sharing.permit_properties_overlay and "p" not in raw_permissions and "p" not in share['Permissions']) + ) and not ( + "p" in share['Permissions'] or + ("p" in raw_permissions and "P" not in share['Permissions']) or + (self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])): + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/PROPFIND/xml_propfind_response/current-user-privilege-set: add D:write-properties") + privileges.append("D:write-properties") elif write: privileges.append("D:all") privileges.append("D:write") privileges.append("D:write-properties") privileges.append("D:write-content") - if "P" in raw_permissions and "D:write-properties" not in privileges: - privileges.append("D:write-properties") - if "p" in raw_permissions: - privileges.append("RADICALE:no-write-properties") - if "T" in raw_permissions: + if ("T" in raw_permissions or (self._sharing.permit_create_token and "t" not in raw_permissions)): privileges.append("RADICALE:share-token") - if "t" in raw_permissions: - privileges.append("RADICALE:no-share-token") - if "M" in raw_permissions: + if ("M" in raw_permissions or (self._sharing.permit_create_map and "m" not in raw_permissions)): privileges.append("RADICALE:share-map") - if "m" in raw_permissions: - privileges.append("RADICALE:no-share-map") for human_tag in privileges: privilege = ET.Element(xmlutils.make_clark("D:privilege")) @@ -624,7 +635,7 @@ class ApplicationPartPropfind(ApplicationBase): headers = {"DAV": httputils.DAV_HEADERS, "Content-Type": "text/xml; charset=%s" % self._encoding} - xml_answer = xml_propfind(base_prefix, path, xml_content, + xml_answer = xml_propfind(self, base_prefix, path, xml_content, allowed_items, user, self._encoding, max_resource_size=self._max_resource_size, shares=shares) if xml_answer is None: return httputils.NOT_ALLOWED From 3d7f821300a41d5a4768927e6252a34803f63315 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:10:10 +0200 Subject: [PATCH 04/11] sharing/propfind: log cosmetics --- radicale/app/propfind.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 29bd28d4..4e1760e1 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -527,7 +527,7 @@ class ApplicationPartPropfind(ApplicationBase): path = pathutils.unstrip_path(item.path, True) raw_permissions = self._rights.authorization(user, path) if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/PROPFIND/_collect_allowed_items/BaseCollection: path=%r user=%r", path, user) + logger.debug("TRACE/PROPFIND/_collect_allowed_items/BaseCollection: path=%r user=%r raw_permissions=%r", path, user, raw_permissions) if item.tag: permissions = rights.intersect(raw_permissions, "rw") target = "collection with tag %r" % item.path @@ -618,14 +618,14 @@ class ApplicationPartPropfind(ApplicationBase): c_user = share['Owner'] c_permissions_filter = share['Permissions'] if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/PROPFIND: test shared collection: PathOrToken=%r PathMapped=%r Owner=%r Permissions=%s", c_share, c_path, c_user, c_permissions_filter) + logger.debug("TRACE/PROPFIND: test shared collection: PathOrToken=%r PathMapped=%r Owner=%r Permissions=%r", c_share, c_path, c_user, c_permissions_filter) c_access = Access(self._rights, c_user, c_path, c_permissions_filter) if not c_access.check("r"): if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/PROPFIND: skip shared collection: PathOrToken=%r PathMapped=%r Owner=%r Permissions=%s (permissions not matching)", c_share, c_path, c_user, c_permissions_filter) + logger.debug("TRACE/PROPFIND: skip shared collection: PathOrToken=%r PathMapped=%r Owner=%r Permissions=%r (permissions not matching)", c_share, c_path, c_user, c_permissions_filter) continue if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/PROPFIND: append shared collection: PathOrToken=%r PathMapped=%r Owner=%r", c_share, c_path, c_user) + logger.debug("TRACE/PROPFIND: append shared collection: PathOrToken=%r PathMapped=%r Owner=%r Permissions=%r", c_share, c_path, c_user, c_permissions_filter) with self._storage.acquire_lock("r", c_user): c_items_iter = iter(self._storage.discover(c_path, "0")) c_allowed_items = list(self._collect_allowed_items(c_items_iter, c_user)) From a1d2ed7cf9b21eb282c971b70783a1315a307c23 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:10:34 +0200 Subject: [PATCH 05/11] sharing/proppatch: fix/adjust permission handling --- radicale/app/proppatch.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index dc4721e2..40646839 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -20,6 +20,7 @@ # along with Radicale. If not, see . import errno +import logging import re import socket import xml.etree.ElementTree as ET @@ -113,23 +114,27 @@ class ApplicationPartProppatch(ApplicationBase): permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) if not access.check("w"): - logger.debug("TRACE/PROPPATCH/xml_proppatch: no write-access: %r", path) + logger.debug("TRACE/PROPPATCH/xml_proppatch: no native write-access: %r", path) if share: - # no write access -> use properties overlay - if self._sharing.permit_properties_overlay: - if permissions_filter is not None and "p" in permissions_filter: - logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted, but denied by permission 'p'", path_orig) - return httputils.NOT_ALLOWED + # priority share->rights->global + raw_permissions = self._rights.authorization(user, path) + if logger.isEnabledFor(logging.DEBUG): + logger.debug("TRACE/PROPPATCH/share: raw_permissions=%r share[Permissions]=%r permit_properties_overlay=%s", raw_permissions, share['Permissions'], self._sharing.permit_properties_overlay) + if ("P" in share['Permissions'] or + ("P" in raw_permissions and "p" not in share['Permissions']) or + (self._sharing.permit_properties_overlay and "p" not in raw_permissions and "p" not in share['Permissions']) + ) and not ( + "p" in share['Permissions'] or + ("p" in raw_permissions and "P" not in share['Permissions']) or + (self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])): + logger.info("PROPPATCH request on shared %r: write-access", path_orig) + if permissions_filter is not None and "e" in permissions_filter: + logger.info("PROPPATCH request on shared %r: write-access, overlay enforced, but disabled by permission 'e'", path_orig) else: - logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted by option", path_orig) share_overlay = True else: - if permissions_filter is not None and "P" in permissions_filter: - logger.info("PROPPATCH request on shared %r: no write-permissions, overlay denied, but granted by permission 'P'", path_orig) - share_overlay = True - else: - logger.info("PROPPATCH request on shared %r: no write-permissions and overlay denied by option", path_orig) - return httputils.NOT_ALLOWED + logger.info("PROPPATCH request on shared %r: no write-access", path_orig) + return httputils.NOT_ALLOWED else: return httputils.NOT_ALLOWED else: From 695fb1976181370bf990d4983db036d98a0cfe9f Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:11:00 +0200 Subject: [PATCH 06/11] sharing: fix property overlay permission handling --- radicale/sharing/__init__.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 812f0144..d4385e3d 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -406,16 +406,6 @@ class BaseSharing: if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/map: not active") - if share is not None: - if self.permit_properties_overlay: - if share['Permissions'] and "p" not in share['Permissions']: - # add permit permission - share['Permissions'] += "P" - else: - if share['Permissions'] and "P" not in share['Permissions']: - # add deny permission - share['Permissions'] += "p" - return share # adjust a share From 1883b48c1b5f68318d195db1d25c4af1291ec143 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:12:43 +0200 Subject: [PATCH 07/11] sharing/conversion/bday: permit PpEe permissions --- radicale/sharing/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index d4385e3d..bd0b5eac 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -910,8 +910,8 @@ class BaseSharing: if Conversion == "bday": # bday is read-only for permission in Permissions: - if permission not in "r": - logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion) + if permission not in "rPpEe": + logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion) return httputils.METHOD_NOT_ALLOWED if Enabled is None: From fc8f18866274123a066433c7add91491bb767728 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:13:19 +0200 Subject: [PATCH 08/11] sharing/tests: add test cases, cosmetics --- radicale/tests/test_sharing.py | 160 +++++++++++++++++++++++++++++++-- 1 file changed, 154 insertions(+), 6 deletions(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 1b30b392..df757946 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -3000,7 +3000,7 @@ permissions: RrWw""") "collection_by_map": "True", "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", - "response_content_on_debug": "False", + "response_content_on_debug": "True", "rights_rule_doesnt_match_on_debug": "True", "request_content_on_debug": "True"}, "rights": {"type": "from_file"}}) @@ -3055,7 +3055,7 @@ permissions: RrWw""") json_dict['PathOrToken'] = path_user1.replace(".ics", "dm-lc" + db_type + ".ics") _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) - logging.info("\n*** deletee map user1/owner1, globally disabled / granted m -> 200") + logging.info("\n*** delete map user1/owner1, globally disabled / granted m -> 200") json_dict['PathMapped'] = path_owner1_m json_dict['PathOrToken'] = path_user1.replace(".ics", "dm-lc" + db_type + ".ics") _, headers, answer = self._sharing_api_json("map", "delete", check=200, login="owner1:owner1pw", json_dict=json_dict) @@ -3124,24 +3124,170 @@ permissions: RrWw""") json_dict['PathMapped'] = path_owner1_t _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) - logging.info("\n*** check PROPFIND priviledges list on collections directly") + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token T (permit=True)") priviledges_T = self._propfind_priviledges(path_owner1_T, login="owner1:owner1pw") assert "RADICALE:share-token" in priviledges_T + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token t (permit=True)") priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") - assert "RADICALE:no-share-token" in priviledges_t + assert "RADICALE:share-token" not in priviledges_t + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token * (permit=True)") + priviledges_p = self._propfind_priviledges(path_owner1_p, login="owner1:owner1pw") + assert "RADICALE:share-token" in priviledges_p + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map M (permit=True)") priviledges_M = self._propfind_priviledges(path_owner1_M, login="owner1:owner1pw") assert "RADICALE:share-map" in priviledges_M + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map m (permit=True)") priviledges_m = self._propfind_priviledges(path_owner1_m, login="owner1:owner1pw") - assert "RADICALE:no-share-map" in priviledges_m + assert "RADICALE:share-map" not in priviledges_m + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map * (permit=True)") + priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-map" in priviledges_t + + self.configure({"sharing": {"permit_create_token": "False", "permit_create_map": "False"}}) + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token T (permit=False)") + priviledges_T = self._propfind_priviledges(path_owner1_T, login="owner1:owner1pw") + assert "RADICALE:share-token" in priviledges_T + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token t (permit=False)") + priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-token" not in priviledges_t + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token * (permit=False)") + priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-token" not in priviledges_t + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map M (permit=False)") + priviledges_M = self._propfind_priviledges(path_owner1_M, login="owner1:owner1pw") + assert "RADICALE:share-map" in priviledges_M + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map m (permit=False)") + priviledges_m = self._propfind_priviledges(path_owner1_m, login="owner1:owner1pw") + assert "RADICALE:share-map" not in priviledges_m + + logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map * (permit=False)") + priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-map" not in priviledges_t + + # continue + self.configure({"sharing": {"permit_create_token": "True", "permit_create_map": "True"}}) + + logging.info("\n*** check PROPFIND priviledges list on collections directly by owner") priviledges_P = self._propfind_priviledges(path_owner1_P, login="owner1:owner1pw") assert "D:write-properties" in priviledges_P priviledges_p = self._propfind_priviledges(path_owner1_p, login="owner1:owner1pw") - assert "RADICALE:no-write-properties" in priviledges_p + assert "D:write-properties" in priviledges_p + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user") + + logging.info("\n*** create map user1/owner1 P -> 200") + path_share = path_user1.replace(".ics", "P-uc" + db_type + ".ics") + json_dict = {} + json_dict['PathMapped'] = path_owner1_P + json_dict['Hidden'] = False + json_dict['Enabled'] = True + json_dict['User'] = "user1" + json_dict['Permissions'] = "r" + json_dict['PathOrToken'] = path_share + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user1:user1pw", json_dict=json_dict) + _, headers, answer = self._sharing_api_json("map", "unhide", check=200, login="user1:user1pw", json_dict=json_dict) + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=P r (permit_properties_overlay=False)") + self.configure({"sharing": {"permit_properties_overlay": "False"}}) + priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" in priviledges_P + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: r (permit_properties_overlay=True)") + self.configure({"sharing": {"permit_properties_overlay": "True"}}) + priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" in priviledges_P + + logging.info("\n*** update map with illegal combination pP") + json_dict = {} + json_dict['PathMapped'] = path_owner1_P + json_dict['PathOrToken'] = path_share + json_dict['User'] = "user1" + json_dict['Permissions'] = "rpP" + _, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** update map with illegal combination eE") + json_dict = {} + json_dict['PathMapped'] = path_owner1_P + json_dict['PathOrToken'] = path_share + json_dict['User'] = "user1" + json_dict['Permissions'] = "reE" + _, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** update map with only p") + json_dict = {} + json_dict['PathMapped'] = path_owner1_p + json_dict['PathOrToken'] = path_share + json_dict['User'] = "user1" + json_dict['Permissions'] = "rp" + _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=P rp (permit_properties_overlay=False)") + self.configure({"sharing": {"permit_properties_overlay": "False"}}) + priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" not in priviledges_P + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=P rp (permit_properties_overlay=True)") + self.configure({"sharing": {"permit_properties_overlay": "True"}}) + priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" not in priviledges_P + + json_dict = {} + json_dict['PathMapped'] = path_owner1_P + json_dict['PathOrToken'] = path_share + json_dict['User'] = "user1" + _, headers, answer = self._sharing_api_json("map", "delete", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** create map user1/owner1 p -> 200") + path_share = path_user1.replace(".ics", "p-lc" + db_type + ".ics") + json_dict = {} + json_dict['PathMapped'] = path_owner1_p + json_dict['Hidden'] = False + json_dict['Enabled'] = True + json_dict['User'] = "user1" + json_dict['Permissions'] = "r" + json_dict['PathOrToken'] = path_share + _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) + _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user1:user1pw", json_dict=json_dict) + _, headers, answer = self._sharing_api_json("map", "unhide", check=200, login="user1:user1pw", json_dict=json_dict) + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p r (permit_properties_overlay=False)") + self.configure({"sharing": {"permit_properties_overlay": "False"}}) + priviledges_p = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" not in priviledges_p + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p r (permit_properties_overlay=True)") + self.configure({"sharing": {"permit_properties_overlay": "True"}}) + priviledges_p = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" not in priviledges_p + + json_dict = {} + json_dict['PathMapped'] = path_owner1_p + json_dict['PathOrToken'] = path_share + json_dict['User'] = "user1" + json_dict['Permissions'] = "rP" + _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner1:owner1pw", json_dict=json_dict) + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p rP (permit_properties_overlay=False)") + self.configure({"sharing": {"permit_properties_overlay": "False"}}) + priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" in priviledges_P + + logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p rP (permit_properties_overlay=True)") + self.configure({"sharing": {"permit_properties_overlay": "True"}}) + priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") + assert "D:write-properties" in priviledges_P def test_sharing_api_permissions_default(self) -> None: """sharing API usage tests related to global permissions.""" @@ -4509,6 +4655,8 @@ permissions: RrWw""") json_dict['PathMapped'] = path_mapped json_dict['PathOrToken'] = path_shared_r json_dict['Conversion'] = "bday" + json_dict['Permissions'] = "rP" + json_dict['Enabled'] = True json_dict['Enabled'] = True json_dict['Hidden'] = False json_dict['Properties'] = {"D:displayname": "Test-BDAY"} From 370d86ba2fd5e0e7959a86aee0a0bf00d4293d7c Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:34:57 +0200 Subject: [PATCH 09/11] sharing/proppatch: bugfix --- radicale/app/proppatch.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 40646839..b2e654cb 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -113,13 +113,13 @@ class ApplicationPartProppatch(ApplicationBase): user = share['Owner'] permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) + raw_permissions = self._rights.authorization(user, path) if not access.check("w"): logger.debug("TRACE/PROPPATCH/xml_proppatch: no native write-access: %r", path) if share: # priority share->rights->global - raw_permissions = self._rights.authorization(user, path) if logger.isEnabledFor(logging.DEBUG): - logger.debug("TRACE/PROPPATCH/share: raw_permissions=%r share[Permissions]=%r permit_properties_overlay=%s", raw_permissions, share['Permissions'], self._sharing.permit_properties_overlay) + logger.debug("TRACE/PROPPATCH/share: raw_permissions=%r share[Permissions]=%r permit_properties_overlay=%s enforce_properties_overlay=%s", raw_permissions, share['Permissions'], self._sharing.permit_properties_overlay, self._sharing.enforce_properties_overlay) if ("P" in share['Permissions'] or ("P" in raw_permissions and "p" not in share['Permissions']) or (self._sharing.permit_properties_overlay and "p" not in raw_permissions and "p" not in share['Permissions']) @@ -129,7 +129,9 @@ class ApplicationPartProppatch(ApplicationBase): (self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])): logger.info("PROPPATCH request on shared %r: write-access", path_orig) if permissions_filter is not None and "e" in permissions_filter: - logger.info("PROPPATCH request on shared %r: write-access, overlay enforced, but disabled by permission 'e'", path_orig) + logger.info("PROPPATCH request on shared %r: write-access, overlay enforced, but disabled by share permission 'e'", path_orig) + elif "e" in raw_permissions: + logger.info("PROPPATCH request on shared %r: write-access, overlay enforced, but disabled by rights permission 'e'", path_orig) else: share_overlay = True else: @@ -144,7 +146,9 @@ class ApplicationPartProppatch(ApplicationBase): logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access/sharing: %r", path_orig) if self._sharing.enforce_properties_overlay: if permissions_filter is not None and "e" in permissions_filter: - logger.info("PROPPATCH request on shared %r: write-permissions, overlay enforced, but disabled by permission 'e'", path_orig) + logger.info("PROPPATCH request on shared %r: write-permissions, overlay enforced, but disabled by share permission 'e'", path_orig) + elif "e" in raw_permissions: + logger.info("PROPPATCH request on shared %r: write-permissions, overlay enforced, but disabled by rights permission 'e'", path_orig) else: share_overlay = True else: From 7a4451484f20971f9f11e210c990e3b25ba2ffc4 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:35:11 +0200 Subject: [PATCH 10/11] sharing/doc: add note about attributes --- SHARING.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/SHARING.md b/SHARING.md index a6b870c4..3c77061b 100644 --- a/SHARING.md +++ b/SHARING.md @@ -589,6 +589,24 @@ Whitelisted ones are defined in `OVERLAY_PROPERTIES_WHITELIST` in `radicale/shar * `enforce_properties_overlay` * supported *share* permissions: `Ee` +#### Properties Overlay Control Precedence + +##### General Permission for Overlay + + 1. permission of particular share configuration: `p` or `P` + + 2. permission based on `rights` per location: `p` or `P` + + 3. config option: `permit_properties_overlay` + +##### Enforce Overlay on Write + + 1. permission of particular share configuration: `e` or `E` + + 2. permission based on `rights` per location: `e` or `E` + + 3. config option: `enforce_properties_overlay` + ### Properties Overlay Example #### Requirements From a4aa42ec2053f0b4bbdde3fb7314b535f507df3f Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 3 Apr 2026 16:43:47 +0200 Subject: [PATCH 11/11] fix typo --- radicale/tests/test_sharing.py | 206 ++++++++++++++++----------------- 1 file changed, 103 insertions(+), 103 deletions(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index df757946..eadedab6 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -89,14 +89,14 @@ class TestSharingApiSanity(BaseTest): assert not isinstance(response, int) return response - def _propfind_priviledges(self, path: str, login) -> list[str]: + def _propfind_privileges(self, path: str, login) -> list[str]: response = self._propfind_allprop(path, login) status, prop = response["D:current-user-privilege-set"] logging.debug("prop: %r", prop) - priviledges = prop.findall(xmlutils.make_clark("D:privilege")) - assert len(priviledges) >= 1 - priviledges_list = [xmlutils.make_human_tag(priviledge.findall("*")[0].tag) for priviledge in priviledges] - return priviledges_list + privileges = prop.findall(xmlutils.make_clark("D:privilege")) + assert len(privileges) >= 1 + privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges] + return privileges_list def _propfind_calendar_color(self, path, login) -> Union[str, None]: propfind_calendar_color = get_file_content("propfind_calendar_color.xml") @@ -3124,67 +3124,67 @@ permissions: RrWw""") json_dict['PathMapped'] = path_owner1_t _, headers, answer = self._sharing_api_json("token", "create", check=200, login="owner1:owner1pw", json_dict=json_dict) - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token T (permit=True)") - priviledges_T = self._propfind_priviledges(path_owner1_T, login="owner1:owner1pw") - assert "RADICALE:share-token" in priviledges_T + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-token T (permit=True)") + privileges_T = self._propfind_privileges(path_owner1_T, login="owner1:owner1pw") + assert "RADICALE:share-token" in privileges_T - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token t (permit=True)") - priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") - assert "RADICALE:share-token" not in priviledges_t + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-token t (permit=True)") + privileges_t = self._propfind_privileges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-token" not in privileges_t - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token * (permit=True)") - priviledges_p = self._propfind_priviledges(path_owner1_p, login="owner1:owner1pw") - assert "RADICALE:share-token" in priviledges_p + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-token * (permit=True)") + privileges_p = self._propfind_privileges(path_owner1_p, login="owner1:owner1pw") + assert "RADICALE:share-token" in privileges_p - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map M (permit=True)") - priviledges_M = self._propfind_priviledges(path_owner1_M, login="owner1:owner1pw") - assert "RADICALE:share-map" in priviledges_M + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-map M (permit=True)") + privileges_M = self._propfind_privileges(path_owner1_M, login="owner1:owner1pw") + assert "RADICALE:share-map" in privileges_M - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map m (permit=True)") - priviledges_m = self._propfind_priviledges(path_owner1_m, login="owner1:owner1pw") - assert "RADICALE:share-map" not in priviledges_m + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-map m (permit=True)") + privileges_m = self._propfind_privileges(path_owner1_m, login="owner1:owner1pw") + assert "RADICALE:share-map" not in privileges_m - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map * (permit=True)") - priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") - assert "RADICALE:share-map" in priviledges_t + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-map * (permit=True)") + privileges_t = self._propfind_privileges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-map" in privileges_t self.configure({"sharing": {"permit_create_token": "False", "permit_create_map": "False"}}) - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token T (permit=False)") - priviledges_T = self._propfind_priviledges(path_owner1_T, login="owner1:owner1pw") - assert "RADICALE:share-token" in priviledges_T + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-token T (permit=False)") + privileges_T = self._propfind_privileges(path_owner1_T, login="owner1:owner1pw") + assert "RADICALE:share-token" in privileges_T - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token t (permit=False)") - priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") - assert "RADICALE:share-token" not in priviledges_t + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-token t (permit=False)") + privileges_t = self._propfind_privileges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-token" not in privileges_t - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-token * (permit=False)") - priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") - assert "RADICALE:share-token" not in priviledges_t + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-token * (permit=False)") + privileges_t = self._propfind_privileges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-token" not in privileges_t - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map M (permit=False)") - priviledges_M = self._propfind_priviledges(path_owner1_M, login="owner1:owner1pw") - assert "RADICALE:share-map" in priviledges_M + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-map M (permit=False)") + privileges_M = self._propfind_privileges(path_owner1_M, login="owner1:owner1pw") + assert "RADICALE:share-map" in privileges_M - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map m (permit=False)") - priviledges_m = self._propfind_priviledges(path_owner1_m, login="owner1:owner1pw") - assert "RADICALE:share-map" not in priviledges_m + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-map m (permit=False)") + privileges_m = self._propfind_privileges(path_owner1_m, login="owner1:owner1pw") + assert "RADICALE:share-map" not in privileges_m - logging.info("\n*** check PROPFIND priviledges list on collections directly: RADICALE:share-map * (permit=False)") - priviledges_t = self._propfind_priviledges(path_owner1_t, login="owner1:owner1pw") - assert "RADICALE:share-map" not in priviledges_t + logging.info("\n*** check PROPFIND privileges list on collections directly: RADICALE:share-map * (permit=False)") + privileges_t = self._propfind_privileges(path_owner1_t, login="owner1:owner1pw") + assert "RADICALE:share-map" not in privileges_t # continue self.configure({"sharing": {"permit_create_token": "True", "permit_create_map": "True"}}) - logging.info("\n*** check PROPFIND priviledges list on collections directly by owner") - priviledges_P = self._propfind_priviledges(path_owner1_P, login="owner1:owner1pw") - assert "D:write-properties" in priviledges_P + logging.info("\n*** check PROPFIND privileges list on collections directly by owner") + privileges_P = self._propfind_privileges(path_owner1_P, login="owner1:owner1pw") + assert "D:write-properties" in privileges_P - priviledges_p = self._propfind_priviledges(path_owner1_p, login="owner1:owner1pw") - assert "D:write-properties" in priviledges_p + privileges_p = self._propfind_privileges(path_owner1_p, login="owner1:owner1pw") + assert "D:write-properties" in privileges_p - logging.info("\n*** check PROPFIND priviledges list on collections directly by user") + logging.info("\n*** check PROPFIND privileges list on collections directly by user") logging.info("\n*** create map user1/owner1 P -> 200") path_share = path_user1.replace(".ics", "P-uc" + db_type + ".ics") @@ -3199,15 +3199,15 @@ permissions: RrWw""") _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user1:user1pw", json_dict=json_dict) _, headers, answer = self._sharing_api_json("map", "unhide", check=200, login="user1:user1pw", json_dict=json_dict) - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=P r (permit_properties_overlay=False)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=P r (permit_properties_overlay=False)") self.configure({"sharing": {"permit_properties_overlay": "False"}}) - priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" in priviledges_P + privileges_P = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" in privileges_P - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: r (permit_properties_overlay=True)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: r (permit_properties_overlay=True)") self.configure({"sharing": {"permit_properties_overlay": "True"}}) - priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" in priviledges_P + privileges_P = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" in privileges_P logging.info("\n*** update map with illegal combination pP") json_dict = {} @@ -3233,15 +3233,15 @@ permissions: RrWw""") json_dict['Permissions'] = "rp" _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner1:owner1pw", json_dict=json_dict) - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=P rp (permit_properties_overlay=False)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=P rp (permit_properties_overlay=False)") self.configure({"sharing": {"permit_properties_overlay": "False"}}) - priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" not in priviledges_P + privileges_P = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" not in privileges_P - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=P rp (permit_properties_overlay=True)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=P rp (permit_properties_overlay=True)") self.configure({"sharing": {"permit_properties_overlay": "True"}}) - priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" not in priviledges_P + privileges_P = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" not in privileges_P json_dict = {} json_dict['PathMapped'] = path_owner1_P @@ -3262,15 +3262,15 @@ permissions: RrWw""") _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user1:user1pw", json_dict=json_dict) _, headers, answer = self._sharing_api_json("map", "unhide", check=200, login="user1:user1pw", json_dict=json_dict) - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p r (permit_properties_overlay=False)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=p r (permit_properties_overlay=False)") self.configure({"sharing": {"permit_properties_overlay": "False"}}) - priviledges_p = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" not in priviledges_p + privileges_p = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" not in privileges_p - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p r (permit_properties_overlay=True)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=p r (permit_properties_overlay=True)") self.configure({"sharing": {"permit_properties_overlay": "True"}}) - priviledges_p = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" not in priviledges_p + privileges_p = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" not in privileges_p json_dict = {} json_dict['PathMapped'] = path_owner1_p @@ -3279,15 +3279,15 @@ permissions: RrWw""") json_dict['Permissions'] = "rP" _, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner1:owner1pw", json_dict=json_dict) - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p rP (permit_properties_overlay=False)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=p rP (permit_properties_overlay=False)") self.configure({"sharing": {"permit_properties_overlay": "False"}}) - priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" in priviledges_P + privileges_P = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" in privileges_P - logging.info("\n*** check PROPFIND priviledges list on collections directly by user: rights=p rP (permit_properties_overlay=True)") + logging.info("\n*** check PROPFIND privileges list on collections directly by user: rights=p rP (permit_properties_overlay=True)") self.configure({"sharing": {"permit_properties_overlay": "True"}}) - priviledges_P = self._propfind_priviledges(path_share, login="user1:user1pw") - assert "D:write-properties" in priviledges_P + privileges_P = self._propfind_privileges(path_share, login="user1:user1pw") + assert "D:write-properties" in privileges_P def test_sharing_api_permissions_default(self) -> None: """sharing API usage tests related to global permissions.""" @@ -3348,14 +3348,14 @@ permissions: RrWw""") assert answer_dict['Lines'] == 1 assert answer_dict['Content'][0]['Permissions'] == "r" - # check PROPFIND/priviledges item as user - logging.info("\n*** PROPFIND/priviledges item as user") - priviledges_list = self._propfind_priviledges(path_user1_r, login="user1:user1pw") - assert "D:read" in priviledges_list - assert "D:write-content" not in priviledges_list - assert "D:write-properties" not in priviledges_list - assert "D:write" not in priviledges_list - assert "D:all" not in priviledges_list + # check PROPFIND/privileges item as user + logging.info("\n*** PROPFIND/privileges item as user") + privileges_list = self._propfind_privileges(path_user1_r, login="user1:user1pw") + assert "D:read" in privileges_list + assert "D:write-content" not in privileges_list + assert "D:write-properties" not in privileges_list + assert "D:write" not in privileges_list + assert "D:all" not in privileges_list logging.info("\n*** create map user1/owner1 rw -> 200") json_dict = {} @@ -3380,14 +3380,14 @@ permissions: RrWw""") json_dict['PathOrToken'] = path_user1_rw _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user1:user1pw", json_dict=json_dict) - # check PROPFIND/priviledges item as user - logging.info("\n*** PROPFIND/priviledges item as user") - priviledges_list = self._propfind_priviledges(path_user1_rw, login="user1:user1pw") - assert "D:read" in priviledges_list - assert "D:write-content" in priviledges_list - assert "D:write-properties" not in priviledges_list - assert "D:write" not in priviledges_list - assert "D:all" not in priviledges_list + # check PROPFIND/privileges item as user + logging.info("\n*** PROPFIND/privileges item as user") + privileges_list = self._propfind_privileges(path_user1_rw, login="user1:user1pw") + assert "D:read" in privileges_list + assert "D:write-content" in privileges_list + assert "D:write-properties" not in privileges_list + assert "D:write" not in privileges_list + assert "D:all" not in privileges_list logging.info("\n*** create map user1/owner1 with adjusted default permissions -> 200") self.configure({"sharing": {"default_permissions_create_map": "RrWw"}}) @@ -4163,14 +4163,14 @@ permissions: RrWw""") json_dict['PathOrToken'] = path_shared_r _, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict) - # check PROPFIND/priviledges item as user - logging.info("\n*** PROPFIND/priviledges item as user -> calendar") - priviledges_list = self._propfind_priviledges(path_shared_r, login="user:userpw") - assert "D:read" in priviledges_list - assert "D:write-content" not in priviledges_list - assert "D:write-properties" in priviledges_list - assert "D:write" not in priviledges_list - assert "D:all" not in priviledges_list + # check PROPFIND/privileges item as user + logging.info("\n*** PROPFIND/privileges item as user -> calendar") + privileges_list = self._propfind_privileges(path_shared_r, login="user:userpw") + assert "D:read" in privileges_list + assert "D:write-content" not in privileges_list + assert "D:write-properties" in privileges_list + assert "D:write" not in privileges_list + assert "D:all" not in privileges_list # verify PROPPATCH as user logging.info("\n*** PROPFIND collection user -> color #AAAAAA") @@ -4692,14 +4692,14 @@ permissions: RrWw""") assert "C:supported-calendar-component-set" in response assert "D:current-user-privilege-set" in response - # check PROPFIND/priviledges item as user - logging.info("\n*** PROPFIND/priviledges item as user -> calendar") - priviledges_list = self._propfind_priviledges(path_shared_r, login="user:userpw") - assert "D:read" in priviledges_list - assert "D:write-content" not in priviledges_list - assert "D:write-properties" in priviledges_list - assert "D:write" not in priviledges_list - assert "D:all" not in priviledges_list + # check PROPFIND/privileges item as user + logging.info("\n*** PROPFIND/privileges item as user -> calendar") + privileges_list = self._propfind_privileges(path_shared_r, login="user:userpw") + assert "D:read" in privileges_list + assert "D:write-content" not in privileges_list + assert "D:write-properties" in privileges_list + assert "D:write" not in privileges_list + assert "D:all" not in privileges_list # verify content as user logging.info("\n*** GET collection user -> ok")