cosmetics, fix mk* conflict incl. testcases
This commit is contained in:
107
SHARING.md
107
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user