From ea74f8e17612c98196b4b26c8a5fe414b7fe70fa Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 07:30:46 +0100 Subject: [PATCH 1/7] sharing: review doc --- SHARING.md | 215 ++++++++++++++++++++++++++++------------------------- 1 file changed, 112 insertions(+), 103 deletions(-) diff --git a/SHARING.md b/SHARING.md index c4cdfc96..0638eed7 100644 --- a/SHARING.md +++ b/SHARING.md @@ -1,121 +1,29 @@ # Collection Sharing -Static collection sharing without permissions filter using soft-links (Unix-only) is supported since storage type `multifilesystem` was implemented, see (Wiki: Sharing Collections)[https://github.com/Kozea/Radicale/wiki/Sharing-Collections] +Static collection sharing without permissions filter using soft-links (Unix-only) is supported since storage type `multifilesystem` was implemented, see [Wiki: Sharing Collections](https://github.com/Kozea/Radicale/wiki/Sharing-Collections) -With _3.7.0_ major extension was implemented using internal mapping configuration stored in a database and a management API. +With _3.7.0_ major extension was implemented + * added internal mapping with configuration stored in a database + * added management API + * WebUI extension using the management API ## Sharing Implementation -Implemenation of sharing collections is done in case entry exists in sharing database by replacing provided data on request and adjust if required data in responses. +Implemenation of sharing collections is done by using a database to lookup the URI and in case entry exists by mapping to target URI and replacing provided data on request and adjust if required data in response. Permissions are filtered by provided `Permissions`. -### CxDAV requests +## Sharing Configuration -#### CxDav request "(DELETE|GET|HEAD|PUT)" - - * Actions - * map - * Lookup by - * `path` (provided in request) - * `user` (authenticated) - * Replace - * `user` by `Owner` - * `path` by `PathMapped` - * Activate - * `permissions_filter` by `Permissions` - -#### CxDav request "REPORT" - - * Actions - * map - * back-map response - * Lookup by - * `path` (provided in request) - * `user` (authenticated) - * Replace - * `user` by `Owner` - * `path` by `PathMapped` - * Activate - * `permissions_filter` by `Permissions` - -#### CxDav request "PROPFIND" without HTTP_DEPTH=1 - - * Actions - * map - * back-map response - * overwrite `Properties` if provided - * Lookup by - * `path` (provided in request) - * `user` (authenticated) - * Replace - * `user` by `Owner` - * `path` by `PathMapped` - * Overlay - * `Properties` if provided - * Activate - * `permissions_filter` by `Permissions` - -#### CxDav request "PROPFIND" with HTTP_DEPTH=1 - - * Actions - * extend list - * Lookup for active shares for `user` in sharing database - * Extend list if conditions are met - * `permissions_filter` by `Permissions` - -#### CxDav request "PROPPATCH" - - * Actions - * map - * adjust properties of a collection - * Lookup by - * `path` (provided in request) - * `user` (authenticated) - * Replace - * `user` by `Owner` - * `path` by `PathMapped` - * Activate - * `permissions_filter` by `Permissions` - * Depending on `permissions_filter`, global options and `Permissions` - * adjust properties of collection - * adjust whitelisted properties in `Properties` for overlay (see OVERLAY_PROPERTIES_WHITELIST) - -#### CxDav request "(MKCALENDAR|MKCOL)" - - * Action - * check for conflicts - * Lookup by - * `user` (authenticated) - * Verify for non-existence as `PathOrToken` in sharing database - * `path` (provided in request) - -#### CxDav request "(MOVE)" - - * Action - * map source - * map destination - * Lookup by - * `path` (provided in request) - * `user` (authenticated) - * `to_path` (provided in request) - * `to_user` (same as `user`) - * Replace - * `user` by `Owner` (of `path`) - * `path` by `PathMapped` (of path) - * `to_user` by `Owner` (of `to_path`) - * `to_path` by `PathMapped` (of `to_path`) - * Activate - * `permissions_filter` by `Permissions` (of `to_path`) - * `to_permissions_filter` by `Permissions` (of `to_path`) +New section `[sharing]` controls sharing configuration, see [DOCUMENTATION:Sharing](DOCUMENTATION.md#sharing) for details ## Sharing Configuration Store Types of supported sharing configuration: - * csv (_>= 3.7.0_) - * files (_>= 3.7.0_) + * *csv* (_>= 3.7.0_) + * *files* (_>= 3.7.0_) ### Sharing Configuration Entry Data @@ -152,6 +60,107 @@ If given, properties are stored in JSON format in CSV. File-based configuration store is using encoded `PathOrToken` as filename for each config. File contains the data stored as "dict" in binary Python "pickle" format (same is also used for item cache files). +## Sharing Request Handling + +### CxDAV requests + +#### CxDav request "(DELETE|GET|HEAD|PUT)" + + * Actions + * map + * Lookup by + * `path` (provided in request) + * `user` (authenticated) + * Replace + * `path` by `PathMapped` + * `user` by `Owner` + * Activate + * `permissions_filter` by `Permissions` + +#### CxDav request "REPORT" + + * Actions + * map + * back-map response + * Lookup by + * `path` (provided in request) + * `user` (authenticated) + * Replace + * `path` by `PathMapped` + * `user` by `Owner` + * Activate + * `permissions_filter` by `Permissions` + +#### CxDav request "PROPFIND" without HTTP_DEPTH=1 + + * Actions + * map + * back-map response + * overwrite `Properties` if provided + * Lookup by + * `path` (provided in request) + * `user` (authenticated) + * Replace + * `path` by `PathMapped` + * `user` by `Owner` + * Overlay + * `Properties` if provided + * Activate + * `permissions_filter` by `Permissions` + +#### CxDav request "PROPFIND" with HTTP_DEPTH=1 + + * Actions + * extend list + * Lookup for active shares for `user` in sharing database + * Extend list if conditions are met + * `permissions_filter` by `Permissions` + +#### CxDav request "PROPPATCH" + + * Actions + * map + * adjust properties of a collection + * Lookup by + * `path` (provided in request) + * `user` (authenticated) + * Replace + * `path` by `PathMapped` + * `user` by `Owner` + * Activate + * `permissions_filter` by `Permissions` + * Depending on `permissions_filter`, global options and `Permissions` + * adjust properties of collection + * adjust whitelisted properties in `Properties` for overlay (see OVERLAY_PROPERTIES_WHITELIST) + +#### CxDav request "(MKCALENDAR|MKCOL)" + + * Action + * check for conflicts + * Lookup by + * `user` (authenticated) + * Verify for non-existence as `PathOrToken` in sharing database + * `path` (provided in request) + +#### CxDav request "(MOVE)" + + * Action + * map source + * map destination + * Lookup by + * `path` (provided in request) + * `user` (authenticated) + * `to_path` (provided in request) + * `to_user` (same as `user`) + * Replace + * `path` by `PathMapped` (of path) + * `user` by `Owner` (of `path`) + * `to_path` by `PathMapped` (of `to_path`) + * `to_user` by `Owner` (of `to_path`) + * Activate + * `permissions_filter` by `Permissions` (of `to_path`) + * `to_permissions_filter` by `Permissions` (of `to_path`) + ## Sharing Access ### Sharing Access via Maps @@ -579,7 +588,7 @@ Whitelisted ones are defined in `OVERLAY_PROPERTIES_WHITELIST` in `radicale/shar #### Requirements - * sharing / permit_properties_overlay = True + * `permit_properties_overlay = True` #### Test sequence From 7c50944ccccfca72937cf08e6de0e61531bb4903 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 07:31:08 +0100 Subject: [PATCH 2/7] sharing/bday: add birthday to event description --- radicale/item/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/radicale/item/__init__.py b/radicale/item/__init__.py index d6e8a474..71f174e6 100644 --- a/radicale/item/__init__.py +++ b/radicale/item/__init__.py @@ -537,6 +537,7 @@ class Item: pass bdayS = match[1] + match[2] + match[3] + bdaySdesc = match[1] + "-" + match[2] + "-" + match[3] bdayY = int(match[1]) bdayM = int(match[2]) bdayD = int(match[3]) @@ -594,6 +595,9 @@ class Item: # add transparency item_ics.vevent.add('transp').value = "TRANSPARENT" + # add description + item_ics.vevent.add('description').value = "BDAY=" + bdaySdesc + href = self.href if href is not None: href = href.rstrip(".vcf") + ".ics" From c4b9407d23fd25f71179e39ff69f4ca5816136cc Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 07:31:35 +0100 Subject: [PATCH 3/7] sharing/bday: extend test case for birthday in event description --- radicale/tests/test_sharing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 4ec1aee6..a5bfee67 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -4288,6 +4288,7 @@ permissions: RrWw""") assert "DTSTART;VALUE=DATE:19700101" in answer assert "DTEND;VALUE=DATE:19700102" in answer assert "TRANSP:TRANSPARENT" in answer + assert "DESCRIPTION:BDAY=1970-01-01" in answer # verify report as user logging.info("\n*** REPORT collection user -> ok") From cc8f6110755e8e9943be49cac6089fa8f1e6aab3 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 07:43:37 +0100 Subject: [PATCH 4/7] sharing/integ_tests: extend exclude for legacy setup.py --- setup.py.legacy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py.legacy b/setup.py.legacy index d0eb791f..83eb3830 100644 --- a/setup.py.legacy +++ b/setup.py.legacy @@ -60,7 +60,7 @@ setup( license="GNU GPL v3", platforms="Any", packages=find_packages( - exclude=["*.tests", "*.tests.*", "tests.*", "tests"]), + exclude=["*.tests", "*.tests.*", "tests.*", "tests", "integ_tests"]), package_data={"radicale": [*web_files, "py.typed"]}, entry_points={"console_scripts": ["radicale = radicale.__main__:run"]}, install_requires=install_requires, From a5e373c8c127b2fe2164879838d1b4fc6d8ad613 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 08:31:26 +0100 Subject: [PATCH 5/7] chatch PermissionError and respond 500 --- radicale/app/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index b64e2c4b..c1e3d8ff 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -549,8 +549,12 @@ class Application(ApplicationPartDelete, ApplicationPartHead, else: profiler_active = True - status, headers, answer, xml_request = function( - environ, base_prefix, path, user, remote_host, remote_useragent) + try: + status, headers, answer, xml_request = function( + environ, base_prefix, path, user, remote_host, remote_useragent) + except PermissionError as e: + logger.error("PermissionError: %s", e) + status, headers, answer, xml_request = httputils.INTERNAL_SERVER_ERROR # Profiling if self._profiling_per_request: From a71689772f67db87e6d96e44b90a129875382288 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 08:45:11 +0100 Subject: [PATCH 6/7] sharing: write properties in db on create --- radicale/sharing/csv.py | 4 +++- radicale/sharing/files.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/radicale/sharing/csv.py b/radicale/sharing/csv.py index c7ce48ed..05d2d9ae 100644 --- a/radicale/sharing/csv.py +++ b/radicale/sharing/csv.py @@ -266,7 +266,9 @@ class Sharing(sharing.BaseSharing): "HiddenByOwner": HiddenByOwner, "HiddenByUser": HiddenByUser, "TimestampCreated": Timestamp, - "TimestampUpdated": Timestamp} + "TimestampUpdated": Timestamp, + "Properties": Properties} + if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/sharing/*/create: add row: %r", row) self._sharing_cache.append(row) diff --git a/radicale/sharing/files.py b/radicale/sharing/files.py index fba8bdd6..a9c1413f 100644 --- a/radicale/sharing/files.py +++ b/radicale/sharing/files.py @@ -244,7 +244,8 @@ class Sharing(sharing.BaseSharing): "HiddenByOwner": HiddenByOwner, "HiddenByUser": HiddenByUser, "TimestampCreated": Timestamp, - "TimestampUpdated": Timestamp} + "TimestampUpdated": Timestamp, + "Properties": Properties} version = DB_VERSION From f9853ce86fdbfe93efed991061a5f9a2cedbed57 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sat, 21 Mar 2026 08:45:28 +0100 Subject: [PATCH 7/7] sharing: test case for write properties on create --- radicale/tests/test_sharing.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index a5bfee67..b1d5cc21 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -942,6 +942,7 @@ class TestSharingApiSanity(BaseTest): json_dict['User'] = "user" json_dict['PathMapped'] = path_mapped json_dict['PathOrToken'] = path_shared + json_dict['Properties'] = {"D:displayname": "Test"} _, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict) answer_dict = json.loads(answer) assert answer_dict['Status'] == "success" @@ -962,6 +963,7 @@ class TestSharingApiSanity(BaseTest): assert answer_dict['Content'][0]['HiddenByOwner'] is True assert answer_dict['Content'][0]['HiddenByUser'] is True assert answer_dict['Content'][0]['Permissions'] == "r" + assert answer_dict['Content'][0]['Properties'] == {"D:displayname": "Test"} logging.info("\n*** enable map by owner (json->json) -> 200") json_dict = {}