Skip to content

Commit

Permalink
Merge pull request #1585 from pbiering/add-permit_overwrite_collection
Browse files Browse the repository at this point in the history
Add option permit_overwrite_collection
  • Loading branch information
pbiering authored Sep 30, 2024
2 parents bfe0ccc + 6736218 commit e59e4d3
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=True) which can be also controlled per collection by rights 'O' or 'o'

## 3.2.3
* Add: support for Python 3.13
Expand Down
11 changes: 11 additions & 0 deletions DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: 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

#### storage

##### type
Expand Down Expand Up @@ -1300,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

Expand Down
2 changes: 2 additions & 0 deletions config
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@
# Permit delete of a collection (global)
#permit_delete_collection = True

# Permit overwrite of a collection (global)
#permit_overwrite_collection = True

[storage]

Expand Down
3 changes: 3 additions & 0 deletions radicale/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion radicale/app/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -125,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()
Expand Down
8 changes: 8 additions & 0 deletions radicale/app/put.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions radicale/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,10 @@ def json_str(value: Any) -> dict:
"value": "True",
"help": "permit delete of a collection",
"type": bool}),
("permit_overwrite_collection", {
"value": "True",
"help": "permit overwrite of a collection",
"type": bool}),
("file", {
"value": "/etc/radicale/rights",
"help": "file for rights management from_file",
Expand Down
39 changes: 36 additions & 3 deletions radicale/tests/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -54,6 +53,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: .*
Expand Down Expand Up @@ -450,8 +459,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")
Expand Down Expand Up @@ -488,6 +497,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/")
Expand Down

0 comments on commit e59e4d3

Please sign in to comment.