Merge pull request #2076 from pbiering/sharing-token-permission-default-rp

Sharing token permission default rp
This commit is contained in:
Peter Bieringer
2026-04-10 06:37:01 +02:00
committed by GitHub
7 changed files with 179 additions and 25 deletions

View File

@@ -4,6 +4,8 @@
* Fix: share address book collection as birthday calendar not working on non-DEBUG level * Fix: share address book collection as birthday calendar not working on non-DEBUG level
* Extension: share accept now PATH and USER for matching email addresses as well * Extension: share accept now PATH and USER for matching email addresses as well
* Adjustment: replace logging/trace_on_debug by new log level "trace" * Adjustment: replace logging/trace_on_debug by new log level "trace"
* Adjustment: sharing/token: adjust default permissions to "rp"
* Fix: sharing/propfind+proppatch: permission check related to properties
## 3.7.0 ## 3.7.0

View File

@@ -310,11 +310,11 @@ Create a share by mapping a collection of an `Owner` to a token.
| Parameter | Type | Requirement | | Parameter | Type | Requirement |
| - | - | - | | - | - | - |
| PathMapped | str | mandatory | | PathMapped | str | mandatory |
| Conversion | str | optional(default:none) | | Conversion | str | optional (default:none) |
| User | str | optional(default:owner) | | User | str | optional (default:owner) |
| Permissions | str | optional(default:r) | | Permissions | str | optional (default:rp) |
| Enabled | bool | optional(owner/default:False) | | Enabled | bool | optional (owner/default:False) |
| Hidden | bool | optional(owner/default:True) | | Hidden | bool | optional (owner/default:True) |
| Properties | str | optional | | Properties | str | optional |
* Output: text/plain|application/json * Output: text/plain|application/json
@@ -359,11 +359,11 @@ Create a share by mapping a collection of an `Owner` to an `User`.
| - | - | - | | - | - | - |
| PathOrToken | str | mandatory | | PathOrToken | str | mandatory |
| PathMapped | str | mandatory | | PathMapped | str | mandatory |
| Conversion | str | optional(default:none) | | Conversion | str | optional (default:none) |
| User | str | mandatory | | User | str | mandatory |
| Permissions | str | optional(default:r) | | Permissions | str | optional (default:r) |
| Enabled | bool | optional(owner/default:False) | | Enabled | bool | optional (owner/default:False) |
| Hidden | bool | optional(owner/default:True) | | Hidden | bool | optional (owner/default:True) |
| Properties | optional | | Properties | optional |
* Output: text/plain|application/json * Output: text/plain|application/json

View File

@@ -313,7 +313,7 @@ def xml_propfind_response(
) and not ( ) and not (
"p" in share['Permissions'] or "p" in share['Permissions'] or
("p" in raw_permissions and "P" not in share['Permissions']) or ("p" in raw_permissions and "P" not in share['Permissions']) or
(self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])): (not self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])):
logger.trace("PROPFIND/xml_propfind_response/current-user-privilege-set: add D:write-properties") logger.trace("PROPFIND/xml_propfind_response/current-user-privilege-set: add D:write-properties")
privileges.append("D:write-properties") privileges.append("D:write-properties")
elif write: elif write:

View File

@@ -124,7 +124,7 @@ class ApplicationPartProppatch(ApplicationBase):
) and not ( ) and not (
"p" in share['Permissions'] or "p" in share['Permissions'] or
("p" in raw_permissions and "P" not in share['Permissions']) or ("p" in raw_permissions and "P" not in share['Permissions']) or
(self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])): (not self._sharing.permit_properties_overlay and "P" not in raw_permissions and "P" not in share['Permissions'])):
logger.info("PROPPATCH request on shared %r: write-access", path_orig) logger.info("PROPPATCH request on shared %r: write-access", path_orig)
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-access, overlay enforced, but disabled by share permission 'e'", path_orig) logger.info("PROPPATCH request on shared %r: write-access, overlay enforced, but disabled by share permission 'e'", path_orig)

View File

