sharing: properties overlay by proppatch

This commit is contained in:
Peter Bieringer
2026-02-28 22:10:01 +01:00
parent 6002894191
commit 51fb882cab
7 changed files with 539 additions and 13 deletions

View File

@@ -2110,8 +2110,8 @@ Permit create of token-based sharing
Default: `false` Default: `false`
* If `False` it can be explicitly granted by `permissions: t` * If `False` it can be explicitly granted by permissions: `T`
* If `True` it can be explicitly forbidden by `permissions: T` * If `True` it can be explicitly forbidden by permissions: `t`
##### permit_create_map ##### permit_create_map
@@ -2121,8 +2121,30 @@ Permit create of map-based sharing
Default: `false` Default: `false`
* If `False` it can be explicitly granted by `permissions: m` * If `False` it can be explicitly granted by permissions: `M`
* If `True` it can be explicitly forbidden by `permissions: M` * If `True` it can be explicitly forbidden by permissions: `m`
##### permit_properties_overlay
_(>= 3.7.0)_
Permit (limited) properties overlay by user of shared collection
Default: `false`
* If `False` it can be explicitly granted by *share* permissions: `P`
* If `True` it can be explicitly forbidden by *share* permissions: `p`
##### enforce_properties_overlay
_(>= 3.7.0)_
Enforce properties overlay even on write access
Default: `true`
* If `False` it can be explicitly enforced by *share* permissions: `E`
* If `True` it can be explicitly forbidden by *share* permissions: `e`
##### default_permissions_create_token ##### default_permissions_create_token
@@ -2130,12 +2152,16 @@ Default permissions for create token-based sharing
Default: `r` Default: `r`
Supported: `rwEePp`
##### default_permissions_create_map ##### default_permissions_create_map
Default permissions for map-based sharing Default permissions for map-based sharing
Default: `r` Default: `r`
Supported: `rwEePp`
## Supported Clients ## Supported Clients
Radicale has been tested with: Radicale has been tested with:

12
config
View File

@@ -327,10 +327,22 @@
# If True it can be explicitly forbidden by permissions: M # If True it can be explicitly forbidden by permissions: M
#permit_create_map = false #permit_create_map = false
# Permit properties overlay
# If False it can be explicitly granted by share permissions: P
# If True it can be explicitly forbidden by share permissions: p
#permit_properties_overlay = false
# Enforce properties overlay on write access
# If False it can be explicitly enforced by share permissions: E
# If True it can be explicitly forbidden by share permissions: e
#enforce_properties_overlay = true
# Default permissions for token-based sharing # Default permissions for token-based sharing
# Supported: rwEePp
#default_permissions_create_token = r #default_permissions_create_token = r
# Default permissions for map-based sharing # Default permissions for map-based sharing
# Supported: rwEePp
#default_permissions_create_map = r #default_permissions_create_map = r

View File

