Skip to content

Commit

Permalink
Merge pull request #1584 from pbiering/change-default-permit_delete_c…
Browse files Browse the repository at this point in the history
…ollection

permit_delete_collection per collection control
  • Loading branch information
pbiering authored Sep 30, 2024
2 parents fce3f0b + 77749cb commit bfe0ccc
Show file tree
Hide file tree
Showing 5 changed files with 62 additions and 11 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,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'

## 3.2.3
* Add: support for Python 3.13
Expand Down
5 changes: 5 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -913,6 +913,9 @@ File for the rights backend `from_file`. See the

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

##### type
Expand Down Expand Up @@ -1295,6 +1298,8 @@ The following `permissions` are recognized:
(CalDAV/CardDAV is susceptible to expensive search requests)
* **W:** write collections (excluding address books and calendars)
* **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

Expand Down
2 changes: 1 addition & 1 deletion radicale/app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ def parent_permissions(self) -> str:

def check(self, permission: str,
item: Optional[types.CollectionOrItem] = None) -> bool:
if permission not in "rw":
if permission not in "rwdD":
raise ValueError("Invalid permission argument: %r" % permission)
if not item:
permissions = permission + permission.upper()
Expand Down
27 changes: 17 additions & 10 deletions radicale/app/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
# Copyright © 2017-2018 Unrud <[email protected]>
# Copyright © 2024-2024 Peter Bieringer <[email protected]>
#
# 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
Expand All @@ -24,6 +25,7 @@
from radicale import httputils, storage, types, xmlutils
from radicale.app.base import Access, ApplicationBase
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
from radicale.log import logger


def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection,
Expand Down Expand Up @@ -71,17 +73,22 @@ def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str,
hook_notification_item_list = []
if isinstance(item, storage.BaseCollection):
if self._permit_delete_collection:
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)
if access.check("d", item):
logger.info("delete of collection is permitted by config/option [rights] permit_delete_collection but explicit forbidden by permission 'd': %s", path)
return httputils.NOT_ALLOWED
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:
assert item.collection is not None
assert item.href is not None
Expand Down
38 changes: 38 additions & 0 deletions radicale/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,19 @@ 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]
user: .*
collection: test-permit-delete
permissions: RrWwD
[forbid delete collection]
user: .*
collection: test-forbid-delete
permissions: RrWwd
[allow all]
user: .*
collection: .*
Expand Down Expand Up @@ -439,6 +450,33 @@ 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)."""
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:
"""Delete the root collection."""
self.mkcalendar("/calendar.ics/")
Expand Down

0 comments on commit bfe0ccc

Please sign in to comment.