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"
|
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(
|
logger.info(
|
||||||
f"Attempting to rotate {len(doc_ids)} documents by {degrees} degrees.",
|
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
|
# Preserve metadata/permissions via overrides; mark as new version
|
||||||
overrides = DocumentMetadataOverrides().from_document(root_doc)
|
overrides = DocumentMetadataOverrides().from_document(root_doc)
|
||||||
|
if user is not None:
|
||||||
|
overrides.actor_id = user.id
|
||||||
|
|
||||||
consume_file.delay(
|
consume_file.delay(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
@@ -645,7 +652,12 @@ def split(
|
|||||||
return "OK"
|
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(
|
logger.info(
|
||||||
f"Attempting to delete pages {pages} from {len(doc_ids)} documents",
|
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)
|
pdf.save(filepath)
|
||||||
|
|
||||||
overrides = DocumentMetadataOverrides().from_document(root_doc)
|
overrides = DocumentMetadataOverrides().from_document(root_doc)
|
||||||
|
if user is not None:
|
||||||
|
overrides.actor_id = user.id
|
||||||
consume_file.delay(
|
consume_file.delay(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
@@ -759,6 +773,7 @@ def edit_pdf(
|
|||||||
)
|
)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
overrides.owner_id = user.id
|
overrides.owner_id = user.id
|
||||||
|
overrides.actor_id = user.id
|
||||||
consume_file.delay(
|
consume_file.delay(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
@@ -776,6 +791,7 @@ def edit_pdf(
|
|||||||
)
|
)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
overrides.owner_id = user.id
|
overrides.owner_id = user.id
|
||||||
|
overrides.actor_id = user.id
|
||||||
if not delete_original:
|
if not delete_original:
|
||||||
overrides.skip_asn_if_exists = True
|
overrides.skip_asn_if_exists = True
|
||||||
if delete_original and len(pdf_docs) == 1:
|
if delete_original and len(pdf_docs) == 1:
|
||||||
@@ -865,6 +881,7 @@ def remove_password(
|
|||||||
)
|
)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
overrides.owner_id = user.id
|
overrides.owner_id = user.id
|
||||||
|
overrides.actor_id = user.id
|
||||||
consume_file.delay(
|
consume_file.delay(
|
||||||
ConsumableDocument(
|
ConsumableDocument(
|
||||||
source=DocumentSource.ConsumeFolder,
|
source=DocumentSource.ConsumeFolder,
|
||||||
@@ -882,6 +899,7 @@ def remove_password(
|
|||||||
)
|
)
|
||||||
if user is not None:
|
if user is not None:
|
||||||
overrides.owner_id = user.id
|
overrides.owner_id = user.id
|
||||||
|
overrides.actor_id = user.id
|
||||||
|
|
||||||
consume_tasks.append(
|
consume_tasks.append(
|
||||||
consume_file.s(
|
consume_file.s(
|
||||||
|
|||||||
@@ -489,14 +489,15 @@ class ConsumerPlugin(
|
|||||||
# If this is a new version of an existing document, we need
|
# If this is a new version of an existing document, we need
|
||||||
# to make sure we're not creating a new document, but updating
|
# to make sure we're not creating a new document, but updating
|
||||||
# the existing one.
|
# the existing one.
|
||||||
|
root_doc = Document.objects.get(
|
||||||
|
pk=self.input_doc.root_document_id,
|
||||||
|
)
|
||||||
original_document = Document.objects.get(
|
original_document = Document.objects.get(
|
||||||
pk=self.input_doc.root_document_id,
|
pk=self.input_doc.root_document_id,
|
||||||
)
|
)
|
||||||
self.log.debug("Saving record for updated version to database")
|
self.log.debug("Saving record for updated version to database")
|
||||||
original_document.pk = None
|
original_document.pk = None
|
||||||
original_document.root_document = Document.objects.get(
|
original_document.root_document = root_doc
|
||||||
pk=self.input_doc.root_document_id,
|
|
||||||
)
|
|
||||||
file_for_checksum = (
|
file_for_checksum = (
|
||||||
self.unmodified_original
|
self.unmodified_original
|
||||||
if self.unmodified_original is not None
|
if self.unmodified_original is not None
|
||||||
@@ -517,7 +518,42 @@ class ConsumerPlugin(
|
|||||||
original_document.version_label = self.metadata.version_label
|
original_document.version_label = self.metadata.version_label
|
||||||
original_document.added = timezone.now()
|
original_document.added = timezone.now()
|
||||||
original_document.modified = 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
|
document = original_document
|
||||||
else:
|
else:
|
||||||
document = self._store(
|
document = self._store(
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ class DocumentMetadataOverrides:
|
|||||||
custom_fields: dict | None = None
|
custom_fields: dict | None = None
|
||||||
skip_asn_if_exists: bool = False
|
skip_asn_if_exists: bool = False
|
||||||
version_label: str | None = None
|
version_label: str | None = None
|
||||||
|
actor_id: int | None = None
|
||||||
|
|
||||||
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
|
def update(self, other: "DocumentMetadataOverrides") -> "DocumentMetadataOverrides":
|
||||||
"""
|
"""
|
||||||
@@ -51,6 +52,8 @@ class DocumentMetadataOverrides:
|
|||||||
self.storage_path_id = other.storage_path_id
|
self.storage_path_id = other.storage_path_id
|
||||||
if other.owner_id is not None:
|
if other.owner_id is not None:
|
||||||
self.owner_id = other.owner_id
|
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:
|
if other.skip_asn_if_exists:
|
||||||
self.skip_asn_if_exists = True
|
self.skip_asn_if_exists = True
|
||||||
if other.version_label is not None:
|
if other.version_label is not None:
|
||||||
|
|||||||
@@ -554,6 +554,36 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase):
|
|||||||
self.assertIsNone(response.data[1]["actor"])
|
self.assertIsNone(response.data[1]["actor"])
|
||||||
self.assertEqual(response.data[1]["action"], "create")
|
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)
|
@override_settings(AUDIT_LOG_ENABLED=False)
|
||||||
def test_document_history_action_disabled(self) -> None:
|
def test_document_history_action_disabled(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1545,6 +1545,8 @@ class DocumentViewSet(
|
|||||||
overrides = DocumentMetadataOverrides()
|
overrides = DocumentMetadataOverrides()
|
||||||
if label:
|
if label:
|
||||||
overrides.version_label = label.strip()
|
overrides.version_label = label.strip()
|
||||||
|
if request.user is not None:
|
||||||
|
overrides.actor_id = request.user.id
|
||||||
|
|
||||||
async_task = consume_file.delay(
|
async_task = consume_file.delay(
|
||||||
input_doc,
|
input_doc,
|
||||||
@@ -1600,7 +1602,24 @@ class DocumentViewSet(
|
|||||||
from documents import index
|
from documents import index
|
||||||
|
|
||||||
index.remove_document_from_index(version_doc)
|
index.remove_document_from_index(version_doc)
|
||||||
|
version_doc_id = version_doc.id
|
||||||
version_doc.delete()
|
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 = (
|
current = (
|
||||||
Document.objects.filter(Q(id=root_doc.id) | Q(root_document=root_doc))
|
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",
|
"modify_custom_fields": "custom_fields",
|
||||||
"set_permissions": None,
|
"set_permissions": None,
|
||||||
"delete": "deleted_at",
|
"delete": "deleted_at",
|
||||||
"rotate": "checksum",
|
"rotate": None,
|
||||||
"delete_pages": "checksum",
|
"delete_pages": None,
|
||||||
"split": None,
|
"split": None,
|
||||||
"merge": None,
|
"merge": None,
|
||||||
"edit_pdf": "checksum",
|
"edit_pdf": None,
|
||||||
"reprocess": "checksum",
|
"reprocess": "checksum",
|
||||||
"remove_password": "checksum",
|
"remove_password": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
@@ -1937,6 +1956,8 @@ class BulkEditView(PassUserMixin):
|
|||||||
if method in [
|
if method in [
|
||||||
bulk_edit.split,
|
bulk_edit.split,
|
||||||
bulk_edit.merge,
|
bulk_edit.merge,
|
||||||
|
bulk_edit.rotate,
|
||||||
|
bulk_edit.delete_pages,
|
||||||
bulk_edit.edit_pdf,
|
bulk_edit.edit_pdf,
|
||||||
bulk_edit.remove_password,
|
bulk_edit.remove_password,
|
||||||
]:
|
]:
|
||||||
|
|||||||
Reference in New Issue
Block a user