@@ -509,7 +509,7 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"help": "enforce properties overlay on write access", "help": "enforce properties overlay on write access",
"type": bool}), "type": bool}),
("default_permissions_create_token", { ("default_permissions_create_token", {
"value": "r", "value": "rp",
"help": "default permissions for token-based sharing", "help": "default permissions for token-based sharing",
"type": rights_permission}), "type": rights_permission}),
("default_permissions_create_map", { ("default_permissions_create_map", {

View File

@@ -880,9 +880,9 @@ class BaseSharing:
else: else:
Permissions = str(Permissions) Permissions = str(Permissions)
if Conversion == "bday": if Conversion == "bday":
# bday is read-only # bday is read-only and not supporting "Ee"
for permission in Permissions: for permission in Permissions:
if permission not in "rPpEe": if permission not in "rPp":
logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion) logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion)
return httputils.METHOD_NOT_ALLOWED return httputils.METHOD_NOT_ALLOWED
@@ -1081,6 +1081,15 @@ class BaseSharing:
logger.trace("" + api_info + ": clear property %r", prop) logger.trace("" + api_info + ": clear property %r", prop)
del Properties[prop] del Properties[prop]
if Permissions is not None and share['Conversion'] is not None:
Permissions = str(Permissions)
if share['Conversion'] == "bday":
# bday is read-only and not supporting "Ee"
for permission in Permissions:
if permission not in "rPp":
logger.warning(api_info + ": PathMapped=%r Permissions=%r not supported for Conversion=%r", PathMapped, Permissions, Conversion)
return httputils.METHOD_NOT_ALLOWED
if user == share['Owner']: if user == share['Owner']:
if PathMapped is not None: if PathMapped is not None:
# check access Permissions # check access Permissions

View File

@@ -5134,6 +5134,7 @@ permissions: RrWw""")
for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)): for db_type in list(filter(lambda item: item != "none", sharing.INTERNAL_TYPES)):
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}})
self.configure({"sharing": {"permit_properties_overlay": "True"}})
path_mapped = "/owner/adressbook-" + db_type + ".vcf/" path_mapped = "/owner/adressbook-" + db_type + ".vcf/"
self.create_addressbook(path_mapped, login="owner:ownerpw") self.create_addressbook(path_mapped, login="owner:ownerpw")
@@ -5170,7 +5171,7 @@ permissions: RrWw""")
assert "NICKNAME-C3" in answer assert "NICKNAME-C3" in answer
# create map # create map
logging.info("\n*** create token with bday conversion -> ok") logging.info("\n*** create token with bday conversion (default permissions) -> ok")
json_dict = {} json_dict = {}
json_dict['User'] = "owner" json_dict['User'] = "owner"
json_dict['PathMapped'] = path_mapped json_dict['PathMapped'] = path_mapped
@@ -5191,15 +5192,6 @@ permissions: RrWw""")
assert "Test-FN-C3 (BDAY)" in answer assert "Test-FN-C3 (BDAY)" in answer
assert "Test-FN (BDAY)" in answer assert "Test-FN (BDAY)" in answer
# check PROPFIND item with token
logging.info("\n*** PROPFIND item with token -> calendar")
response = self._propfind_allprop(path_shared)
logging.debug("response: %r", response)
assert "CR:supported-address-data" not in response
assert "D:sync-token" not in response
assert "C:supported-calendar-component-set" in response
assert "D:current-user-privilege-set" in response
# verify content as owner # verify content as owner
logging.info("\n*** GET collection owner -> ok") logging.info("\n*** GET collection owner -> ok")
_, headers, answer = self.request("GET", path_shared) _, headers, answer = self.request("GET", path_shared)
@@ -5209,7 +5201,7 @@ permissions: RrWw""")
assert 'Content-Disposition' in headers assert 'Content-Disposition' in headers
assert 'Calendar.ics' in headers['Content-Disposition'] assert 'Calendar.ics' in headers['Content-Disposition']
# create map # create map of ics with conversion -> fail
logging.info("\n*** create token with bday conversion but unsupported permissions -> fail") logging.info("\n*** create token with bday conversion but unsupported permissions -> fail")
json_dict = {} json_dict = {}
json_dict['User'] = "owner" json_dict['User'] = "owner"
@@ -5220,6 +5212,157 @@ permissions: RrWw""")
json_dict['Conversion'] = "bday" json_dict['Conversion'] = "bday"
_, headers, answer = self._sharing_api_json("token", "create", check=405, login="owner:ownerpw", json_dict=json_dict) _, headers, answer = self._sharing_api_json("token", "create", check=405, login="owner:ownerpw", json_dict=json_dict)
# check PROPFIND item with token
logging.info("\n*** PROPFIND item with token -> calendar")
response = self._propfind_allprop(path_shared)
logging.debug("response: %r", response)
assert "CR:supported-address-data" not in response
assert "D:sync-token" not in response
assert "C:supported-calendar-component-set" in response
assert "D:current-user-privilege-set" in response
status, props = response["D:current-user-privilege-set"]
privileges = props.findall(xmlutils.make_clark("D:privilege"))
assert len(privileges) >= 1
privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges]
assert "D:read" in privileges_list
assert "D:write-content" not in privileges_list
assert "D:write-properties" not in privileges_list
assert "D:write" not in privileges_list
assert "D:all" not in privileges_list
# execute PROPPATCH color as user
logging.info("\n*** PROPPATCH color collection with token -> permission denied")
color = "#BBBBBB"
_, responses = self.proppatch(path=path_shared, data="""\
<?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/">""" + color + """</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", check=403)
# update map
logging.info("\n*** update token with bday conversion ("r" permissions) -> ok")
json_dict = {}
json_dict['User'] = "owner"
json_dict['PathOrToken'] = path_shared
json_dict['Permissions'] = "r"
_, headers, answer = self._sharing_api_json("token", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
logging.info("\n*** PROPFIND item with token -> calendar")
response = self._propfind_allprop(path_shared)
logging.debug("response: %r", response)
status, props = response["D:current-user-privilege-set"]
privileges = props.findall(xmlutils.make_clark("D:privilege"))
assert len(privileges) >= 1
privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges]
assert "D:read" in privileges_list
assert "D:write-content" not in privileges_list
assert "D:write-properties" in privileges_list
assert "D:write" not in privileges_list
assert "D:all" not in privileges_list
# execute PROPPATCH color as user
logging.info("\n*** PROPPATCH color collection with token -> ok")
color = "#BBBBBB"
_, responses = self.proppatch(path=path_shared, data="""\
<?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/">""" + color + """</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", check=207)
self.configure({"sharing": {"permit_properties_overlay": "False"}})
logging.info("\n*** PROPFIND item with token (r) -> calendar")
response = self._propfind_allprop(path_shared)
logging.debug("response: %r", response)
status, props = response["D:current-user-privilege-set"]
privileges = props.findall(xmlutils.make_clark("D:privilege"))
assert len(privileges) >= 1
privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges]
assert "D:read" in privileges_list
assert "D:write-content" not in privileges_list
assert "D:write-properties" not in privileges_list
assert "D:write" not in privileges_list
assert "D:all" not in privileges_list
# execute PROPPATCH color as user
logging.info("\n*** PROPPATCH color collection with token -> ok")
color = "#BBBBBB"
_, responses = self.proppatch(path=path_shared, data="""\
<?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/">""" + color + """</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", check=403)
# update map to "rP"
logging.info("\n*** update token with bday conversion ('rP' permissions) -> ok")
json_dict = {}
json_dict['User'] = "owner"
json_dict['PathOrToken'] = path_shared
json_dict['Permissions'] = "rP"
_, headers, answer = self._sharing_api_json("token", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
logging.info("\n*** PROPFIND item with token -> calendar")
response = self._propfind_allprop(path_shared)
logging.debug("response: %r", response)
status, props = response["D:current-user-privilege-set"]
privileges = props.findall(xmlutils.make_clark("D:privilege"))
assert len(privileges) >= 1
privileges_list = [xmlutils.make_human_tag(privilege.findall("*")[0].tag) for privilege in privileges]
assert "D:read" in privileges_list
assert "D:write-content" not in privileges_list
assert "D:write-properties" in privileges_list
assert "D:write" not in privileges_list
assert "D:all" not in privileges_list
# execute PROPPATCH color as user
logging.info("\n*** PROPPATCH color collection with token -> ok")
color = "#BBBBBB"
_, responses = self.proppatch(path=path_shared, data="""\
<?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/">""" + color + """</I:calendar-color>
</D:prop>
</D:set>
</D:propertyupdate>""", check=207)
# update map to "rPe"
logging.info("\n*** update token with bday conversion ('rPe' permissions) -> not supported")
json_dict = {}
json_dict['User'] = "owner"
json_dict['PathOrToken'] = path_shared
json_dict['Permissions'] = "rPe"
_, headers, answer = self._sharing_api_json("token", "update", check=405, login="owner:ownerpw", json_dict=json_dict)
# update map to "rPE"
logging.info("\n*** update token with bday conversion ('rPE' permissions) -> not supported")
json_dict = {}
json_dict['User'] = "owner"
json_dict['PathOrToken'] = path_shared
json_dict['Permissions'] = "rPE"
_, headers, answer = self._sharing_api_json("token", "update", check=405, login="owner:ownerpw", json_dict=json_dict)
# update map Conversion
logging.info("\n*** update token remove Conversion -> not supported")
json_dict = {}
json_dict['User'] = "owner"
json_dict['PathOrToken'] = path_shared
json_dict['Conversion'] = ""
_, headers, answer = self._sharing_api_json("token", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
def test_sharing_api_token_ics_bday(self) -> None: def test_sharing_api_token_ics_bday(self) -> None:
"""share-by-token ics with bday conversion (has to fail).""" """share-by-token ics with bday conversion (has to fail)."""
self.configure({"auth": {"type": "htpasswd", self.configure({"auth": {"type": "htpasswd",