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.
|
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
|
## Sharing Configuration Store
|
||||||
|
|
||||||
Types of supported sharing configuration:
|
Types of supported sharing configuration:
|
||||||
|
|||||||
@@ -60,12 +60,12 @@ class ApplicationPartDelete(ApplicationBase):
|
|||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
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"):
|
if not access.check("w"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
|
|||||||
@@ -79,12 +79,12 @@ class ApplicationPartGet(ApplicationBase):
|
|||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
permissions_filter = share['Permissions']
|
||||||
access = Access(self._rights, user, path, permissions_filter)
|
access = Access(self._rights, user, path, permissions_filter)
|
||||||
if not access.check("r") and "i" not in access.permissions:
|
if not access.check("r") and "i" not in access.permissions:
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ class ApplicationPartMkcalendar(ApplicationBase):
|
|||||||
"Bad MKCALENDAR request on %r: %s", path, e, exc_info=True)
|
"Bad MKCALENDAR request on %r: %s", path, e, exc_info=True)
|
||||||
return httputils.BAD_REQUEST
|
return httputils.BAD_REQUEST
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# check for shared collections (active or inactive)
|
# check for shared collections (all users / active or inactive)
|
||||||
collections_shared_map = self._sharing.sharing_collection_map_list(user, active=False)
|
collections_share_map = self._sharing.sharing_collection_map_list(user=None, active=False)
|
||||||
if collections_shared_map:
|
if collections_share_map:
|
||||||
for sharing in collections_shared_map:
|
for share in collections_share_map:
|
||||||
if sharing['PathOrToken'] == path:
|
if share['PathOrToken'] == path:
|
||||||
return httputils.CONFLICT
|
return httputils.CONFLICT
|
||||||
# TODO: use this?
|
# TODO: use this?
|
||||||
# timezone = props.get("C:calendar-timezone")
|
# 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'")
|
logger.warning("MKCOL request %r (type:%s): %s", path, collection_type, "rejected because of missing rights 'W'")
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# check for shared collections (active or inactive)
|
# check for shared collections (all users, active or inactive)
|
||||||
collections_shared_map = self._sharing.sharing_collection_map_list(user, active=False)
|
collections_share_map = self._sharing.sharing_collection_map_list(user=None, active=False)
|
||||||
if collections_shared_map:
|
if collections_share_map:
|
||||||
for sharing in collections_shared_map:
|
for share in collections_share_map:
|
||||||
if sharing['PathOrToken'] == path:
|
if share['PathOrToken'] == path:
|
||||||
return httputils.CONFLICT
|
return httputils.CONFLICT
|
||||||
with self._storage.acquire_lock("w", user, path=path, request="MKCOL"):
|
with self._storage.acquire_lock("w", user, path=path, request="MKCOL"):
|
||||||
item = next(iter(self._storage.discover(path)), None)
|
item = next(iter(self._storage.discover(path)), None)
|
||||||
|
|||||||
@@ -72,12 +72,12 @@ class ApplicationPartMove(ApplicationBase):
|
|||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
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"):
|
if not access.check("w"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
@@ -89,12 +89,12 @@ class ApplicationPartMove(ApplicationBase):
|
|||||||
to_path = to_path[len(base_prefix):]
|
to_path = to_path[len(base_prefix):]
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(to_path, to_user)
|
share = self._sharing.sharing_collection_resolver(to_path, to_user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
to_path = sharing['PathMapped']
|
to_path = share['PathMapped']
|
||||||
to_user = sharing['Owner']
|
to_user = share['Owner']
|
||||||
to_permissions_filter = sharing['Permissions']
|
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)
|
||||||
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"):
|
if not to_access.check("w"):
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from radicale.log import logger
|
|||||||
def xml_propfind(base_prefix: str, path: str,
|
def xml_propfind(base_prefix: str, path: str,
|
||||||
xml_request: Optional[ET.Element],
|
xml_request: Optional[ET.Element],
|
||||||
allowed_items: Iterable[Tuple[types.CollectionOrItem, str]],
|
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 and answer PROPFIND requests.
|
||||||
|
|
||||||
Read rfc4918-9.1 for info.
|
Read rfc4918-9.1 for info.
|
||||||
@@ -74,7 +74,7 @@ def xml_propfind(base_prefix: str, path: str,
|
|||||||
write = permission == "w"
|
write = permission == "w"
|
||||||
multistatus.append(xml_propfind_response(
|
multistatus.append(xml_propfind_response(
|
||||||
base_prefix, path, item, props, user, encoding, write=write,
|
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
|
return multistatus
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def xml_propfind(base_prefix: str, path: str,
|
|||||||
def xml_propfind_response(
|
def xml_propfind_response(
|
||||||
base_prefix: str, path: str, item: types.CollectionOrItem,
|
base_prefix: str, path: str, item: types.CollectionOrItem,
|
||||||
props: Sequence[str], user: str, encoding: str, max_resource_size: int, write: bool = False,
|
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."""
|
"""Build and return a PROPFIND response."""
|
||||||
if propname and allprop or (props and (propname or allprop)):
|
if propname and allprop or (props and (propname or allprop)):
|
||||||
raise ValueError("Only use one of props, propname and allprops")
|
raise ValueError("Only use one of props, propname and allprops")
|
||||||
@@ -102,9 +102,9 @@ def xml_propfind_response(
|
|||||||
collection.path, item.href))
|
collection.path, item.href))
|
||||||
response = ET.Element(xmlutils.make_clark("D:response"))
|
response = ET.Element(xmlutils.make_clark("D:response"))
|
||||||
href = ET.Element(xmlutils.make_clark("D:href"))
|
href = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
if sharing:
|
if share:
|
||||||
# backmap
|
# backmap
|
||||||
uri = uri.replace(sharing['PathMapped'], sharing['PathOrToken'])
|
uri = uri.replace(share['PathMapped'], share['PathOrToken'])
|
||||||
href.text = xmlutils.make_href(base_prefix, uri)
|
href.text = xmlutils.make_href(base_prefix, uri)
|
||||||
response.append(href)
|
response.append(href)
|
||||||
|
|
||||||
@@ -183,9 +183,9 @@ def xml_propfind_response(
|
|||||||
is_collection and collection.is_principal):
|
is_collection and collection.is_principal):
|
||||||
child_element = ET.Element(xmlutils.make_clark("D:href"))
|
child_element = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
child_element.text = xmlutils.make_href(base_prefix, path)
|
child_element.text = xmlutils.make_href(base_prefix, path)
|
||||||
if sharing:
|
if share:
|
||||||
# backmap
|
# 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)
|
element.append(child_element)
|
||||||
elif tag == xmlutils.make_clark("C:supported-calendar-component-set"):
|
elif tag == xmlutils.make_clark("C:supported-calendar-component-set"):
|
||||||
human_tag = xmlutils.make_human_tag(tag)
|
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 = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
child_element.text = xmlutils.make_href(
|
child_element.text = xmlutils.make_href(
|
||||||
base_prefix, "/%s/" % user)
|
base_prefix, "/%s/" % user)
|
||||||
if sharing:
|
if share:
|
||||||
# backmap
|
# 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)
|
element.append(child_element)
|
||||||
else:
|
else:
|
||||||
element.append(ET.Element(
|
element.append(ET.Element(
|
||||||
@@ -345,12 +345,12 @@ def xml_propfind_response(
|
|||||||
human_tag = xmlutils.make_human_tag(tag)
|
human_tag = xmlutils.make_human_tag(tag)
|
||||||
tag_text = collection.get_meta(human_tag)
|
tag_text = collection.get_meta(human_tag)
|
||||||
if tag_text is not None:
|
if tag_text is not None:
|
||||||
if sharing:
|
if share:
|
||||||
# map from overlay
|
# map from overlay
|
||||||
if sharing['Properties']:
|
if share['Properties']:
|
||||||
if human_tag in sharing['Properties']:
|
if human_tag in share['Properties']:
|
||||||
if sharing['Properties'][human_tag] is not None:
|
if share['Properties'][human_tag] is not None:
|
||||||
tag_text = sharing['Properties'][human_tag]
|
tag_text = share['Properties'][human_tag]
|
||||||
element.text = tag_text
|
element.text = tag_text
|
||||||
else:
|
else:
|
||||||
is404 = True
|
is404 = True
|
||||||
@@ -426,15 +426,15 @@ class ApplicationPartPropfind(ApplicationBase):
|
|||||||
"""Manage PROPFIND request."""
|
"""Manage PROPFIND request."""
|
||||||
http_depth = environ.get("HTTP_DEPTH", "0")
|
http_depth = environ.get("HTTP_DEPTH", "0")
|
||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
sharing = None
|
share = None
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
permissions_filter = share['Permissions']
|
||||||
access = Access(self._rights, user, path, permissions_filter)
|
access = Access(self._rights, user, path, permissions_filter)
|
||||||
if not access.check("r"):
|
if not access.check("r"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
@@ -467,13 +467,13 @@ class ApplicationPartPropfind(ApplicationBase):
|
|||||||
if logger.isEnabledFor(logging.DEBUG):
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
logger.debug("TRACE/PROPFIND: get shared collections")
|
logger.debug("TRACE/PROPFIND: get shared collections")
|
||||||
# check for shared collections
|
# check for shared collections
|
||||||
collections_shared_map = self._sharing.sharing_collection_map_list(user)
|
collections_share_map = self._sharing.sharing_collection_map_list(user)
|
||||||
if collections_shared_map:
|
if collections_share_map:
|
||||||
for sharing in collections_shared_map:
|
for share in collections_share_map:
|
||||||
c_share = sharing['PathOrToken']
|
c_share = share['PathOrToken']
|
||||||
c_path = sharing['PathMapped']
|
c_path = share['PathMapped']
|
||||||
c_user = sharing['Owner']
|
c_user = share['Owner']
|
||||||
c_permissions_filter = sharing['Permissions']
|
c_permissions_filter = share['Permissions']
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
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)
|
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)
|
c_access = Access(self._rights, c_user, c_path, c_permissions_filter)
|
||||||
@@ -490,7 +490,7 @@ class ApplicationPartPropfind(ApplicationBase):
|
|||||||
headers = {"DAV": httputils.DAV_HEADERS,
|
headers = {"DAV": httputils.DAV_HEADERS,
|
||||||
"Content-Type": "text/xml; charset=%s" % self._encoding}
|
"Content-Type": "text/xml; charset=%s" % self._encoding}
|
||||||
xml_answer = xml_propfind(base_prefix, path, xml_content,
|
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:
|
if xml_answer is None:
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content)
|
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,
|
def xml_proppatch(base_prefix: str, path: str,
|
||||||
xml_request: Optional[ET.Element],
|
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 and answer PROPPATCH requests.
|
||||||
|
|
||||||
Read rfc4918-9.2 for info.
|
Read rfc4918-9.2 for info.
|
||||||
@@ -48,9 +48,9 @@ def xml_proppatch(base_prefix: str, path: str,
|
|||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
href = ET.Element(xmlutils.make_clark("D:href"))
|
href = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
href.text = xmlutils.make_href(base_prefix, path)
|
href.text = xmlutils.make_href(base_prefix, path)
|
||||||
if sharing:
|
if share:
|
||||||
# backmap
|
# backmap
|
||||||
href.text = href.text.replace(sharing['PathMapped'], sharing['PathOrToken'])
|
href.text = href.text.replace(share['PathMapped'], share['PathOrToken'])
|
||||||
response.append(href)
|
response.append(href)
|
||||||
# Create D:propstat element for props with status 200 OK
|
# Create D:propstat element for props with status 200 OK
|
||||||
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
|
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
|
||||||
@@ -62,27 +62,27 @@ def xml_proppatch(base_prefix: str, path: str,
|
|||||||
response.append(propstat)
|
response.append(propstat)
|
||||||
|
|
||||||
props_with_remove = xmlutils.props_from_request(xml_request)
|
props_with_remove = xmlutils.props_from_request(xml_request)
|
||||||
if sharing and sharing_overlay:
|
if share and share_overlay:
|
||||||
# PROPPATCH overlay adjustment
|
# PROPPATCH overlay adjustment
|
||||||
logger.debug("TRACE/PROPPATCH/xml_proppatch: sharing+sharing_overlay is active: %r", sharing)
|
logger.debug("TRACE/PROPPATCH/xml_proppatch: share+share_overlay is active: %r", share)
|
||||||
if sharing['Properties'] is not None:
|
if share['Properties'] is not None:
|
||||||
all_props_with_remove = cast(Dict[str, Optional[str]], radicale_item.check_and_sanitize_props(sharing['Properties']))
|
all_props_with_remove = cast(Dict[str, Optional[str]], radicale_item.check_and_sanitize_props(share['Properties']))
|
||||||
else:
|
else:
|
||||||
all_props_with_remove = {}
|
all_props_with_remove = {}
|
||||||
all_props_with_remove.update(props_with_remove)
|
all_props_with_remove.update(props_with_remove)
|
||||||
all_props = radicale_item.check_and_sanitize_props(all_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:
|
else:
|
||||||
if collection is not None:
|
if collection is not None:
|
||||||
# always the case, but makes mypy happy
|
# always the case, but makes mypy happy
|
||||||
all_props_with_remove = cast(Dict[str, Optional[str]], dict(collection.get_meta()))
|
all_props_with_remove = cast(Dict[str, Optional[str]], dict(collection.get_meta()))
|
||||||
all_props_with_remove.update(props_with_remove)
|
all_props_with_remove.update(props_with_remove)
|
||||||
all_props = radicale_item.check_and_sanitize_props(all_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 is not None: always the case, but makes mypy happy
|
||||||
_sharing.database_update_sharing(ShareType=sharing['ShareType'],
|
_sharing.database_update_sharing(ShareType=share['ShareType'],
|
||||||
PathOrToken=sharing['PathOrToken'],
|
PathOrToken=share['PathOrToken'],
|
||||||
OwnerOrUser=sharing['User'],
|
OwnerOrUser=share['User'],
|
||||||
Properties=cast(Dict[str, str], all_props))
|
Properties=cast(Dict[str, str], all_props))
|
||||||
else:
|
else:
|
||||||
if collection is not None:
|
if collection is not None:
|
||||||
@@ -100,21 +100,21 @@ class ApplicationPartProppatch(ApplicationBase):
|
|||||||
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
|
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
|
||||||
"""Manage PROPPATCH request."""
|
"""Manage PROPPATCH request."""
|
||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
sharing = None
|
share = None
|
||||||
sharing_overlay = False
|
share_overlay = False
|
||||||
path_orig = path
|
path_orig = path
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
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"):
|
if not access.check("w"):
|
||||||
logger.debug("TRACE/PROPPATCH/xml_proppatch: no write-access: %r", path)
|
logger.debug("TRACE/PROPPATCH/xml_proppatch: no write-access: %r", path)
|
||||||
if sharing:
|
if share:
|
||||||
# no write access -> use properties overlay
|
# no write access -> use properties overlay
|
||||||
if self._sharing.permit_properties_overlay:
|
if self._sharing.permit_properties_overlay:
|
||||||
if permissions_filter is not None and "p" in permissions_filter:
|
if permissions_filter is not None and "p" in permissions_filter:
|
||||||
@@ -122,11 +122,11 @@ class ApplicationPartProppatch(ApplicationBase):
|
|||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
else:
|
else:
|
||||||
logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted by option", path_orig)
|
logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted by option", path_orig)
|
||||||
sharing_overlay = True
|
share_overlay = True
|
||||||
else:
|
else:
|
||||||
if permissions_filter is not None and "P" in permissions_filter:
|
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)
|
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:
|
else:
|
||||||
logger.info("PROPPATCH request on shared %r: no write-permissions and overlay denied by option", path_orig)
|
logger.info("PROPPATCH request on shared %r: no write-permissions and overlay denied by option", path_orig)
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
@@ -134,18 +134,18 @@ class ApplicationPartProppatch(ApplicationBase):
|
|||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
else:
|
else:
|
||||||
logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access: %r", path)
|
logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access: %r", path)
|
||||||
if sharing:
|
if share:
|
||||||
# write access -> check for enforced properties overlay
|
# write access -> check for enforced properties overlay
|
||||||
logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access/sharing: %r", path_orig)
|
logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access/sharing: %r", path_orig)
|
||||||
if self._sharing.enforce_properties_overlay:
|
if self._sharing.enforce_properties_overlay:
|
||||||
if permissions_filter is not None and "e" in permissions_filter:
|
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)
|
logger.info("PROPPATCH request on shared %r: write-permissions, overlay enforced, but disabled by permission 'e'", path_orig)
|
||||||
else:
|
else:
|
||||||
sharing_overlay = True
|
share_overlay = True
|
||||||
else:
|
else:
|
||||||
if permissions_filter is not None and "E" in permissions_filter:
|
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)
|
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:
|
try:
|
||||||
xml_content = self._read_xml_request_body(environ)
|
xml_content = self._read_xml_request_body(environ)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
@@ -156,13 +156,13 @@ class ApplicationPartProppatch(ApplicationBase):
|
|||||||
logger.debug("Client timed out", exc_info=True)
|
logger.debug("Client timed out", exc_info=True)
|
||||||
return httputils.REQUEST_TIMEOUT
|
return httputils.REQUEST_TIMEOUT
|
||||||
|
|
||||||
if sharing_overlay:
|
if share_overlay:
|
||||||
# call API function internally and no not trigger any hook
|
# call API function internally and no not trigger any hook
|
||||||
headers = {"DAV": httputils.DAV_HEADERS,
|
headers = {"DAV": httputils.DAV_HEADERS,
|
||||||
"Content-Type": "text/xml; charset=%s" % self._encoding}
|
"Content-Type": "text/xml; charset=%s" % self._encoding}
|
||||||
try:
|
try:
|
||||||
xml_answer = xml_proppatch(base_prefix, path, xml_content,
|
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:
|
if xml_content is not None:
|
||||||
content = DefusedET.tostring(
|
content = DefusedET.tostring(
|
||||||
xml_content,
|
xml_content,
|
||||||
@@ -199,7 +199,7 @@ class ApplicationPartProppatch(ApplicationBase):
|
|||||||
"Content-Type": "text/xml; charset=%s" % self._encoding}
|
"Content-Type": "text/xml; charset=%s" % self._encoding}
|
||||||
try:
|
try:
|
||||||
xml_answer = xml_proppatch(base_prefix, path, xml_content,
|
xml_answer = xml_proppatch(base_prefix, path, xml_content,
|
||||||
item, sharing)
|
item, share)
|
||||||
if xml_content is not None:
|
if xml_content is not None:
|
||||||
content = DefusedET.tostring(
|
content = DefusedET.tostring(
|
||||||
xml_content,
|
xml_content,
|
||||||
|
|||||||
@@ -184,12 +184,12 @@ class ApplicationPartPut(ApplicationBase):
|
|||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
permissions_filter = share['Permissions']
|
||||||
access = Access(self._rights, user, path, permissions_filter)
|
access = Access(self._rights, user, path, permissions_filter)
|
||||||
access = Access(self._rights, user, path, permissions_filter)
|
access = Access(self._rights, user, path, permissions_filter)
|
||||||
if not access.check("w"):
|
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,
|
collection: storage.BaseCollection, encoding: str,
|
||||||
unlock_storage_fn: Callable[[], None],
|
unlock_storage_fn: Callable[[], None],
|
||||||
max_occurrence: int = 0, user: str = "", remote_addr: str = "", remote_useragent: str = "",
|
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 and answer REPORT requests that return XML.
|
||||||
|
|
||||||
Read rfc3253-3.6 for info.
|
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:
|
if found_props or not_found_props:
|
||||||
multistatus.append(xml_item_response(
|
multistatus.append(xml_item_response(
|
||||||
base_prefix, uri, found_props=found_props,
|
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
|
return client.MULTI_STATUS, multistatus
|
||||||
|
|
||||||
@@ -711,13 +711,13 @@ def _find_overridden(
|
|||||||
def xml_item_response(base_prefix: str, href: str,
|
def xml_item_response(base_prefix: str, href: str,
|
||||||
found_props: Sequence[ET.Element] = (),
|
found_props: Sequence[ET.Element] = (),
|
||||||
not_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"))
|
response = ET.Element(xmlutils.make_clark("D:response"))
|
||||||
|
|
||||||
href_element = ET.Element(xmlutils.make_clark("D:href"))
|
href_element = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
href_element.text = xmlutils.make_href(base_prefix, href)
|
href_element.text = xmlutils.make_href(base_prefix, href)
|
||||||
if sharing:
|
if share:
|
||||||
href_element.text = href_element.text.replace(sharing['PathMapped'], sharing['PathOrToken'])
|
href_element.text = href_element.text.replace(share['PathMapped'], share['PathOrToken'])
|
||||||
response.append(href_element)
|
response.append(href_element)
|
||||||
|
|
||||||
if found_item:
|
if found_item:
|
||||||
@@ -820,15 +820,15 @@ class ApplicationPartReport(ApplicationBase):
|
|||||||
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
|
path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse:
|
||||||
"""Manage REPORT request."""
|
"""Manage REPORT request."""
|
||||||
permissions_filter = None
|
permissions_filter = None
|
||||||
sharing = None
|
share = None
|
||||||
if self._sharing._enabled:
|
if self._sharing._enabled:
|
||||||
# Sharing by token or map (if enabled)
|
# Sharing by token or map (if enabled)
|
||||||
sharing = self._sharing.sharing_collection_resolver(path, user)
|
share = self._sharing.sharing_collection_resolver(path, user)
|
||||||
if sharing:
|
if share:
|
||||||
# overwrite and run through extended permission check
|
# overwrite and run through extended permission check
|
||||||
path = sharing['PathMapped']
|
path = share['PathMapped']
|
||||||
user = sharing['Owner']
|
user = share['Owner']
|
||||||
permissions_filter = sharing['Permissions']
|
permissions_filter = share['Permissions']
|
||||||
access = Access(self._rights, user, path, permissions_filter)
|
access = Access(self._rights, user, path, permissions_filter)
|
||||||
if not access.check("r"):
|
if not access.check("r"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
@@ -871,7 +871,7 @@ class ApplicationPartReport(ApplicationBase):
|
|||||||
try:
|
try:
|
||||||
status, xml_answer = xml_report(
|
status, xml_answer = xml_report(
|
||||||
base_prefix, path, xml_content, collection, self._encoding,
|
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:
|
except ValueError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Bad REPORT request on %r: %s", path, e, exc_info=True)
|
"Bad REPORT request on %r: %s", path, e, exc_info=True)
|
||||||
|
|||||||
@@ -2415,8 +2415,23 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
"collection_by_token": "True"},
|
"collection_by_token": "True"},
|
||||||
"logging": {"request_header_on_debug": "False",
|
"logging": {"request_header_on_debug": "False",
|
||||||
"response_content_on_debug": "False",
|
"response_content_on_debug": "False",
|
||||||
|
"rights_rule_doesnt_match_on_debug": "True",
|
||||||
"request_content_on_debug": "True"},
|
"request_content_on_debug": "True"},
|
||||||
"rights": {"type": "owner_only"}})
|
"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
|
json_dict: dict
|
||||||
|
|
||||||
path_user1 = "/user1/calendarCCu1.ics/"
|
path_user1 = "/user1/calendarCCu1.ics/"
|
||||||
@@ -2440,6 +2455,9 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
logging.info("\n*** test: %s", db_type)
|
logging.info("\n*** test: %s", db_type)
|
||||||
self.configure({"sharing": {"type": db_type}})
|
self.configure({"sharing": {"type": db_type}})
|
||||||
|
|
||||||
|
# owner_only
|
||||||
|
self.configure({"rights": {"type": "owner_only"}})
|
||||||
|
|
||||||
# create map
|
# create map
|
||||||
logging.info("\n*** create map user1/owner1 -> ok")
|
logging.info("\n*** create map user1/owner1 -> ok")
|
||||||
json_dict = {}
|
json_dict = {}
|
||||||
@@ -2453,7 +2471,7 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
answer_dict = json.loads(answer)
|
answer_dict = json.loads(answer)
|
||||||
assert answer_dict['Status'] == "success"
|
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)
|
self.mkcalendar(path_user1_shared1, login="user1:user1pw", check=409)
|
||||||
|
|
||||||
# create map
|
# create map
|
||||||
@@ -2469,7 +2487,7 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
answer_dict = json.loads(answer)
|
answer_dict = json.loads(answer)
|
||||||
assert answer_dict['Status'] == "success"
|
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)
|
self.mkcalendar(path_user2_shared1, login="user2:user2pw", check=409)
|
||||||
|
|
||||||
# create map
|
# create map
|
||||||
@@ -2483,6 +2501,15 @@ class TestSharingApiSanity(BaseTest):
|
|||||||
json_dict['Hidden'] = False
|
json_dict['Hidden'] = False
|
||||||
_, headers, answer = self._sharing_api_json("map", "create", check=409, login="owner1:owner1pw", json_dict=json_dict)
|
_, 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:
|
def test_sharing_api_permissions_global(self) -> None:
|
||||||
"""sharing API usage tests related to global permissions."""
|
"""sharing API usage tests related to global permissions."""
|
||||||
self.configure({"auth": {"type": "htpasswd",
|
self.configure({"auth": {"type": "htpasswd",
|
||||||
|
|||||||
Reference in New Issue
Block a user