Просмотр исходного кода

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

permit_delete_collection per collection control
Peter Bieringer 1 год назад
Родитель
Сommit
bfe0ccc463
5 измененных файлов с 62 добавлено и 11 удалено
  1. 1 0
      CHANGELOG.md
  2. 5 0
      DOCUMENTATION.md
  3. 1 1
      radicale/app/base.py
  4. 17 10
      radicale/app/delete.py
  5. 38 0
      radicale/tests/test_base.py

+ 1 - 0
CHANGELOG.md

@@ -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

+ 5 - 0
DOCUMENTATION.md

@@ -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
@@ -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
 

+ 1 - 1
radicale/app/base.py

@@ -125,7 +125,7 @@ class Access:
 
     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()

+ 17 - 10
radicale/app/delete.py

@@ -3,6 +3,7 @@
 # Copyright © 2008 Pascal Halter
 # Copyright © 2008-2017 Guillaume Ayoub
 # 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
 # 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.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,
@@ -71,17 +73,22 @@ class ApplicationPartDelete(ApplicationBase):
             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

+ 38 - 0
radicale/tests/test_base.py

@@ -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: .*
@@ -439,6 +450,33 @@ permissions: RrWw""")
         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/")