@@ -29,7 +29,7 @@ from typing import Dict, Optional, Union, cast
import defusedxml.ElementTree as DefusedET import defusedxml.ElementTree as DefusedET
import radicale.item as radicale_item import radicale.item as radicale_item
from radicale import httputils, storage, types, xmlutils from radicale import httputils, sharing, storage, types, xmlutils
from radicale.app.base import Access, ApplicationBase from radicale.app.base import Access, ApplicationBase
from radicale.hook import HookNotificationItem, HookNotificationItemTypes from radicale.hook import HookNotificationItem, HookNotificationItemTypes
from radicale.log import logger from radicale.log import logger
@@ -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: storage.BaseCollection, sharing: Union[dict, None] = None) -> ET.Element: collection: Union[storage.BaseCollection, None], sharing: Union[dict, None] = None, sharing_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.
@@ -62,11 +62,32 @@ 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)
all_props_with_remove = cast(Dict[str, Optional[str]], if sharing and sharing_overlay:
dict(collection.get_meta())) # 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']))
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)
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_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)
collection.set_meta(all_props) if sharing and sharing_overlay and _sharing is not None:
# _sharing is not None: always the case, but makes mypy happy
_sharing.update_sharing(ShareType=sharing['ShareType'],
PathOrToken=sharing['PathOrToken'],
OwnerOrUser=sharing['User'],
Properties=cast(Dict[str, str], all_props))
else:
if collection is not None:
# always the case, but makes mypy happy
collection.set_meta(all_props)
for short_name in props_with_remove: for short_name in props_with_remove:
props_ok.append(ET.Element(xmlutils.make_clark(short_name))) props_ok.append(ET.Element(xmlutils.make_clark(short_name)))
@@ -80,6 +101,8 @@ class ApplicationPartProppatch(ApplicationBase):
"""Manage PROPPATCH request.""" """Manage PROPPATCH request."""
permissions_filter = None permissions_filter = None
sharing = None sharing = None
sharing_overlay = False
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) sharing = self._sharing.sharing_collection_resolver(path, user)
@@ -90,7 +113,39 @@ class ApplicationPartProppatch(ApplicationBase):
permissions_filter = sharing['Permissions'] permissions_filter = sharing['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 logger.debug("TRACE/PROPPATCH/xml_proppatch: no write-access: %r", path)
if sharing:
# no write access -> use properties overlay
if self._sharing.permit_properties_overlay:
if permissions_filter is not None and "P" in permissions_filter:
logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted, but denied by permission 'P'", path_orig)
return httputils.NOT_ALLOWED
else:
logger.info("PROPPATCH request on shared %r: no write-permissions, overlay permitted by option", path_orig)
sharing_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
else:
logger.info("PROPPATCH request on shared %r: no write-permissions and overlay denied by option", path_orig)
return httputils.NOT_ALLOWED
else:
return httputils.NOT_ALLOWED
else:
logger.debug("TRACE/PROPPATCH/xml_proppatch: write-access: %r", path)
if sharing:
# 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
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
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:
@@ -100,6 +155,38 @@ class ApplicationPartProppatch(ApplicationBase):
except socket.timeout: except socket.timeout:
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:
# 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)
if xml_content is not None:
content = DefusedET.tostring(
xml_content,
encoding=self._encoding
).decode(encoding=self._encoding)
except ValueError as e:
# return better matching HTTP result in case errno is provided and catched
errno_match = re.search("\\[Errno ([0-9]+)\\]", str(e))
if errno_match:
logger.error(
"Failed PROPPATCH request on %r: %s", path, e, exc_info=True)
errno_e = int(errno_match.group(1))
if errno_e == errno.ENOSPC:
return httputils.INSUFFICIENT_STORAGE
elif errno_e in [errno.EPERM, errno.EACCES]:
return httputils.FORBIDDEN
else:
return httputils.INTERNAL_SERVER_ERROR
else:
logger.warning(
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
return httputils.BAD_REQUEST
return client.MULTI_STATUS, headers, self._xml_response(xml_answer), xmlutils.pretty_xml(xml_content)
with self._storage.acquire_lock("w", user, path=path, request="PROPPATCH"): with self._storage.acquire_lock("w", user, path=path, request="PROPPATCH"):
item = next(iter(self._storage.discover(path)), None) item = next(iter(self._storage.discover(path)), None)
if not item: if not item:

View File

@@ -491,6 +491,14 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"value": "false", "value": "false",
"help": "permit create of map-based sharing", "help": "permit create of map-based sharing",
"type": bool}), "type": bool}),
("permit_properties_overlay", {
"value": "false",
"help": "permit properties overlay",
"type": bool}),
("enforce_properties_overlay", {
"value": "true",
"help": "enforce properties overlay on write access",
"type": bool}),
("default_permissions_create_token", { ("default_permissions_create_token", {
"value": "r", "value": "r",
"help": "default permissions for token-based sharing", "help": "default permissions for token-based sharing",

View File

@@ -33,9 +33,15 @@ Permissions:
- o: deny overwriting a collection in case permit_overwrite_collection=True (>= 3.3.0) - o: deny overwriting a collection in case permit_overwrite_collection=True (>= 3.3.0)
- T: permit create of token-based sharing of collection in case permit_create_token=False (>= 3.7.0) - T: permit create of token-based sharing of collection in case permit_create_token=False (>= 3.7.0)
- t: deny create of token-based sharing of collection in case permit_create_token=True (>= 3.7.0) - t: deny create of token-based sharing of collection in case permit_create_token=True (>= 3.7.0)
- M: permit create of map-based sharing of collection in case permit_create_map= False (>= 3.7.0) - M: permit create of map-based sharing of collection in case permit_create_map=False (>= 3.7.0)
- m: deny create of map-based sharing of collection in case permit_create_map=True (>= 3.7.0) - m: deny create of map-based sharing of collection in case permit_create_map=True (>= 3.7.0)
Permissions only supported so far in share permissions:
- P: permit properties overlay in case permit_properties_overlay=False (>= 3.7.0)
- p: deny properties overlay in case permit_properties_overlay=True (>= 3.7.0)
- E: enable enforce properties overlay in case enforce_properties_overlay=False (>= 3.7.0)
- e: disable enforce of properties overlay in case enforce_properties_overlay=True (>= 3.7.0)
Take a look at the class ``BaseRights`` if you want to implement your own. Take a look at the class ``BaseRights`` if you want to implement your own.
""" """
@@ -47,7 +53,7 @@ from radicale import config, utils
INTERNAL_TYPES: Sequence[str] = ("authenticated", "owner_write", "owner_only", INTERNAL_TYPES: Sequence[str] = ("authenticated", "owner_write", "owner_only",
"from_file") "from_file")
INTERNAL_PERMISSIONS: str = "RriWwDdOoTtMm" INTERNAL_PERMISSIONS: str = "RriWwDdOoTtMmPpEe"
def load(configuration: "config.Configuration") -> "BaseRights": def load(configuration: "config.Configuration") -> "BaseRights":

View File

@@ -108,12 +108,16 @@ class BaseSharing:
self.permit_create_map = configuration.get("sharing", "permit_create_map") self.permit_create_map = configuration.get("sharing", "permit_create_map")
self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token") self.default_permissions_create_token = configuration.get("sharing", "default_permissions_create_token")
self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map") self.default_permissions_create_map = configuration.get("sharing", "default_permissions_create_map")
self.permit_properties_overlay = configuration.get("sharing", "permit_properties_overlay")
self.enforce_properties_overlay = configuration.get("sharing", "enforce_properties_overlay")
logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map) logger.info("sharing.collection_by_map : %s", self.sharing_collection_by_map)
logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token) logger.info("sharing.collection_by_token: %s", self.sharing_collection_by_token)
logger.info("sharing.permit_create_token: %s", self.permit_create_token) logger.info("sharing.permit_create_token: %s", self.permit_create_token)
logger.info("sharing.permit_create_map : %s", self.permit_create_map) logger.info("sharing.permit_create_map : %s", self.permit_create_map)
logger.info("sharing.default_permissions_create_token: %r", self.default_permissions_create_token) logger.info("sharing.default_permissions_create_token: %r", self.default_permissions_create_token)
logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map) logger.info("sharing.default_permissions_create_map : %r", self.default_permissions_create_map)
logger.info("sharing.permit_properties_overlay: %s", self.permit_properties_overlay)
logger.info("sharing.enforce_properties_overlay: %s", self.enforce_properties_overlay)
if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False)): if ((self.sharing_collection_by_map is False) and (self.sharing_collection_by_token is False)):
logger.info("sharing disabled as no feature is enabled") logger.info("sharing disabled as no feature is enabled")

