diff --git a/radicale/app/delete.py b/radicale/app/delete.py index 585c645b..5388e86d 100644 --- a/radicale/app/delete.py +++ b/radicale/app/delete.py @@ -57,6 +57,7 @@ class ApplicationPartDelete(ApplicationBase): def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: """Manage DELETE request.""" + actor = user permissions_filter = None if self._sharing._enabled: # Sharing by token or map (if enabled) @@ -99,7 +100,8 @@ class ApplicationPartDelete(ApplicationBase): content_type=i.name, uid=i.uid, old_content=i.serialize(), # type: ignore - new_content=None + new_content=None, + actor=actor, ) ) xml_answer = xml_delete(base_prefix, path, item) @@ -116,6 +118,7 @@ class ApplicationPartDelete(ApplicationBase): uid=item.uid, old_content=item.serialize(), # type: ignore new_content=None, + actor=actor, ) ) xml_answer = xml_delete( diff --git a/radicale/app/proppatch.py b/radicale/app/proppatch.py index 2fd48671..f97d4737 100644 --- a/radicale/app/proppatch.py +++ b/radicale/app/proppatch.py @@ -99,6 +99,7 @@ class ApplicationPartProppatch(ApplicationBase): def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: """Manage PROPPATCH request.""" + actor = user permissions_filter = None share = None share_overlay = False @@ -220,7 +221,8 @@ class ApplicationPartProppatch(ApplicationBase): content_type=None, # Can't easily determine content type, won't trigger email hook uid=None, old_content=None, - new_content=content + new_content=content, + actor=actor, ) self._hook.notify(hook_notification_item) except ValueError as e: diff --git a/radicale/app/put.py b/radicale/app/put.py index 08f1bb0a..7d14955e 100644 --- a/radicale/app/put.py +++ b/radicale/app/put.py @@ -181,6 +181,7 @@ class ApplicationPartPut(ApplicationBase): def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str, remote_host: str, remote_useragent: str) -> types.WSGIResponse: """Manage PUT request.""" + actor = user permissions_filter = None if self._sharing._enabled: # Sharing by token or map (if enabled) @@ -320,6 +321,7 @@ class ApplicationPartPut(ApplicationBase): uid=None, old_content=existing_item.serialize(), new_content=item.serialize(), + actor=actor, ) else: # We assume the item is new because it was not in the replaced_items hook_notification_item = HookNotificationItem( @@ -329,7 +331,8 @@ class ApplicationPartPut(ApplicationBase): content_type=item.name, uid=None, old_content=None, - new_content=item.serialize() + new_content=item.serialize(), + actor=actor, ) self._hook.notify(hook_notification_item) except ValueError as e: @@ -358,6 +361,7 @@ class ApplicationPartPut(ApplicationBase): uid=None, old_content=replaced_item.serialize() if replaced_item else None, new_content=prepared_item.serialize(), + actor=actor, ) self._hook.notify(hook_notification_item) except ValueError as e: diff --git a/radicale/hook/__init__.py b/radicale/hook/__init__.py index b1f771d0..632f184a 100644 --- a/radicale/hook/__init__.py +++ b/radicale/hook/__init__.py @@ -61,9 +61,10 @@ def _cleanup(path): class HookNotificationItem: def __init__(self, notification_item_type, path, content=None, content_type=None, uid=None, new_content=None, - old_content=None): + old_content=None, actor=None): self.type = notification_item_type.value self.point = _cleanup(path) + self.actor = actor self._content_legacy = content self.content_type = content_type self.uid = uid diff --git a/radicale/tests/test_hook_rabbitmq.py b/radicale/tests/test_hook_rabbitmq.py index 80abb55c..f42076e6 100644 --- a/radicale/tests/test_hook_rabbitmq.py +++ b/radicale/tests/test_hook_rabbitmq.py @@ -19,6 +19,7 @@ Radicale tests related to hook 'rabbitmq' """ +import json import logging import os @@ -112,3 +113,52 @@ permissions: RrWw""") found = True if (found is False): raise ValueError("Logging misses expected log line") + + @pytest.mark.skipif(has_pika == 0, reason="No pika module installed") + def test_shared_event_actor(self, caplog) -> None: + caplog.set_level(logging.WARNING) + + htpasswd_file_path = os.path.join(self.colpath, ".htpasswd") + encoding: str = self.configuration.get("encoding", "stock") + with open(htpasswd_file_path, "w", encoding=encoding) as f: + f.write("owner:ownerpw\nuser:userpw") + + self.configure({"auth": {"type": "htpasswd", + "htpasswd_filename": htpasswd_file_path, + "htpasswd_encryption": "plain"}, + "rights": {"type": "owner_only"}, + "sharing": {"type": "csv", + "collection_by_map": "True", + "permit_create_map": "True"}}) + + path_owner = "/owner/calendar.ics/" + path_shared = "/user/calendar-shared.ics/" + + self.mkcalendar(path_owner, login="owner:ownerpw") + self.request("POST", "/.sharing/v1/map/create", + data=json.dumps({"User": "user", + "PathMapped": path_owner, + "PathOrToken": path_shared, + "Permissions": "rw", + "Enabled": True}), + content_type="application/json", accept="application/json", + login="owner:ownerpw", check=200) + self.request("POST", "/.sharing/v1/map/enable", + data=json.dumps({"PathOrToken": path_shared}), + content_type="application/json", accept="application/json", + login="user:userpw", check=200) + + caplog.clear() + event = get_file_content("event1.ics") + path = path_shared + "event1.ics" + + self.put(path, event, login="user:userpw") + assert any("notification_item: {'type': 'upsert'" in line and + "'actor': 'user'" in line + for line in caplog.messages) + + caplog.clear() + self.request("DELETE", path, login="user:userpw", check=200) + assert any("notification_item: {'type': 'delete'" in line and + "'actor': 'user'" in line + for line in caplog.messages)