From 690c891a1cddf1230dd1a7e556ab5e59b46f9deb Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:40:59 +0200 Subject: [PATCH 01/10] sharing/doc: cosmetics + change default permissions --- SHARING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/SHARING.md b/SHARING.md index 964f755b..a419042b 100644 --- a/SHARING.md +++ b/SHARING.md @@ -310,11 +310,11 @@ Create a share by mapping a collection of an `Owner` to a token. | Parameter | Type | Requirement | | - | - | - | | PathMapped | str | mandatory | -| Conversion | str | optional(default:none) | -| User | str | optional(default:owner) | -| Permissions | str | optional(default:r) | -| Enabled | bool | optional(owner/default:False) | -| Hidden | bool | optional(owner/default:True) | +| Conversion | str | optional (default:none) | +| User | str | optional (default:owner) | +| Permissions | str | optional (default:rp) | +| Enabled | bool | optional (owner/default:False) | +| Hidden | bool | optional (owner/default:True) | | Properties | str | optional | * Output: text/plain|application/json From c4816cc2b1a94f0302cdb148ebc20898bc81a57d Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:41:20 +0200 Subject: [PATCH 02/10] sharing/doc: cosmetics --- SHARING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/SHARING.md b/SHARING.md index a419042b..7fc128a9 100644 --- a/SHARING.md +++ b/SHARING.md @@ -359,11 +359,11 @@ Create a share by mapping a collection of an `Owner` to an `User`. | - | - | - | | PathOrToken | str | mandatory | | PathMapped | str | mandatory | -| Conversion | str | optional(default:none) | +| Conversion | str | optional (default:none) | | User | str | mandatory | -| Permissions | str | optional(default:r) | -| Enabled | bool | optional(owner/default:False) | -| Hidden | bool | optional(owner/default:True) | +| Permissions | str | optional (default:r) | +| Enabled | bool | optional (owner/default:False) | +| Hidden | bool | optional (owner/default:True) | | Properties | optional | * Output: text/plain|application/json From bd1c408d7d78b55adc74d132e23374555b65742d Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:41:35 +0200 Subject: [PATCH 03/10] sharing/propfind+proppatch: fix permission check for properties overlay --- radicale/app/propfind.py | 2 +- radicale/app/proppatch.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 41ba6ef9..45f5e18d 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -313,7 +313,7 @@ def xml_propfind_response( ) 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'])): + (not self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])): logger.trace("PROPFIND/xml_propfind_response/current-user-privilege-set: add D:write-properties") privileges.append("D:write-properties") elif write: diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 462f3a0e..0f60dc97 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -124,7 +124,7 @@ class ApplicationPartProppatch(ApplicationBase): ) 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'])): + (not 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 share permission 'e'", path_orig) From bff39ccef5750de59a7a7385396a23cd4d75e114 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:41:59 +0200 Subject: [PATCH 04/10] sharing/token/create: change default permissions to rp --- radicale/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/config.py b/radicale/config.py index f7179bad..581d4939 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -509,7 +509,7 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "help": "enforce properties overlay on write access", "type": bool}), ("default_permissions_create_token", { - "value": "r", + "value": "rp", "help": "default permissions for token-based sharing", "type": rights_permission}), ("default_permissions_create_map", { From a9f02f1a601a6be4163da3f6e885c8dbf4caff72 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:42:19 +0200 Subject: [PATCH 05/10] sharing/token/proppatch: add additional test cases --- radicale/tests/test_sharing.py | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index eadedab6..7507af96 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -5170,7 +5170,7 @@ permissions: RrWw""") assert "NICKNAME-C3" in answer # create map - logging.info("\n*** create token with bday conversion -> ok") + logging.info("\n*** create token with bday conversion (default permissions) -> ok") json_dict = {} json_dict['User'] = "owner" json_dict['PathMapped'] = path_mapped @@ -5200,6 +5200,41 @@ permissions: RrWw""") assert "C:supported-calendar-component-set" in response assert "D:current-user-privilege-set" in response + # execute PROPPATCH color as user + logging.info("\n*** PROPPATCH color collection with token -> permission denied") + color="#BBBBBB" + _, responses = self.proppatch(path=path_shared, data="""\ + + + + + """ + color + """ + + +""", check=403) + + # update map + logging.info("\n*** update token with bday conversion ("r" permissions) -> ok") + json_dict = {} + json_dict['User'] = "owner" + json_dict['PathOrToken'] = path_shared + json_dict['Permissions'] = "r" + json_dict['Conversion'] = "bday" + _, headers, answer = self._sharing_api_json("token", "update", check=200, login="owner:ownerpw", json_dict=json_dict) + + # execute PROPPATCH color as user + logging.info("\n*** PROPPATCH color collection with token -> ok") + color="#BBBBBB" + _, responses = self.proppatch(path=path_shared, data="""\ + + + + + """ + color + """ + + +""", check=207) + # verify content as owner logging.info("\n*** GET collection owner -> ok") _, headers, answer = self.request("GET", path_shared) From b29171ff8f79210c51ea703e0a26adb0c728ff53 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:43:01 +0200 Subject: [PATCH 06/10] sharing/tests: flake8 fixes --- radicale/tests/test_sharing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 7507af96..c140ad10 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -5202,7 +5202,7 @@ permissions: RrWw""") # execute PROPPATCH color as user logging.info("\n*** PROPPATCH color collection with token -> permission denied") - color="#BBBBBB" + color = "#BBBBBB" _, responses = self.proppatch(path=path_shared, data="""\ @@ -5224,7 +5224,7 @@ permissions: RrWw""") # execute PROPPATCH color as user logging.info("\n*** PROPPATCH color collection with token -> ok") - color="#BBBBBB" + color = "#BBBBBB" _, responses = self.proppatch(path=path_shared, data="""\ From 1ff7d4d8740e940e5d4bdde55eda4aae89aac5c9 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 20:44:43 +0200 Subject: [PATCH 07/10] sharing/update: changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d72e62b5..3e441527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Fix: share address book collection as birthday calendar not working on non-DEBUG level * Extension: share accept now PATH and USER for matching email addresses as well * Adjustment: replace logging/trace_on_debug by new log level "trace" +* Adjustment: sharing/token: adjust default permissions to "rp" +* Fix: sharing/propfind+proppatch: permission check related to properties ## 3.7.0 From 8c3db5389284295fa5253b9f683a7ea596d6ed3e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 9 Apr 2026 21:30:48 +0200 Subject: [PATCH 08/10] sharing/tests: add additional propfind checks --- radicale/tests/test_sharing.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index c140ad10..e47c9b0e 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -5199,6 +5199,15 @@ permissions: RrWw""") assert "D:sync-token" not in response assert "C:supported-calendar-component-set" in response assert "D:current-user-privilege-set" in response + status, props = response["D:current-user-privilege-set"] + privileges = props.findall(xmlutils.make_clark("D:privilege")) + assert len(privileges) >= 1 + privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges] + 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 # execute PROPPATCH color as user logging.info("\n*** PROPPATCH color collection with token -> permission denied") @@ -5222,6 +5231,19 @@ permissions: RrWw""") json_dict['Conversion'] = "bday" _, headers, answer = self._sharing_api_json("token", "update", check=200, login="owner:ownerpw", json_dict=json_dict) + logging.info("\n*** PROPFIND item with token -> calendar") + response = self._propfind_allprop(path_shared) + logging.debug("response: %r", response) + status, props = response["D:current-user-privilege-set"] + privileges = props.findall(xmlutils.make_clark("D:privilege")) + assert len(privileges) >= 1 + privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges] + 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 + # execute PROPPATCH color as user logging.info("\n*** PROPPATCH color collection with token -> ok") color = "#BBBBBB" From b88b99d11e2c7ee7e55bbfb5e564e589f83a7980 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 10 Apr 2026 05:51:38 +0200 Subject: [PATCH 09/10] sharing/bday: do not permit permissions eE --- radicale/sharing/__init__.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index dc926022..41b8067b 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -880,9 +880,9 @@ class BaseSharing: else: Permissions = str(Permissions) if Conversion == "bday": - # bday is read-only + # bday is read-only and not supporting "Ee" for permission in Permissions: - if permission not in "rPpEe": + if permission not in "rPp": logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion) return httputils.METHOD_NOT_ALLOWED @@ -1081,6 +1081,15 @@ class BaseSharing: logger.trace("" + api_info + ": clear property %r", prop) del Properties[prop] + if Permissions is not None and share['Conversion'] is not None: + Permissions = str(Permissions) + if share['Conversion'] == "bday": + # bday is read-only and not supporting "Ee" + for permission in Permissions: + if permission not in "rPp": + logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion) + return httputils.METHOD_NOT_ALLOWED + if user == share['Owner']: if PathMapped is not None: # check access Permissions From 5e928ab30e15a79a60addf539143d40417856d98 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Fri, 10 Apr 2026 05:52:06 +0200 Subject: [PATCH 10/10] sharing/tests: extend/reorg test cases related to permissions --- radicale/tests/test_sharing.py | 120 ++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 17 deletions(-) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index e47c9b0e..3302680a 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -5134,6 +5134,7 @@ permissions: RrWw""") for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): logging.info("\n*** test: %s", db_type) self.configure({"sharing": {"type": db_type}}) + self.configure({"sharing": {"permit_properties_overlay": "True"}}) path_mapped = "/owner/adressbook-" + db_type + ".vcf/" self.create_addressbook(path_mapped, login="owner:ownerpw") @@ -5191,6 +5192,26 @@ permissions: RrWw""") assert "Test-FN-C3 (BDAY)" in answer assert "Test-FN (BDAY)" in answer + # verify content as owner + logging.info("\n*** GET collection owner -> ok") + _, headers, answer = self.request("GET", path_shared) + assert 'Content-Type' in headers + assert 'text/calendar' in headers['Content-Type'] + # title from default + assert 'Content-Disposition' in headers + assert 'Calendar.ics' in headers['Content-Disposition'] + + # create map of ics with conversion -> fail + logging.info("\n*** create token with bday conversion but unsupported permissions -> fail") + json_dict = {} + json_dict['User'] = "owner" + json_dict['PathMapped'] = path_mapped + json_dict['Enabled'] = True + json_dict['Permissions'] = "rw" + json_dict['Hidden'] = False + json_dict['Conversion'] = "bday" + _, headers, answer = self._sharing_api_json("token", "create", check=405, login="owner:ownerpw", json_dict=json_dict) + # check PROPFIND item with token logging.info("\n*** PROPFIND item with token -> calendar") response = self._propfind_allprop(path_shared) @@ -5228,7 +5249,6 @@ permissions: RrWw""") json_dict['User'] = "owner" json_dict['PathOrToken'] = path_shared json_dict['Permissions'] = "r" - json_dict['Conversion'] = "bday" _, headers, answer = self._sharing_api_json("token", "update", check=200, login="owner:ownerpw", json_dict=json_dict) logging.info("\n*** PROPFIND item with token -> calendar") @@ -5257,25 +5277,91 @@ permissions: RrWw""") """, check=207) - # verify content as owner - logging.info("\n*** GET collection owner -> ok") - _, headers, answer = self.request("GET", path_shared) - assert 'Content-Type' in headers - assert 'text/calendar' in headers['Content-Type'] - # title from default - assert 'Content-Disposition' in headers - assert 'Calendar.ics' in headers['Content-Disposition'] + self.configure({"sharing": {"permit_properties_overlay": "False"}}) - # create map - logging.info("\n*** create token with bday conversion but unsupported permissions -> fail") + logging.info("\n*** PROPFIND item with token (r) -> calendar") + response = self._propfind_allprop(path_shared) + logging.debug("response: %r", response) + status, props = response["D:current-user-privilege-set"] + privileges = props.findall(xmlutils.make_clark("D:privilege")) + assert len(privileges) >= 1 + privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges] + 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 + + # execute PROPPATCH color as user + logging.info("\n*** PROPPATCH color collection with token -> ok") + color = "#BBBBBB" + _, responses = self.proppatch(path=path_shared, data="""\ + + + + + """ + color + """ + + +""", check=403) + + # update map to "rP" + logging.info("\n*** update token with bday conversion ('rP' permissions) -> ok") json_dict = {} json_dict['User'] = "owner" - json_dict['PathMapped'] = path_mapped - json_dict['Enabled'] = True - json_dict['Permissions'] = "rw" - json_dict['Hidden'] = False - json_dict['Conversion'] = "bday" - _, headers, answer = self._sharing_api_json("token", "create", check=405, login="owner:ownerpw", json_dict=json_dict) + json_dict['PathOrToken'] = path_shared + json_dict['Permissions'] = "rP" + _, headers, answer = self._sharing_api_json("token", "update", check=200, login="owner:ownerpw", json_dict=json_dict) + + logging.info("\n*** PROPFIND item with token -> calendar") + response = self._propfind_allprop(path_shared) + logging.debug("response: %r", response) + status, props = response["D:current-user-privilege-set"] + privileges = props.findall(xmlutils.make_clark("D:privilege")) + assert len(privileges) >= 1 + privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges] + 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 + + # execute PROPPATCH color as user + logging.info("\n*** PROPPATCH color collection with token -> ok") + color = "#BBBBBB" + _, responses = self.proppatch(path=path_shared, data="""\ + + + + + """ + color + """ + + +""", check=207) + + # update map to "rPe" + logging.info("\n*** update token with bday conversion ('rPe' permissions) -> not supported") + json_dict = {} + json_dict['User'] = "owner" + json_dict['PathOrToken'] = path_shared + json_dict['Permissions'] = "rPe" + _, headers, answer = self._sharing_api_json("token", "update", check=405, login="owner:ownerpw", json_dict=json_dict) + + # update map to "rPE" + logging.info("\n*** update token with bday conversion ('rPE' permissions) -> not supported") + json_dict = {} + json_dict['User'] = "owner" + json_dict['PathOrToken'] = path_shared + json_dict['Permissions'] = "rPE" + _, headers, answer = self._sharing_api_json("token", "update", check=405, login="owner:ownerpw", json_dict=json_dict) + + # update map Conversion + logging.info("\n*** update token remove Conversion -> not supported") + json_dict = {} + json_dict['User'] = "owner" + json_dict['PathOrToken'] = path_shared + json_dict['Conversion'] = "" + _, headers, answer = self._sharing_api_json("token", "update", check=400, login="owner:ownerpw", json_dict=json_dict) def test_sharing_api_token_ics_bday(self) -> None: """share-by-token ics with bday conversion (has to fail)."""