View File

@@ -2495,7 +2495,7 @@ permissions: RrWw""")
assert answer_dict['Lines'] == 1 assert answer_dict['Lines'] == 1
assert answer_dict['Content'][0]['Permissions'] == "RrWw" assert answer_dict['Content'][0]['Permissions'] == "RrWw"
def test_sharing_api_map_propfind_overlay(self) -> None: def test_sharing_api_map_propfind_overlay_api(self) -> None:
"""share-by-map API usage tests related to proppatch.""" """share-by-map API usage tests related to proppatch."""
self.configure({"auth": {"type": "htpasswd", self.configure({"auth": {"type": "htpasswd",
"htpasswd_filename": self.htpasswd_file_path, "htpasswd_filename": self.htpasswd_file_path,
@@ -2690,3 +2690,386 @@ permissions: RrWw""")
form_array.append("PathOrToken=" + path_shared_r) form_array.append("PathOrToken=" + path_shared_r)
form_array.append("Properties=BUGGYENTRY=BUGGYVALUE") form_array.append("Properties=BUGGYENTRY=BUGGYVALUE")
_, headers, answer = self._sharing_api_form("map", "update", check=400, login="user:userpw", form_array=form_array) _, headers, answer = self._sharing_api_form("map", "update", check=400, login="user:userpw", form_array=form_array)
def test_sharing_api_map_propfind_overlay_proppatch(self) -> None:
"""share-by-map API usage tests related to proppatch."""
self.configure({"auth": {"type": "htpasswd",
"htpasswd_filename": self.htpasswd_file_path,
"htpasswd_encryption": "plain"},
"sharing": {
"type": "csv",
"permit_create_map": True,
"permit_create_token": True,
"collection_by_map": "True",
"collection_by_token": "True"},
"logging": {"request_header_on_debug": "False",
"response_content_on_debug": "True",
"request_content_on_debug": "True"},
"rights": {"type": "owner_only"}})
json_dict: dict
logging.info("\n*** prepare and test access")
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
logging.info("\n*** test: %s", db_type)
self.configure({"sharing": {"type": db_type}})
path_mapped = "/owner/calendarPFP-" + db_type + ".ics/"
path_shared_r = "/user/calendarPFP-shared-by-owner-r-" + db_type + ".ics/"
self.mkcalendar(path_mapped, login="owner:ownerpw")
# check PROPFIND as owner
logging.info("\n*** PROPFIND collection owner -> ok")
_, responses = self.propfind(path_mapped, """\
<?xml version="1.0" encoding="utf-8"?>
<propfind xmlns="DAV:">
<prop>
<current-user-principal />
</prop>
</propfind>""", login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["D:current-user-principal"]
assert status == 200 and len(prop) == 1
element = prop.find(xmlutils.make_clark("D:href"))
assert element is not None and element.text == "/owner/"
# execute PROPPATCH as owner
logging.info("\n*** PROPPATCH collection owner -> ok")
_, responses = self.proppatch(path_mapped, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/">#AAAAAA</I:calendar-color>
<C:calendar-description xmlns:C="urn:ietf:params:xml:ns:caldav">ICAL-OWNER</C:calendar-description>
</D:prop>
</D:set>
</D:propertyupdate>""", login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
# verify PROPPATCH by owner
logging.info("\n*** PROPFIND collection owner -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["C:calendar-description"]
logging.debug("calendar-description: %r", prop.text)
assert status == 200 and prop.text == "ICAL-OWNER"
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#AAAAAA"
# create map
logging.info("\n*** create map user/owner:r -> ok")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Permissions'] = "rp"
json_dict['Enabled'] = "True"
json_dict['Hidden'] = "False"
_, headers, answer = self._sharing_api_json("map", "create", check=200, login="owner:ownerpw", json_dict=json_dict)
answer_dict = json.loads(answer)
assert answer_dict['Status'] == "success"
# enable map by user
logging.info("\n*** enable map by user")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
_, headers, answer = self._sharing_api_json("map", "enable", check=200, login="user:userpw", json_dict=json_dict)
# verify PROPPATCH as user
logging.info("\n*** PROPFIND collection user -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["C:calendar-description"]
logging.debug("calendar-description: %r", prop.text)
assert status == 200 and prop.text == "ICAL-OWNER"
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#AAAAAA"
# execute PROPPATCH as user
logging.info("\n*** PROPPATCH collection user -> ok")
_, responses = self.proppatch(path_shared_r, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/">#BBBBBB</I:calendar-color>
<C:calendar-description xmlns:C="urn:ietf:params:xml:ns:caldav">ICAL-USER</C:calendar-description>
</D:prop>
</D:set>
</D:propertyupdate>""", login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int) and len(response) == 2
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
status, prop = response["C:calendar-description"]
assert status == 200 and not prop.text
logging.info("\n*** list (json->json)")
json_dict['PathOrToken'] = path_shared_r
_, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict)
answer_dict = json.loads(answer)
assert answer_dict['Status'] == "success"
assert answer_dict['Lines'] == 1
logging.info("\n*** list (json->csv)")
json_dict['PathOrToken'] = path_shared_r
_, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/csv")
logging.info("\n*** list (json->txt)")
json_dict['PathOrToken'] = path_shared_r
_, headers, answer = self._sharing_api_json("map", "list", check=200, login="owner:ownerpw", json_dict=json_dict, accept="text/plain")
# verify overlay as user
logging.info("\n*** PROPFIND collection user (overlay) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_shared_r, propfind_calendar_color, login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int)
status, prop = response["C:calendar-description"]
logging.debug("calendar-description: %r", prop.text)
assert status == 200 and prop.text == "ICAL-USER"
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#BBBBBB"
# verify overlay not visible by owner
logging.info("\n*** PROPFIND collection owner -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["C:calendar-description"]
logging.debug("calendar-description: %r", prop.text)
assert status == 200 and prop.text == "ICAL-OWNER"
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#AAAAAA"
# execute PROPPATCH as user (delete color)
logging.info("\n*** PROPPATCH collection user (delete color) -> ok")
_, responses = self.proppatch(path_shared_r, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:remove>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/" />
</D:prop>
</D:remove>
</D:propertyupdate>""", login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
# verify overlay as user
logging.info("\n*** PROPFIND collection user (overlay, color back to owner) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_shared_r, propfind_calendar_color, login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int)
status, prop = response["C:calendar-description"]
logging.debug("calendar-description: %r", prop.text)
assert status == 200 and prop.text == "ICAL-USER"
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#AAAAAA"
# update map by owner
logging.info("\n*** update map by owner (disable property overlay)")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Permissions'] = "rwe"
json_dict['User'] = "user"
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
# execute PROPPATCH as user
logging.info("\n*** PROPPATCH collection user (set color) -> ok")
_, responses = self.proppatch(path_shared_r, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/">#DDDDDD</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
# verify overlay as user
logging.info("\n*** PROPFIND collection user (overlay, color) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_shared_r, propfind_calendar_color, login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#DDDDDD"
# verify overlay visible by owner
logging.info("\n*** PROPFIND collection owner (visible enforced change) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#DDDDDD"
# update map by owner
logging.info("\n*** update map by owner (enable property overlay)")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Permissions'] = "rwE"
json_dict['User'] = "user"
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
# execute PROPPATCH as user
logging.info("\n*** PROPPATCH collection user (set color rw, enforce overlay enabled by default) -> ok")
_, responses = self.proppatch(path_shared_r, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/">#EEEEEE</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
# verify overlay visible by owner
logging.info("\n*** PROPFIND collection owner (invisible change) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#DDDDDD"
# verify overlay as user
logging.info("\n*** PROPFIND collection user (overlay, color) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_shared_r, propfind_calendar_color, login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#EEEEEE"
# update map by owner
logging.info("\n*** update map by owner (enable property overlay)")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Permissions'] = "rwe"
json_dict['User'] = "user"
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
# execute PROPPATCH as user
logging.info("\n*** PROPPATCH collection user (set color rwe, enforce overlay enabled by default) -> ok")
_, responses = self.proppatch(path_shared_r, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/">#EEEE00</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
# verify overlay visible by owner
logging.info("\n*** PROPFIND collection owner (visible change) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#EEEE00"
self.configure({"sharing": {"enforce_properties_overlay": False}})
# update map by owner
logging.info("\n*** update map by owner (enable property overlay)")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Permissions'] = "rw"
json_dict['User'] = "user"
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
logging.info("\n*** PROPPATCH collection user (set color rwe but enforce disabled) -> ok")
_, responses = self.proppatch(path_shared_r, """\
<?xml version="1.0" encoding="utf-8"?>
<D:propertyupdate xmlns:D="DAV:">
<D:set>
<D:prop>
<I:calendar-color xmlns:I="http://apple.com/ns/ical/">#FFFFFF</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", login="user:userpw")
logging.info("response: %r", responses)
response = responses[path_shared_r]
assert not isinstance(response, int) and len(response) == 1
status, prop = response["ICAL:calendar-color"]
assert status == 200 and not prop.text
# verify visible by owner
logging.info("\n*** PROPFIND collection owner (visible change) -> ok")
propfind_calendar_color = get_file_content("propfind_multiple.xml")
_, responses = self.propfind(path_mapped, propfind_calendar_color, login="owner:ownerpw")
logging.info("response: %r", responses)
response = responses[path_mapped]
assert not isinstance(response, int)
status, prop = response["ICAL:calendar-color"]
logging.debug("calendar-color: %r", prop.text)
assert status == 200 and prop.text == "#FFFFFF"