1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-08-10 18:40:53 +00:00

Merge pull request #1584 from pbiering/change-default-permit_delete_collection

permit_delete_collection per collection control
This commit is contained in:
Peter Bieringer 2024-09-30 21:25:58 +02:00 committed by GitHub
commit bfe0ccc463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 62 additions and 11 deletions

View file

@ -4,6 +4,7 @@
* Adjustment: option [auth] htpasswd_encryption change default from "md5" to "autodetect" * Adjustment: option [auth] htpasswd_encryption change default from "md5" to "autodetect"
* Add: option [auth] type=ldap with (group) rights management via LDAP/LDAPS * 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'
## 3.2.3 ## 3.2.3
* Add: support for Python 3.13 * Add: support for Python 3.13

View file

@ -913,6 +913,9 @@ File for the rights backend `from_file`. See the
Global control of permission to delete complete collection (default: True) 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
#### storage #### storage
##### type ##### type
@ -1295,6 +1298,8 @@ The following `permissions` are recognized:
(CalDAV/CardDAV is susceptible to expensive search requests) (CalDAV/CardDAV is susceptible to expensive search requests)
* **W:** write collections (excluding address books and calendars) * **W:** write collections (excluding address books and calendars)
* **w:** write address book and calendar collections * **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
### Storage ### Storage

View file

@ -125,7 +125,7 @@ class Access:
def check(self, permission: str, def check(self, permission: str,
item: Optional[types.CollectionOrItem] = None) -> bool: item: Optional[types.CollectionOrItem] = None) -> bool:
if permission not in "rw": if permission not in "rwdD":
raise ValueError("Invalid permission argument: %r" % permission) raise ValueError("Invalid permission argument: %r" % permission)
if not item: if not item:
permissions = permission + permission.upper() permissions = permission + permission.upper()

View file

@ -3,6 +3,7 @@
# Copyright © 2008 Pascal Halter # Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub # Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <unrud@outlook.com> # Copyright © 2017-2018 Unrud <unrud@outlook.com>
# Copyright © 2024-2024 Peter Bieringer <pb@bieringer.de>
# #
# This library is free software: you can redistribute it and/or modify # This library is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -24,6 +25,7 @@ from typing import Optional
from radicale import httputils, storage, types, xmlutils from radicale import httputils, 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
def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection, def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection,
@ -71,17 +73,22 @@ class ApplicationPartDelete(ApplicationBase):
hook_notification_item_list = [] hook_notification_item_list = []
if isinstance(item, storage.BaseCollection): if isinstance(item, storage.BaseCollection):
if self._permit_delete_collection: if self._permit_delete_collection:
for i in item.get_all(): if access.check("d", item):
hook_notification_item_list.append( logger.info("delete of collection is permitted by config/option [rights] permit_delete_collection but explicit forbidden by permission 'd': %s", path)
HookNotificationItem( return httputils.NOT_ALLOWED
HookNotificationItemTypes.DELETE,
access.path,
i.uid
)
)
xml_answer = xml_delete(base_prefix, path, item)
else: else:
return httputils.NOT_ALLOWED if not access.check("D", item):
logger.info("delete of collection is prevented by config/option [rights] permit_delete_collection and not explicit allowed by permission 'D': %s", path)
return httputils.NOT_ALLOWED
for i in item.get_all():
hook_notification_item_list.append(
HookNotificationItem(
HookNotificationItemTypes.DELETE,
access.path,
i.uid
)
)
xml_answer = xml_delete(base_prefix, path, item)
else: else:
assert item.collection is not None assert item.collection is not None
assert item.href is not None assert item.href is not None

View file

@ -41,8 +41,19 @@ class TestBaseRequests(BaseTest):
def setup_method(self) -> None: def setup_method(self) -> None:
BaseTest.setup_method(self) BaseTest.setup_method(self)
rights_file_path = os.path.join(self.colpath, "rights") rights_file_path = os.path.join(self.colpath, "rights")
self.configure({"rights": {"permit_delete_collection": True}})
with open(rights_file_path, "w") as f: with open(rights_file_path, "w") as f:
f.write("""\ f.write("""\
[permit delete collection]
user: .*
collection: test-permit-delete
permissions: RrWwD
[forbid delete collection]
user: .*
collection: test-forbid-delete
permissions: RrWwd
[allow all] [allow all]
user: .* user: .*
collection: .* collection: .*
@ -439,6 +450,33 @@ permissions: RrWw""")
assert responses["/calendar.ics/"] == 200 assert responses["/calendar.ics/"] == 200
self.get("/calendar.ics/", check=404) self.get("/calendar.ics/", check=404)
def test_delete_collection_not_permitted(self) -> None:
"""Delete a collection (try if not permitted)."""
self.configure({"rights": {"permit_delete_collection": False}})
self.mkcalendar("/calendar.ics/")
event = get_file_content("event1.ics")
self.put("/calendar.ics/event1.ics", event)
_, responses = self.delete("/calendar.ics/", check=401)
self.get("/calendar.ics/", check=200)
def test_delete_collection_global_forbid_explicit_permit(self) -> None:
"""Delete a collection with permitted path (expect permit)."""
self.configure({"rights": {"permit_delete_collection": False}})
self.mkcalendar("/test-permit-delete/")
event = get_file_content("event1.ics")
self.put("/test-permit-delete/event1.ics", event)
_, responses = self.delete("/test-permit-delete/", check=200)
self.get("/test-permit-delete/", check=404)
def test_delete_collection_global_permit_explicit_forbid(self) -> None:
"""Delete a collection with permitted path (expect forbid)."""
self.configure({"rights": {"permit_delete_collection": True}})
self.mkcalendar("/test-forbid-delete/")
event = get_file_content("event1.ics")
self.put("/test-forbid-delete/event1.ics", event)
_, responses = self.delete("/test-forbid-delete/", check=401)
self.get("/test-forbid-delete/", check=200)
def test_delete_root_collection(self) -> None: def test_delete_root_collection(self) -> None:
"""Delete the root collection.""" """Delete the root collection."""
self.mkcalendar("/calendar.ics/") self.mkcalendar("/calendar.ics/")