diff --git a/docker/management_script.sh b/docker/management_script.sh index 91a6336d0..6d5e84549 100755 --- a/docker/management_script.sh +++ b/docker/management_script.sh @@ -7,6 +7,11 @@ cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then python3 manage.py management_command "$@" -elif [[ $(id -un) == "paperless" ]]; then +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py management_command "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py management_command "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/convert_mariadb_uuid b/docker/rootfs/usr/local/bin/convert_mariadb_uuid index 019c558f1..7adb0a1af 100755 --- a/docker/rootfs/usr/local/bin/convert_mariadb_uuid +++ b/docker/rootfs/usr/local/bin/convert_mariadb_uuid @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py convert_mariadb_uuid "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py convert_mariadb_uuid "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py convert_mariadb_uuid "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py convert_mariadb_uuid "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/createsuperuser b/docker/rootfs/usr/local/bin/createsuperuser index 2b56869f6..b91cee3c5 100755 --- a/docker/rootfs/usr/local/bin/createsuperuser +++ b/docker/rootfs/usr/local/bin/createsuperuser @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py createsuperuser "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py createsuperuser "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py createsuperuser "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py createsuperuser "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_archiver b/docker/rootfs/usr/local/bin/document_archiver index 8d7771d26..4200aa7aa 100755 --- a/docker/rootfs/usr/local/bin/document_archiver +++ b/docker/rootfs/usr/local/bin/document_archiver @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_archiver "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_archiver "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_archiver "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_archiver "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_create_classifier b/docker/rootfs/usr/local/bin/document_create_classifier index 23acc6741..518551a4b 100755 --- a/docker/rootfs/usr/local/bin/document_create_classifier +++ b/docker/rootfs/usr/local/bin/document_create_classifier @@ -6,7 +6,16 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_create_classifier "$@" + python3 manage.py document_create_classifier "$@" +elif [[ $(id -u) == 0 ]]; then + s6-setuidgid paperless python3 manage.py document_create_classifier "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_create_classifier "$@" +else + echo "Unknown user." + exit 1 +fi +er "$@" elif [[ $(id -un) == "paperless" ]]; then s6-setuidgid paperless python3 manage.py document_create_classifier "$@" fi diff --git a/docker/rootfs/usr/local/bin/document_exporter b/docker/rootfs/usr/local/bin/document_exporter index d55f01d48..a82d70a16 100755 --- a/docker/rootfs/usr/local/bin/document_exporter +++ b/docker/rootfs/usr/local/bin/document_exporter @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_exporter "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_exporter "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_exporter "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_exporter "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_fuzzy_match b/docker/rootfs/usr/local/bin/document_fuzzy_match index c6e4edadc..b97c2a9ba 100755 --- a/docker/rootfs/usr/local/bin/document_fuzzy_match +++ b/docker/rootfs/usr/local/bin/document_fuzzy_match @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_fuzzy_match "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_fuzzy_match "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_fuzzy_match "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_fuzzy_match "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_importer b/docker/rootfs/usr/local/bin/document_importer index 07c92bb04..dbfb40a57 100755 --- a/docker/rootfs/usr/local/bin/document_importer +++ b/docker/rootfs/usr/local/bin/document_importer @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_importer "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_importer "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_importer "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_importer "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_index b/docker/rootfs/usr/local/bin/document_index index 47c893c10..b05f765da 100755 --- a/docker/rootfs/usr/local/bin/document_index +++ b/docker/rootfs/usr/local/bin/document_index @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_index "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_index "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_index "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_index "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_renamer b/docker/rootfs/usr/local/bin/document_renamer index 3406182ee..720edc0d8 100755 --- a/docker/rootfs/usr/local/bin/document_renamer +++ b/docker/rootfs/usr/local/bin/document_renamer @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_renamer "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_renamer "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_renamer "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_renamer "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_retagger b/docker/rootfs/usr/local/bin/document_retagger index b0d1047ff..6cbe03c19 100755 --- a/docker/rootfs/usr/local/bin/document_retagger +++ b/docker/rootfs/usr/local/bin/document_retagger @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_retagger "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_retagger "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_retagger "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_retagger "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_sanity_checker b/docker/rootfs/usr/local/bin/document_sanity_checker index d792124fc..8fff13a52 100755 --- a/docker/rootfs/usr/local/bin/document_sanity_checker +++ b/docker/rootfs/usr/local/bin/document_sanity_checker @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_sanity_checker "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_sanity_checker "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_sanity_checker "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_sanity_checker "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/document_thumbnails b/docker/rootfs/usr/local/bin/document_thumbnails index 71d80e00d..3c0f0de4c 100755 --- a/docker/rootfs/usr/local/bin/document_thumbnails +++ b/docker/rootfs/usr/local/bin/document_thumbnails @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py document_thumbnails "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_thumbnails "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py document_thumbnails "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py document_thumbnails "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/mail_fetcher b/docker/rootfs/usr/local/bin/mail_fetcher index 654c07389..762b850b9 100755 --- a/docker/rootfs/usr/local/bin/mail_fetcher +++ b/docker/rootfs/usr/local/bin/mail_fetcher @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py mail_fetcher "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py mail_fetcher "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py mail_fetcher "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py mail_fetcher "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/manage_superuser b/docker/rootfs/usr/local/bin/manage_superuser index a6e41168c..8f550cd1a 100755 --- a/docker/rootfs/usr/local/bin/manage_superuser +++ b/docker/rootfs/usr/local/bin/manage_superuser @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py manage_superuser "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py manage_superuser "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py manage_superuser "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py manage_superuser "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docker/rootfs/usr/local/bin/prune_audit_logs b/docker/rootfs/usr/local/bin/prune_audit_logs index 04446df17..8a3ab3299 100755 --- a/docker/rootfs/usr/local/bin/prune_audit_logs +++ b/docker/rootfs/usr/local/bin/prune_audit_logs @@ -6,7 +6,12 @@ set -e cd "${PAPERLESS_SRC_DIR}" if [[ -n "${USER_IS_NON_ROOT}" ]]; then - python3 manage.py prune_audit_logs "$@" -elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py prune_audit_logs "$@" +elif [[ $(id -u) == 0 ]]; then s6-setuidgid paperless python3 manage.py prune_audit_logs "$@" +elif [[ $(id -un) == "paperless" ]]; then + python3 manage.py prune_audit_logs "$@" +else + echo "Unknown user." + exit 1 fi diff --git a/docs/changelog.md b/docs/changelog.md index f222a7305..d6a87e299 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,24 @@ # Changelog +## paperless-ngx 2.20.6 + +### Bug Fixes + +- Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888)) +- Fixhancement: change date calculation for 'this year' to include future documents [@shamoon](https://github.com/shamoon) ([#11884](https://github.com/paperless-ngx/paperless-ngx/pull/11884)) +- Fix: Running management scripts under rootless could fail [@stumpylog](https://github.com/stumpylog) ([#11870](https://github.com/paperless-ngx/paperless-ngx/pull/11870)) +- Fix: use correct field id for overrides [@shamoon](https://github.com/shamoon) ([#11869](https://github.com/paperless-ngx/paperless-ngx/pull/11869)) + +### All App Changes + +
+3 changes + +- Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888)) +- Fixhancement: change date calculation for 'this year' to include future documents [@shamoon](https://github.com/shamoon) ([#11884](https://github.com/paperless-ngx/paperless-ngx/pull/11884)) +- Fix: use correct field id for overrides [@shamoon](https://github.com/shamoon) ([#11869](https://github.com/paperless-ngx/paperless-ngx/pull/11869)) +
+ ## paperless-ngx 2.20.5 ### Bug Fixes diff --git a/pyproject.toml b/pyproject.toml index b25c4ea34..b9bbca96b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.5" +version = "2.20.6" description = "A community-supported supercharged document management system: scan, index and archive all your physical documents" readme = "README.md" requires-python = ">=3.10" diff --git a/src-ui/package.json b/src-ui/package.json index 0536c0489..560c88037 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.5", + "version": "2.20.6", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/app/components/manage/saved-views/saved-views.component.html b/src-ui/src/app/components/manage/saved-views/saved-views.component.html index 10487fec8..2f5ef3338 100644 --- a/src-ui/src/app/components/manage/saved-views/saved-views.component.html +++ b/src-ui/src/app/components/manage/saved-views/saved-views.component.html @@ -51,6 +51,7 @@ @if (displayFields) { } + Note: ordering is not preserved diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 9ebf29d16..3ce1d16cc 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -6,7 +6,7 @@ export const environment = { apiVersion: '9', // match src/paperless/settings.py appTitle: 'Paperless-ngx', tag: 'prod', - version: '2.20.5', + version: '2.20.6', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index de5f4d33f..f7ed197da 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -43,6 +43,7 @@ from guardian.utils import get_group_obj_perms_model from guardian.utils import get_user_obj_perms_model from rest_framework import fields from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied from rest_framework.fields import SerializerMethodField from rest_framework.filters import OrderingFilter @@ -444,6 +445,19 @@ class OwnedObjectSerializer( return instance def update(self, instance, validated_data): + user = getattr(self, "user", None) + is_superuser = user.is_superuser if user is not None else False + is_owner = instance.owner == user if user is not None else False + is_unowned = instance.owner is None + + if ( + ("owner" in validated_data and validated_data["owner"] != instance.owner) + or "set_permissions" in validated_data + ) and not (is_superuser or is_owner or is_unowned): + raise PermissionDenied( + _("Insufficient permissions."), + ) + if "set_permissions" in validated_data: self._set_permissions(validated_data["set_permissions"], instance) self.validate_unique_together(validated_data, instance) diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index 96d22dc2c..ef06ce648 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -1220,6 +1220,17 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): self.assertEqual(response.status_code, status.HTTP_405_METHOD_NOT_ALLOWED) + def test_upload_insufficient_permissions(self): + self.client.force_authenticate(user=User.objects.create_user("testuser2")) + + with (Path(__file__).parent / "samples" / "simple.pdf").open("rb") as f: + response = self.client.post( + "/api/documents/post_document/", + {"document": f}, + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_upload_empty_metadata(self): self.consume_file_mock.return_value = celery.result.AsyncResult( id=str(uuid.uuid4()), diff --git a/src/documents/tests/test_api_permissions.py b/src/documents/tests/test_api_permissions.py index bc81dabe9..31b860745 100644 --- a/src/documents/tests/test_api_permissions.py +++ b/src/documents/tests/test_api_permissions.py @@ -441,6 +441,59 @@ class TestApiAuth(DirectoriesMixin, APITestCase): self.assertTrue(checker.has_perm("change_document", doc)) self.assertIn("change_document", get_perms(group1, doc)) + def test_document_permissions_change_requires_owner(self): + owner = User.objects.create_user(username="owner") + editor = User.objects.create_user(username="editor") + editor.user_permissions.add( + *Permission.objects.all(), + ) + + doc = Document.objects.create( + title="Ownered doc", + content="sensitive", + checksum="abc123", + mime_type="application/pdf", + owner=owner, + ) + + assign_perm("view_document", editor, doc) + assign_perm("change_document", editor, doc) + + self.client.force_authenticate(editor) + response = self.client.patch( + f"/api/documents/{doc.pk}/", + json.dumps( + { + "set_permissions": { + "view": { + "users": [editor.id], + "groups": [], + }, + "change": { + "users": None, + "groups": None, + }, + }, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + + self.client.force_authenticate(editor) + response = self.client.patch( + f"/api/documents/{doc.pk}/", + json.dumps( + { + "owner": editor.id, + }, + ), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_dynamic_permissions_fields(self): user1 = User.objects.create_user(username="user1") user1.user_permissions.add(*Permission.objects.filter(codename="view_document")) diff --git a/src/documents/views.py b/src/documents/views.py index c0f3b5db4..df1fa9827 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1180,7 +1180,7 @@ class DocumentViewSet( ): return HttpResponseForbidden("Insufficient permissions to delete notes") - note = Note.objects.get(id=int(request.GET.get("id"))) + note = Note.objects.get(id=int(request.GET.get("id")), document=doc) if settings.AUDIT_LOG_ENABLED: LogEntry.objects.log_create( instance=doc, @@ -1840,6 +1840,8 @@ class PostDocumentView(GenericAPIView): parser_classes = (parsers.MultiPartParser,) def post(self, request, *args, **kwargs): + if not request.user.has_perm("documents.add_document"): + return HttpResponseForbidden("Insufficient permissions") serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/src/paperless/version.py b/src/paperless/version.py index aeeee68e0..ec6eaed08 100644 --- a/src/paperless/version.py +++ b/src/paperless/version.py @@ -1,6 +1,6 @@ from typing import Final -__version__: Final[tuple[int, int, int]] = (2, 20, 5) +__version__: Final[tuple[int, int, int]] = (2, 20, 6) # Version string like X.Y.Z __full_version_str__: Final[str] = ".".join(map(str, __version__)) # Version string like X.Y diff --git a/uv.lock b/uv.lock index 067d4cfb7..c4839e61b 100644 --- a/uv.lock +++ b/uv.lock @@ -3157,7 +3157,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.5" +version = "2.20.6" source = { virtual = "." } dependencies = [ { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },