From 973b26b2e9636e4d2d6c1fe1666c8f77db730a2e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Sep 2024 18:15:42 +0200 Subject: [PATCH 1/9] add new option rights/permit_overwrite_collection --- config | 2 ++ radicale/app/__init__.py | 3 +++ radicale/app/base.py | 1 + radicale/config.py | 4 ++++ 4 files changed, 10 insertions(+) diff --git a/config b/config index 5fb11290..3704f65b 100644 --- a/config +++ b/config @@ -115,6 +115,8 @@ # Permit delete of a collection (global) #permit_delete_collection = True +# Permit overwrite of a collection (global) +#permit_overwrite_collection = False [storage] diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index ee958ad4..4f11ad3f 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -70,6 +70,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead, _auth_realm: str _extra_headers: Mapping[str, str] _permit_delete_collection: bool + _permit_overwrite_collection: bool def __init__(self, configuration: config.Configuration) -> None: """Initialize Application. @@ -91,6 +92,8 @@ def __init__(self, configuration: config.Configuration) -> None: self._auth_realm = configuration.get("auth", "realm") self._permit_delete_collection = configuration.get("rights", "permit_delete_collection") logger.info("permit delete of collection: %s", self._permit_delete_collection) + self._permit_overwrite_collection = configuration.get("rights", "permit_overwrite_collection") + logger.info("permit overwrite of collection: %s", self._permit_overwrite_collection) self._extra_headers = dict() for key in self.configuration.options("headers"): self._extra_headers[key] = configuration.get("headers", key) diff --git a/radicale/app/base.py b/radicale/app/base.py index 71fd8073..2132111d 100644 --- a/radicale/app/base.py +++ b/radicale/app/base.py @@ -40,6 +40,7 @@ class ApplicationBase: _web: web.BaseWeb _encoding: str _permit_delete_collection: bool + _permit_overwrite_collection: bool _hook: hook.BaseHook def __init__(self, configuration: config.Configuration) -> None: diff --git a/radicale/config.py b/radicale/config.py index 5bddaf91..27c48760 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -245,6 +245,10 @@ def json_str(value: Any) -> dict: "value": "True", "help": "permit delete of a collection", "type": bool}), + ("permit_overwrite_collection", { + "value": "False", + "help": "permit overwrite of a collection", + "type": bool}), ("file", { "value": "/etc/radicale/rights", "help": "file for rights management from_file", From d41aa60d6156b759cb5c53f1586991e345a78954 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Sun, 29 Sep 2024 19:55:16 +0200 Subject: [PATCH 2/9] cosmetics --- radicale/tests/test_base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index b6046c1a..afc15f9d 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -450,8 +450,8 @@ def test_delete_collection(self) -> None: assert responses["/calendar.ics/"] == 200 self.get("/calendar.ics/", check=404) - def test_delete_collection_not_permitted(self) -> None: - """Delete a collection (try if not permitted).""" + def test_delete_collection_global_forbid(self) -> None: + """Delete a collection (expect forbidden).""" self.configure({"rights": {"permit_delete_collection": False}}) self.mkcalendar("/calendar.ics/") event = get_file_content("event1.ics") From e0594d5b33800e87b4a2c4b7a387081917a95665 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:13:00 +0200 Subject: [PATCH 3/9] permit_overwrite_collection doc + rights + test cases --- DOCUMENTATION.md | 9 +++++++++ radicale/app/put.py | 8 ++++++++ radicale/tests/test_base.py | 24 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 46a9cd23..42f489e1 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -916,6 +916,15 @@ Global control of permission to delete complete collection (default: True) If False it can be permitted by permissions per section with: D If True it can be forbidden by permissions per section with: d +##### permit_overwrite_collection + +(New since 3.3.0) + +Global control of permission to overwrite complete collection (default: False) + +If False it can be permitted by permissions per section with: O +If True it can be forbidden by permissions per section with: o + #### storage ##### type diff --git a/radicale/app/put.py b/radicale/app/put.py index 15a7e00d..710e4435 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -177,6 +177,14 @@ def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str, if write_whole_collection: if ("w" if tag else "W") not in access.permissions: return httputils.NOT_ALLOWED + if not self._permit_overwrite_collection: + if ("O") not in access.permissions: + logger.info("overwrite of collection is prevented by config/option [rights] permit_overwrite_collection and not explicit allowed by permssion 'O': %s", path) + return httputils.NOT_ALLOWED + else: + if ("o") in access.permissions: + logger.info("overwrite of collection is allowed by config/option [rights] permit_overwrite_collection but explicit forbidden by permission 'o': %s", path) + return httputils.NOT_ALLOWED elif "w" not in access.parent_permissions: return httputils.NOT_ALLOWED diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index afc15f9d..cfcd2d0c 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -488,6 +488,30 @@ def test_delete_root_collection(self) -> None: self.get("/calendar.ics/", check=404) self.get("/event1.ics", 404) + def test_overwrite_collection_global_forbid(self) -> None: + """Overwrite a collection (expect forbid).""" + self.configure({"rights": {"permit_overwrite_collection": False}}) + event = get_file_content("event1.ics") + self.put("/calender.ics/", event, check=401) + + def test_overwrite_collection_global_forbid_explict_permit(self) -> None: + """Overwrite a collection with permitted path (expect permit).""" + self.configure({"rights": {"permit_overwrite_collection": False}}) + event = get_file_content("event1.ics") + self.put("/test-permit-overwrite/", event, check=201) + + def test_overwrite_collection_global_permit(self) -> None: + """Overwrite a collection (expect permit).""" + self.configure({"rights": {"permit_overwrite_collection": True}}) + event = get_file_content("event1.ics") + self.put("/calender.ics/", event, check=201) + + def test_overwrite_collection_global_permit_explict_forbid(self) -> None: + """Overwrite a collection with forbidden path (expect forbid).""" + self.configure({"rights": {"permit_overwrite_collection": True}}) + event = get_file_content("event1.ics") + self.put("/test-forbid-overwrite/", event, check=401) + def test_propfind(self) -> None: calendar_path = "/calendar.ics/" self.mkcalendar("/calendar.ics/") From 457af284e1bcf4d40be55a5d49da3dec35d925f5 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:28:29 +0200 Subject: [PATCH 4/9] whitelist new permissions --- radicale/app/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/radicale/app/base.py b/radicale/app/base.py index 2132111d..28b6f262 100644 --- a/radicale/app/base.py +++ b/radicale/app/base.py @@ -126,7 +126,7 @@ def parent_permissions(self) -> str: def check(self, permission: str, item: Optional[types.CollectionOrItem] = None) -> bool: - if permission not in "rwdD": + if permission not in "rwdDoO": raise ValueError("Invalid permission argument: %r" % permission) if not item: permissions = permission + permission.upper() From 110ee9d24764aef556b2f0c1e0a98a970db8983d Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:28:42 +0200 Subject: [PATCH 5/9] doucment new permissions --- DOCUMENTATION.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 42f489e1..b0d2a651 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1309,6 +1309,8 @@ The following `permissions` are recognized: * **w:** write address book and calendar collections * **D:** permit delete of collection in case permit_delete_collection=False * **d:** forbid delete of collection in case permit_delete_collection=True +* **O:** permit overwrite of collection in case permit_overwrite_collection=False +* **o:** forbid overwrite of collection in case permit_overwrite_collection=True ### Storage From eed6bcee01c8413b24278b3e79e18dc39cf6bbf8 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:33:29 +0200 Subject: [PATCH 6/9] add collection rights for dedicated test cases --- radicale/tests/test_base.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index cfcd2d0c..c6deeade 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -54,6 +54,16 @@ def setup_method(self) -> None: collection: test-forbid-delete permissions: RrWwd +[permit overwrite collection] +user: .* +collection: test-permit-overwrite +permissions: RrWwO + +[forbid overwrite collection] +user: .* +collection: test-forbid-overwrite +permissions: RrWwo + [allow all] user: .* collection: .* From 0505b7b6031094f56ef7290a7ae1a7d672505882 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:35:11 +0200 Subject: [PATCH 7/9] update changelog permit_overwrite_collection --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1134ffd..9ef5e6b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ * Adjustment: option [auth] htpasswd_encryption change default from "md5" to "autodetect" * Add: option [auth] type=ldap with (group) rights management via LDAP/LDAPS * Enhancement: permit_delete_collection can be now controlled also per collection by rights 'D' or 'd' +* Add: option [rights] permit_overwrite_collection (default=False) which can be also controlled per collection by rights 'O' or 'o' ## 3.2.3 * Add: support for Python 3.13 From ba9776d688b88e4382d2888adcf448c009edab22 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:43:50 +0200 Subject: [PATCH 8/9] change default, remove leftover --- DOCUMENTATION.md | 2 +- config | 2 +- radicale/config.py | 2 +- radicale/tests/test_base.py | 1 - 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index b0d2a651..64aff476 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -920,7 +920,7 @@ If True it can be forbidden by permissions per section with: d (New since 3.3.0) -Global control of permission to overwrite complete collection (default: False) +Global control of permission to overwrite complete collection (default: True) If False it can be permitted by permissions per section with: O If True it can be forbidden by permissions per section with: o diff --git a/config b/config index 3704f65b..67b4a969 100644 --- a/config +++ b/config @@ -116,7 +116,7 @@ #permit_delete_collection = True # Permit overwrite of a collection (global) -#permit_overwrite_collection = False +#permit_overwrite_collection = True [storage] diff --git a/radicale/config.py b/radicale/config.py index 27c48760..3b4f4c7b 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -246,7 +246,7 @@ def json_str(value: Any) -> dict: "help": "permit delete of a collection", "type": bool}), ("permit_overwrite_collection", { - "value": "False", + "value": "True", "help": "permit overwrite of a collection", "type": bool}), ("file", { diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index c6deeade..c47df720 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -41,7 +41,6 @@ class TestBaseRequests(BaseTest): def setup_method(self) -> None: BaseTest.setup_method(self) rights_file_path = os.path.join(self.colpath, "rights") - self.configure({"rights": {"permit_delete_collection": True}}) with open(rights_file_path, "w") as f: f.write("""\ [permit delete collection] From 67362189f5e2775f6274291a9cbd2f758f015458 Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Mon, 30 Sep 2024 21:45:43 +0200 Subject: [PATCH 9/9] fix according to latest default change --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ef5e6b8..e84e88f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ * Adjustment: option [auth] htpasswd_encryption change default from "md5" to "autodetect" * Add: option [auth] type=ldap with (group) rights management via LDAP/LDAPS * Enhancement: permit_delete_collection can be now controlled also per collection by rights 'D' or 'd' -* Add: option [rights] permit_overwrite_collection (default=False) which can be also controlled per collection by rights 'O' or 'o' +* Add: option [rights] permit_overwrite_collection (default=True) which can be also controlled per collection by rights 'O' or 'o' ## 3.2.3 * Add: support for Python 3.13