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:
commit
bfe0ccc463
5 changed files with 62 additions and 11 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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/")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue