cosmetics, fix mk* conflict incl. testcases

This commit is contained in:
Peter Bieringer
2026-03-05 08:50:59 +01:00
parent 6848e852ca
commit bef0727b94
11 changed files with 239 additions and 105 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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")

View File

@@ -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)

View File

@@ -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"):

View File

@@ -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)

View File

@@ -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,

View File

@@ -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"):

View File

@@ -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)

View File

@@ -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",