mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-11 23:59:31 -06:00
audit log entries for version
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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,
|
||||
]:
|
||||
|
||||
Reference in New Issue
Block a user