From bef0727b944c2977e03035ee7cd8ea2d44a40f22 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 5 Mar 2026 08:50:59 +0100 Subject: [PATCH] cosmetics, fix mk* conflict incl. testcases --- SHARING.md | 107 +++++++++++++++++++++++++++++++++ radicale/app/delete.py | 10 +-- radicale/app/get.py | 10 +-- radicale/app/mkcalendar.py | 10 +-- radicale/app/mkcol.py | 10 +-- radicale/app/move.py | 20 +++--- radicale/app/propfind.py | 56 ++++++++--------- radicale/app/proppatch.py | 56 ++++++++--------- radicale/app/put.py | 10 +-- radicale/app/report.py | 24 ++++---- radicale/tests/test_sharing.py | 31 +++++++++- 11 files changed, 239 insertions(+), 105 deletions(-) diff --git a/SHARING.md b/SHARING.md index 3568dbad..8200f1e6 100644 --- a/SHARING.md +++ b/SHARING.md @@ -5,6 +5,113 @@ Static collection sharing without permissions filter using soft-links (Unix-only With 3.7.0 a major extension was implemented using internal mapping configuration stored in a database and a 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. + +Permissions are filtered by provided `Permissions`. + +### CxDAV requests + +#### 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`) + + ## Sharing Configuration Store Types of supported sharing configuration: diff --git a/radicale/app/delete.py b/radicale/app/delete.py index ff22e0c5..3401755a 100644 --- a/radicale/app/delete.py +++ b/radicale/app/delete.py @@ -60,12 +60,12 @@ class ApplicationPartDelete(ApplicationBase): permissions_filter = None if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) if not access.check("w"): return httputils.NOT_ALLOWED diff --git a/radicale/app/get.py b/radicale/app/get.py index cea26f31..fa4158ad 100644 --- a/radicale/app/get.py +++ b/radicale/app/get.py @@ -79,12 +79,12 @@ class ApplicationPartGet(ApplicationBase): permissions_filter = None if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) if not access.check("r") and "i" not in access.permissions: return httputils.NOT_ALLOWED diff --git a/radicale/app/mkcalendar.py b/radicale/app/mkcalendar.py index b892aa6b..b2825075 100644 --- a/radicale/app/mkcalendar.py +++ b/radicale/app/mkcalendar.py @@ -55,11 +55,11 @@ class ApplicationPartMkcalendar(ApplicationBase): "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST if self._sharing._enabled: - # check for shared collections (active or inactive) - collections_shared_map = self._sharing.sharing_collection_map_list(user, active=False) - if collections_shared_map: - for sharing in collections_shared_map: - if sharing['PathOrToken'] == path: + # check for shared collections (all users / active or inactive) + collections_share_map = self._sharing.sharing_collection_map_list(user=None, active=False) + if collections_share_map: + for share in collections_share_map: + if share['PathOrToken'] == path: return httputils.CONFLICT # TODO: use this? # timezone = props.get("C:calendar-timezone") diff --git a/radicale/app/mkcol.py b/radicale/app/mkcol.py index 6670349f..d3bef110 100644 --- a/radicale/app/mkcol.py +++ b/radicale/app/mkcol.py @@ -62,11 +62,11 @@ class ApplicationPartMkcol(ApplicationBase): logger.warning("MKCOL request %r (type:%s): %s", path, collection_type, "rejected because of missing rights 'W'") return httputils.NOT_ALLOWED if self._sharing._enabled: - # check for shared collections (active or inactive) - collections_shared_map = self._sharing.sharing_collection_map_list(user, active=False) - if collections_shared_map: - for sharing in collections_shared_map: - if sharing['PathOrToken'] == path: + # check for shared collections (all users, active or inactive) + collections_share_map = self._sharing.sharing_collection_map_list(user=None, active=False) + if collections_share_map: + for share in collections_share_map: + if share['PathOrToken'] == path: return httputils.CONFLICT with self._storage.acquire_lock("w", user, path=path, request="MKCOL"): item = next(iter(self._storage.discover(path)), None) diff --git a/radicale/app/move.py b/radicale/app/move.py index b093aa25..9af6e378 100644 --- a/radicale/app/move.py +++ b/radicale/app/move.py @@ -72,12 +72,12 @@ class ApplicationPartMove(ApplicationBase): permissions_filter = None if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) if not access.check("w"): return httputils.NOT_ALLOWED @@ -89,12 +89,12 @@ class ApplicationPartMove(ApplicationBase): to_path = to_path[len(base_prefix):] if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(to_path, to_user) - if sharing: + share = self._sharing.sharing_collection_resolver(to_path, to_user) + if share: # overwrite and run through extended permission check - to_path = sharing['PathMapped'] - to_user = sharing['Owner'] - to_permissions_filter = sharing['Permissions'] + to_path = share['PathMapped'] + to_user = share['Owner'] + to_permissions_filter = share['Permissions'] to_access = Access(self._rights, to_user, to_path, to_permissions_filter) to_access = Access(self._rights, to_user, to_path, to_permissions_filter) if not to_access.check("w"): diff --git a/radicale/app/propfind.py b/radicale/app/propfind.py index 64cb45f8..16db8f3c 100644 --- a/radicale/app/propfind.py +++ b/radicale/app/propfind.py @@ -37,7 +37,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]], - user: str, encoding: str, max_resource_size: int, sharing: Union[dict, None] = None) -> Optional[ET.Element]: + user: str, encoding: str, max_resource_size: int, share: Union[dict, None] = None) -> Optional[ET.Element]: """Read and answer PROPFIND requests. Read rfc4918-9.1 for info. @@ -74,7 +74,7 @@ def xml_propfind(base_prefix: str, path: str, 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, sharing=sharing)) + allprop=allprop, propname=propname, max_resource_size=max_resource_size, share=share)) return multistatus @@ -82,7 +82,7 @@ def xml_propfind(base_prefix: str, path: str, 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, sharing: Union[dict, None] = None) -> ET.Element: + propname: bool = False, allprop: bool = False, share: Union[dict, None] = None) -> 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") @@ -102,9 +102,9 @@ def xml_propfind_response( collection.path, item.href)) response = ET.Element(xmlutils.make_clark("D:response")) href = ET.Element(xmlutils.make_clark("D:href")) - if sharing: + if share: # backmap - uri = uri.replace(sharing['PathMapped'], sharing['PathOrToken']) + uri = uri.replace(share['PathMapped'], share['PathOrToken']) href.text = xmlutils.make_href(base_prefix, uri) response.append(href) @@ -183,9 +183,9 @@ def xml_propfind_response( is_collection and collection.is_principal): child_element = ET.Element(xmlutils.make_clark("D:href")) child_element.text = xmlutils.make_href(base_prefix, path) - if sharing: + if share: # backmap - child_element.text = child_element.text.replace(sharing['PathMapped'], sharing['PathOrToken']) + child_element.text = child_element.text.replace(share['PathMapped'], share['PathOrToken']) element.append(child_element) elif tag == xmlutils.make_clark("C:supported-calendar-component-set"): human_tag = xmlutils.make_human_tag(tag) @@ -221,9 +221,9 @@ def xml_propfind_response( child_element = ET.Element(xmlutils.make_clark("D:href")) child_element.text = xmlutils.make_href( base_prefix, "/%s/" % user) - if sharing: + if share: # backmap - child_element.text = child_element.text.replace(sharing['Owner'], sharing['User']) + child_element.text = child_element.text.replace(share['Owner'], share['User']) element.append(child_element) else: element.append(ET.Element( @@ -345,12 +345,12 @@ def xml_propfind_response( human_tag = xmlutils.make_human_tag(tag) tag_text = collection.get_meta(human_tag) if tag_text is not None: - if sharing: + if share: # map from overlay - if sharing['Properties']: - if human_tag in sharing['Properties']: - if sharing['Properties'][human_tag] is not None: - tag_text = sharing['Properties'][human_tag] + if share['Properties']: + if human_tag in share['Properties']: + if share['Properties'][human_tag] is not None: + tag_text = share['Properties'][human_tag] element.text = tag_text else: is404 = True @@ -426,15 +426,15 @@ class ApplicationPartPropfind(ApplicationBase): """Manage PROPFIND request.""" http_depth = environ.get("HTTP_DEPTH", "0") permissions_filter = None - sharing = None + share = None if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) if not access.check("r"): return httputils.NOT_ALLOWED @@ -467,13 +467,13 @@ class ApplicationPartPropfind(ApplicationBase): if logger.isEnabledFor(logging.DEBUG): logger.debug("TRACE/PROPFIND: get shared collections") # check for shared collections - collections_shared_map = self._sharing.sharing_collection_map_list(user) - if collections_shared_map: - for sharing in collections_shared_map: - c_share = sharing['PathOrToken'] - c_path = sharing['PathMapped'] - c_user = sharing['Owner'] - c_permissions_filter = sharing['Permissions'] + collections_share_map = self._sharing.sharing_collection_map_list(user) + if collections_share_map: + for share in collections_share_map: + c_share = share['PathOrToken'] + c_path = share['PathMapped'] + 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) c_access = Access(self._rights, c_user, c_path, c_permissions_filter) @@ -490,7 +490,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, - allowed_items, user, self._encoding, max_resource_size=self._max_resource_size, sharing=sharing) + allowed_items, user, self._encoding, max_resource_size=self._max_resource_size, share=share) if xml_answer is None: return httputils.NOT_ALLOWED return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content) diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 59d56c83..768a075f 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -37,7 +37,7 @@ from radicale.log import logger def xml_proppatch(base_prefix: str, path: str, xml_request: Optional[ET.Element], - collection: Union[storage.BaseCollection, None], sharing: Union[dict, None] = None, sharing_overlay: bool = False, _sharing: Union[sharing.BaseSharing, None] = None) -> ET.Element: + collection: Union[storage.BaseCollection, None], share: Union[dict, None] = None, share_overlay: bool = False, _sharing: Union[sharing.BaseSharing, None] = None) -> ET.Element: """Read and answer PROPPATCH requests. Read rfc4918-9.2 for info. @@ -48,9 +48,9 @@ def xml_proppatch(base_prefix: str, path: str, multistatus.append(response) href = ET.Element(xmlutils.make_clark("D:href")) href.text = xmlutils.make_href(base_prefix, path) - if sharing: + if share: # backmap - href.text = href.text.replace(sharing['PathMapped'], sharing['PathOrToken']) + href.text = href.text.replace(share['PathMapped'], share['PathOrToken']) response.append(href) # Create D:propstat element for props with status 200 OK propstat = ET.Element(xmlutils.make_clark("D:propstat")) @@ -62,27 +62,27 @@ def xml_proppatch(base_prefix: str, path: str, response.append(propstat) props_with_remove = xmlutils.props_from_request(xml_request) - if sharing and sharing_overlay: + if share and share_overlay: # PROPPATCH overlay adjustment - logger.debug("TRACE/PROPPATCH/xml_proppatch: sharing+sharing_overlay is active: %r", sharing) - if sharing['Properties'] is not None: - all_props_with_remove = cast(Dict[str, Optional[str]], radicale_item.check_and_sanitize_props(sharing['Properties'])) + logger.debug("TRACE/PROPPATCH/xml_proppatch: share+share_overlay is active: %r", share) + if share['Properties'] is not None: + all_props_with_remove = cast(Dict[str, Optional[str]], radicale_item.check_and_sanitize_props(share['Properties'])) else: all_props_with_remove = {} all_props_with_remove.update(props_with_remove) all_props = radicale_item.check_and_sanitize_props(all_props_with_remove) - logger.debug("TRACE/PROPPATCH/xml_proppatch: sharing+sharing_overlay result: %r", all_props) + logger.debug("TRACE/PROPPATCH/xml_proppatch: share+share_overlay result: %r", all_props) else: if collection is not None: # always the case, but makes mypy happy all_props_with_remove = cast(Dict[str, Optional[str]], dict(collection.get_meta())) all_props_with_remove.update(props_with_remove) all_props = radicale_item.check_and_sanitize_props(all_props_with_remove) - if sharing and sharing_overlay and _sharing is not None: + if share and share_overlay and _sharing is not None: # _sharing is not None: always the case, but makes mypy happy - _sharing.database_update_sharing(ShareType=sharing['ShareType'], - PathOrToken=sharing['PathOrToken'], - OwnerOrUser=sharing['User'], + _sharing.database_update_sharing(ShareType=share['ShareType'], + PathOrToken=share['PathOrToken'], + OwnerOrUser=share['User'], Properties=cast(Dict[str, str], all_props)) else: if collection is not None: @@ -100,21 +100,21 @@ class ApplicationPartProppatch(ApplicationBase): path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: """Manage PROPPATCH request.""" permissions_filter = None - sharing = None - sharing_overlay = False + share = None + share_overlay = False path_orig = path if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + 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) - if sharing: + 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: @@ -122,11 +122,11 @@ class ApplicationPartProppatch(ApplicationBase): return httputils.NOT_ALLOWED else: logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted by option", path_orig) - sharing_overlay = True + 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) - sharing_overlay = True + 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 @@ -134,18 +134,18 @@ class ApplicationPartProppatch(ApplicationBase): return httputils.NOT_ALLOWED else: logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access: %r", path) - if sharing: + if share: # write access -> check for enforced properties overlay 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) else: - sharing_overlay = True + share_overlay = True else: if permissions_filter is not None and "E" in permissions_filter: logger.info("PROPPATCH request on shared %r: write-permissions, overlay not enforced, but enforced by permission 'E'", path_orig) - sharing_overlay = True + share_overlay = True try: xml_content = self._read_xml_request_body(environ) except RuntimeError as e: @@ -156,13 +156,13 @@ class ApplicationPartProppatch(ApplicationBase): logger.debug("Client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT - if sharing_overlay: + if share_overlay: # call API function internally and no not trigger any hook headers = {"DAV": httputils.DAV_HEADERS, "Content-Type": "text/xml; charset=%s" % self._encoding} try: xml_answer = xml_proppatch(base_prefix, path, xml_content, - None, sharing, sharing_overlay, self._sharing) + None, share, share_overlay, self._sharing) if xml_content is not None: content = DefusedET.tostring( xml_content, @@ -199,7 +199,7 @@ class ApplicationPartProppatch(ApplicationBase): "Content-Type": "text/xml; charset=%s" % self._encoding} try: xml_answer = xml_proppatch(base_prefix, path, xml_content, - item, sharing) + item, share) if xml_content is not None: content = DefusedET.tostring( xml_content, diff --git a/radicale/app/put.py b/radicale/app/put.py index 4e62ec1d..b9b764c8 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -184,12 +184,12 @@ class ApplicationPartPut(ApplicationBase): permissions_filter = None if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) access = Access(self._rights, user, path, permissions_filter) if not access.check("w"): diff --git a/radicale/app/report.py b/radicale/app/report.py index 35e9237e..b940fc92 100644 --- a/radicale/app/report.py +++ b/radicale/app/report.py @@ -151,7 +151,7 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], collection: storage.BaseCollection, encoding: str, unlock_storage_fn: Callable[[], None], max_occurrence: int = 0, user: str = "", remote_addr: str = "", remote_useragent: str = "", - sharing: Union[dict, None] = None) -> Tuple[int, ET.Element]: + share: Union[dict, None] = None) -> Tuple[int, ET.Element]: """Read and answer REPORT requests that return XML. Read rfc3253-3.6 for info. @@ -360,7 +360,7 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], if found_props or not_found_props: multistatus.append(xml_item_response( base_prefix, uri, found_props=found_props, - not_found_props=not_found_props, found_item=True, sharing=sharing)) + not_found_props=not_found_props, found_item=True, share=share)) return client.MULTI_STATUS, multistatus @@ -711,13 +711,13 @@ def _find_overridden( def xml_item_response(base_prefix: str, href: str, found_props: Sequence[ET.Element] = (), not_found_props: Sequence[ET.Element] = (), - found_item: bool = True, sharing: Union[dict, None] = None) -> ET.Element: + found_item: bool = True, share: Union[dict, None] = None) -> ET.Element: response = ET.Element(xmlutils.make_clark("D:response")) href_element = ET.Element(xmlutils.make_clark("D:href")) href_element.text = xmlutils.make_href(base_prefix, href) - if sharing: - href_element.text = href_element.text.replace(sharing['PathMapped'], sharing['PathOrToken']) + if share: + href_element.text = href_element.text.replace(share['PathMapped'], share['PathOrToken']) response.append(href_element) if found_item: @@ -820,15 +820,15 @@ class ApplicationPartReport(ApplicationBase): path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: """Manage REPORT request.""" permissions_filter = None - sharing = None + share = None if self._sharing._enabled: # Sharing by token or map (if enabled) - sharing = self._sharing.sharing_collection_resolver(path, user) - if sharing: + share = self._sharing.sharing_collection_resolver(path, user) + if share: # overwrite and run through extended permission check - path = sharing['PathMapped'] - user = sharing['Owner'] - permissions_filter = sharing['Permissions'] + path = share['PathMapped'] + user = share['Owner'] + permissions_filter = share['Permissions'] access = Access(self._rights, user, path, permissions_filter) if not access.check("r"): return httputils.NOT_ALLOWED @@ -871,7 +871,7 @@ class ApplicationPartReport(ApplicationBase): try: status, xml_answer = xml_report( base_prefix, path, xml_content, collection, self._encoding, - lock_stack.close, max_occurrence, user, remote_host, remote_useragent, sharing=sharing) + lock_stack.close, max_occurrence, user, remote_host, remote_useragent, share=share) except ValueError as e: logger.warning( "Bad REPORT request on %r: %s", path, e, exc_info=True) diff --git a/radicale/tests/test_sharing.py b/radicale/tests/test_sharing.py index 41670242..303f3a79 100644 --- a/radicale/tests/test_sharing.py +++ b/radicale/tests/test_sharing.py @@ -2415,8 +2415,23 @@ class TestSharingApiSanity(BaseTest): "collection_by_token": "True"}, "logging": {"request_header_on_debug": "False", "response_content_on_debug": "False", + "rights_rule_doesnt_match_on_debug": "True", "request_content_on_debug": "True"}, "rights": {"type": "owner_only"}}) + + rights_file_path = os.path.join(self.colpath, "rights") + with open(rights_file_path, "w") as f: + f.write("""\ +[default-collection] +user: .+ +collection: .+ +permissions: RrWw +[default] +user: .+ +collection: {user}(/.*)? +permissions: RrWw""") + self.configure({"rights": {"file": rights_file_path}}) + json_dict: dict path_user1 = "/user1/calendarCCu1.ics/" @@ -2440,6 +2455,9 @@ class TestSharingApiSanity(BaseTest): logging.info("\n*** test: %s", db_type) self.configure({"sharing": {"type": db_type}}) + # owner_only + self.configure({"rights": {"type": "owner_only"}}) + # create map logging.info("\n*** create map user1/owner1 -> ok") json_dict = {} @@ -2453,7 +2471,7 @@ class TestSharingApiSanity(BaseTest): answer_dict = json.loads(answer) assert answer_dict['Status'] == "success" - logging.info("\n*** mkcalendar user1 for shared -> conflict") + logging.info("\n*** mkcalendar as user1 for user1/shared1 -> conflict") self.mkcalendar(path_user1_shared1, login="user1:user1pw", check=409) # create map @@ -2469,7 +2487,7 @@ class TestSharingApiSanity(BaseTest): answer_dict = json.loads(answer) assert answer_dict['Status'] == "success" - logging.info("\n*** mkcol user2 for shared -> conflict") + logging.info("\n*** mkcol as user2 for user2/shared1 -> conflict") self.mkcalendar(path_user2_shared1, login="user2:user2pw", check=409) # create map @@ -2483,6 +2501,15 @@ class TestSharingApiSanity(BaseTest): json_dict['Hidden'] = False _, headers, answer = self._sharing_api_json("map", "create", check=409, login="owner1:owner1pw", json_dict=json_dict) + # from_file + self.configure({"rights": {"type": "from_file"}}) + + logging.info("\n*** mkcalendar as user1 for user2/shared1 with rights from file -> conflict") + self.mkcalendar(path_user2_shared1, login="user1:user1pw", check=409) + + logging.info("\n*** mkcol as user1 for user2/shared1 with rights from file -> conflict") + self.mkcol(path_user2_shared1, login="user1:user1pw", check=409) + def test_sharing_api_permissions_global(self) -> None: """sharing API usage tests related to global permissions.""" self.configure({"auth": {"type": "htpasswd",