audit log entries for version

This commit is contained in:
shamoon
2026-02-10 17:27:20 -08:00
parent d81748b39d
commit 9d3e62ff16
5 changed files with 118 additions and 10 deletions

View File

@@ -414,7 +414,12 @@ def set_permissions(
return "OK"
def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]:
def rotate(
doc_ids: list[int],
degrees: int,
*,
user: User | None = None,
) -> Literal["OK"]:
logger.info(
f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.",
)
@@ -447,6 +452,8 @@ def rotate(doc_ids: list[int], degrees: int) -> Literal["OK"]:
# Preserve metadata/permissions via overrides; mark as new version
overrides = DocumentMetadataOverrides().from_document(root_doc)
if user is not None:
overrides.actor_id = user.id
consume_file.delay(
ConsumableDocument(
@@ -645,7 +652,12 @@ def split(
return "OK"
def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
def delete_pages(
doc_ids: list[int],
pages: list[int],
*,
user: User | None = None,
) -> Literal["OK"]:
logger.info(
f"Attempting to delete pages {pages} from {len(doc_ids)} documents",
)
@@ -675,6 +687,8 @@ def delete_pages(doc_ids: list[int], pages: list[int]) -> Literal["OK"]:
pdf.save(filepath)
overrides = DocumentMetadataOverrides().from_document(root_doc)
if user is not None:
overrides.actor_id = user.id
consume_file.delay(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
@@ -759,6 +773,7 @@ def edit_pdf(
)
if user is not None:
overrides.owner_id = user.id
overrides.actor_id = user.id
consume_file.delay(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
@@ -776,6 +791,7 @@ def edit_pdf(
)
if user is not None:
overrides.owner_id = user.id
overrides.actor_id = user.id
if not delete_original:
overrides.skip_asn_if_exists = True
if delete_original and len(pdf_docs) == 1:
@@ -865,6 +881,7 @@ def remove_password(
)
if user is not None:
overrides.owner_id = user.id
overrides.actor_id = user.id
consume_file.delay(
ConsumableDocument(
source=DocumentSource.ConsumeFolder,
@@ -882,6 +899,7 @@ def remove_password(
)
if user is not None:
overrides.owner_id = user.id
overrides.actor_id = user.id
consume_tasks.append(
consume_file.s(

View File

@@ -489,14 +489,15 @@ class ConsumerPlugin(
# If this is a new version of an existing document, we need
# to make sure we're not creating a new document, but updating
# the existing one.
root_doc = Document.objects.get(
pk=self.input_doc.root_document_id,
)
original_document = Document.objects.get(
pk=self.input_doc.root_document_id,
)
self.log.debug("Saving record for updated version to database")
original_document.pk = None
original_document.root_document = Document.objects.get(
pk=self.input_doc.root_document_id,
)
original_document.root_document = root_doc
file_for_checksum = (
self.unmodified_original
if self.unmodified_original is not None
@@ -517,7 +518,42 @@ class ConsumerPlugin(
original_document.version_label = self.metadata.version_label
original_document.added = timezone.now()
original_document.modified = timezone.now()
original_document.save()
actor = None
# Save the new version, potentially creating an audit log entry for the version addition if enabled.
if (
settings.AUDIT_LOG_ENABLED
and self.metadata.actor_id is not None
):
from auditlog.models import LogEntry
actor = User.objects.filter(pk=self.metadata.actor_id).first()
if actor is not None:
from auditlog.context import set_actor
with set_actor(actor):
original_document.save()
else:
original_document.save()
else:
original_document.save()
# Create a log entry for the version addition, if enabled
if settings.AUDIT_LOG_ENABLED:
from auditlog.models import LogEntry
LogEntry.objects.log_create(
instance=root_doc,
changes={
"Version Added": ["None", original_document.id],
},
action=LogEntry.Action.UPDATE,
actor=actor,
additional_data={
"reason": "Version added",
"version_id": original_document.id,
},
)
document = original_document
else:
document = self._store(

View File

@@ -32,6 +32,7 @@ class DocumentMetadataOverrides:
custom_fields: dict | None = None
skip_asn_if_exists: bool = False
version_label: str | None = None
actor_id: int | None = None
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
"""
@@ -51,6 +52,8 @@ class DocumentMetadataOverrides:
self.storage_path_id = other.storage_path_id
if other.owner_id is not None:
self.owner_id = other.owner_id
if other.actor_id is not None:
self.actor_id = other.actor_id
if other.skip_asn_if_exists:
self.skip_asn_if_exists = True
if other.version_label is not None:

View File

@@ -554,6 +554,36 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
self.assertIsNone(response.data[1]["actor"])
self.assertEqual(response.data[1]["action"], "create")
def test_document_history_logs_version_deletion(self) -> None:
root_doc = Document.objects.create(
title="Root",
checksum="123",
mime_type="application/pdf",
owner=self.user,
)
version_doc = Document.objects.create(
title="Version",
checksum="456",
mime_type="application/pdf",
root_document=root_doc,
owner=self.user,
)
response = self.client.delete(
f"/api/documents/{root_doc.pk}/versions/{version_doc.pk}/",
)
self.assertEqual(response.status_code, status.HTTP_200_OK)
response = self.client.get(f"/api/documents/{root_doc.pk}/history/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 2)
self.assertEqual(response.data[0]["actor"]["id"], self.user.id)
self.assertEqual(response.data[0]["action"], "update")
self.assertEqual(
response.data[0]["changes"],
{"Version Deleted": [version_doc.pk, "None"]},
)
@override_settings(AUDIT_LOG_ENABLED=False)
def test_document_history_action_disabled(self) -> None:
"""

View File

@@ -1545,6 +1545,8 @@ class DocumentViewSet(
overrides = DocumentMetadataOverrides()
if label:
overrides.version_label = label.strip()
if request.user is not None:
overrides.actor_id = request.user.id
async_task = consume_file.delay(
input_doc,
@@ -1600,7 +1602,24 @@ class DocumentViewSet(
from documents import index
index.remove_document_from_index(version_doc)
version_doc_id = version_doc.id
version_doc.delete()
if settings.AUDIT_LOG_ENABLED:
actor = (
request.user if request.user and request.user.is_authenticated else None
)
LogEntry.objects.log_create(
instance=root_doc,
changes={
"Version Deleted": [version_doc_id, "None"],
},
action=LogEntry.Action.UPDATE,
actor=actor,
additional_data={
"reason": "Version deleted",
"version_id": version_doc_id,
},
)
current = (
Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc))
@@ -1913,13 +1932,13 @@ class BulkEditView(PassUserMixin):
"modify_custom_fields": "custom_fields",
"set_permissions": None,
"delete": "deleted_at",
"rotate": "checksum",
"delete_pages": "checksum",
"rotate": None,
"delete_pages": None,
"split": None,
"merge": None,
"edit_pdf": "checksum",
"edit_pdf": None,
"reprocess": "checksum",
"remove_password": "checksum",
"remove_password": None,
}
permission_classes = (IsAuthenticated,)
@@ -1937,6 +1956,8 @@ class BulkEditView(PassUserMixin):
if method in [
bulk_edit.split,
bulk_edit.merge,
bulk_edit.rotate,
bulk_edit.delete_pages,
bulk_edit.edit_pdf,
bulk_edit.remove_password,
]: