diff --git a/.python-version b/.python-version deleted file mode 100644 index ae2119c..0000000 --- a/.python-version +++ /dev/null @@ -1 +0,0 @@ -guillotina-audit diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d9b9f33..23e21ae 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,5 +1,5 @@ 1.0.4 (2023-11-16) ------------------ +------------------ - Changing requirement of guillotina diff --git a/guillotina_audit/__init__.py b/guillotina_audit/__init__.py index 3034f0e..1ebd3ff 100644 --- a/guillotina_audit/__init__.py +++ b/guillotina_audit/__init__.py @@ -18,3 +18,4 @@ def includeme(root, settings): configure.scan("guillotina_audit.subscriber") configure.scan("guillotina_audit.api") configure.scan("guillotina_audit.permissions") + configure.scan("guillotina_audit.models") diff --git a/guillotina_audit/models.py b/guillotina_audit/models.py new file mode 100644 index 0000000..ba8e89d --- /dev/null +++ b/guillotina_audit/models.py @@ -0,0 +1,30 @@ +import json +import datetime + +from pydantic import BaseModel, field_serializer +from enum import Enum +from typing import Optional +from datetime import date + +from datetime import timezone + + +class Action(str, Enum): + modified = "modified" + added = "added" + removed = "removed" + moved = "moved" + duplicated = "duplicated" + +class Document(BaseModel): + action: Action + path: Optional[str] = None + uuid: Optional[str] = None + payload: Optional[dict] = None + creator: Optional[str] = None + creation_date: date = datetime.datetime.now(timezone.utc) + type_name: str + + @field_serializer('payload') + def serialize_payload(self, payload: dict, _info): + return json.dumps(payload) diff --git a/guillotina_audit/subscriber.py b/guillotina_audit/subscriber.py index 61c0ace..a31299c 100644 --- a/guillotina_audit/subscriber.py +++ b/guillotina_audit/subscriber.py @@ -3,6 +3,8 @@ from guillotina.interfaces import IObjectAddedEvent from guillotina.interfaces import IObjectModifiedEvent from guillotina.interfaces import IObjectRemovedEvent +from guillotina.interfaces import IObjectMovedEvent +from guillotina.interfaces import IObjectDuplicatedEvent from guillotina.interfaces import IResource from guillotina_audit.interfaces import IAuditUtility @@ -17,9 +19,15 @@ ) # after indexing async def audit_object_added(obj, event): try: - audit = query_utility(IAuditUtility) - audit.log_entry(obj, event) + if event.__providedBy__(IObjectDuplicatedEvent) is True: + pass + elif event.__providedBy__(IObjectMovedEvent) is True: + pass + elif event.__providedBy__(IObjectAddedEvent) is True: + audit = query_utility(IAuditUtility) + audit.log_entry(obj, event) except Exception: + __import__("pdb").set_trace() logger.error("Error adding audit", exc_info=True) @@ -43,3 +51,25 @@ async def audit_object_removed(obj, event): audit.log_entry(obj, event) except Exception: logger.error("Error adding audit", exc_info=True) + + +@configure.subscriber( + for_=(IResource, IObjectMovedEvent), priority=1001 +) # after indexing +async def audit_object_moved(obj, event): + try: + audit = query_utility(IAuditUtility) + audit.log_entry(obj, event) + except Exception: + logger.error("Error adding audit", exc_info=True) + + +@configure.subscriber( + for_=(IResource, IObjectDuplicatedEvent), priority=1001 +) # after indexing +async def audit_object_duplicated(obj, event): + try: + audit = query_utility(IAuditUtility) + audit.log_entry(obj, event) + except Exception: + logger.error("Error adding audit", exc_info=True) diff --git a/guillotina_audit/tests/test_audit_basic.py b/guillotina_audit/tests/test_audit_basic.py index 177bb32..e270800 100644 --- a/guillotina_audit/tests/test_audit_basic.py +++ b/guillotina_audit/tests/test_audit_basic.py @@ -2,6 +2,7 @@ from datetime import timedelta from guillotina.component import query_utility from guillotina_audit.interfaces import IAuditUtility +from guillotina_audit.models import Document import asyncio import json @@ -84,3 +85,71 @@ async def test_audit_basic(guillotina_es): ) # noqa assert len(resp["hits"]["hits"]) == 0 assert status == 200 + + response, status = await guillotina_es( + "POST", + "/db/guillotina/", + data=json.dumps({"@type": "Folder", "id": "foo_folder1", "title": "Foo Folder"}), + ) + assert status == 201 + + response, status = await guillotina_es( + "POST", + "/db/guillotina/foo_folder1/@duplicate", + data=json.dumps({"destination": "/", "new_id": "foo_folder2"}), + ) + assert status == 200 + + await asyncio.sleep(2) + resp, status = await guillotina_es( + "GET", + "/db/guillotina/@audit?action=duplicated", + ) # noqa + assert len(resp["hits"]["hits"]) == 1 + assert status == 200 + + response, status = await guillotina_es( + "POST", + "/db/guillotina/foo_folder2/@move", + data=json.dumps({"destination": "/foo_folder1"}), + ) + assert status == 200 + await asyncio.sleep(2) + + resp, status = await guillotina_es( + "GET", + "/db/guillotina/@audit?action=moved", + ) # noqa + assert len(resp["hits"]["hits"]) == 1 + assert status == 200 + + +async def test_audit_wildcard(guillotina_es): + response, status = await guillotina_es( + "POST", "/db/guillotina/@addons", data=json.dumps({"id": "audit"}) + ) + assert status == 200 + await asyncio.sleep(2) + audit_utility = query_utility(IAuditUtility) + + payload = Document(action="added", type_name="Fullscreen") + audit_utility.log_wildcard(payload) + await asyncio.sleep(2) + + resp, status = await guillotina_es( + "GET", + "/db/guillotina/@audit?action=added&type_name=Fullscreen", + ) # noqa + assert status == 200 + assert len(resp["hits"]["hits"]) == 1 + + payload = Document(action="added", type_name="Click", path="/foopath", payload={"hola": "hola"}, creator="creator", uuid="12345") + audit_utility.log_wildcard(payload) + await asyncio.sleep(2) + + resp, status = await guillotina_es( + "GET", + "/db/guillotina/@audit?action=added&type_name=Click", + ) # noqa + assert status == 200 + assert len(resp["hits"]["hits"]) == 1 diff --git a/guillotina_audit/utility.py b/guillotina_audit/utility.py index 02e5c11..a8f178c 100644 --- a/guillotina_audit/utility.py +++ b/guillotina_audit/utility.py @@ -6,8 +6,11 @@ from guillotina.interfaces import IObjectAddedEvent from guillotina.interfaces import IObjectModifiedEvent from guillotina.interfaces import IObjectRemovedEvent +from guillotina.interfaces import IObjectMovedEvent +from guillotina.interfaces import IObjectDuplicatedEvent from guillotina.utils.auth import get_authenticated_user from guillotina.utils.content import get_content_path +from guillotina_audit.models import Document import asyncio import datetime @@ -65,10 +68,21 @@ def default_mappings(self): }, } + def log_wildcard(self, payload: Document): + coroutine = self.async_es.index(index=self.index, body=payload.dict(exclude_unset=True)) + asyncio.create_task(coroutine) + def log_entry(self, obj, event): document = {} user = get_authenticated_user() - if IObjectModifiedEvent.providedBy(event): + + if IObjectDuplicatedEvent.providedBy(event): + document["action"] = "duplicated" + document["creation_date"] = datetime.datetime.now(timezone.utc) + elif IObjectMovedEvent.providedBy(event): + document["action"] = "moved" + document["creation_date"] = datetime.datetime.now(timezone.utc) + elif IObjectModifiedEvent.providedBy(event): document["action"] = "modified" document["creation_date"] = obj.modification_date if self._settings.get("save_payload", False) is True: diff --git a/setup.py b/setup.py index 4160b41..3299127 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ packages=find_packages(exclude=["ez_setup"]), install_requires=[ "guillotina>=6.0.0a16", + "pydantic", "elasticsearch[async]>=7.8.0,<8.0.0", "zope.interface==5.1.0" # TODO: remove once guillotina has solved this ],