diff --git a/docs/changelog.md b/docs/changelog.md index fb4229e5a..13b935e01 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,9 +1,37 @@ # Changelog +## paperless-ngx 2.20.9 + +### Security + +- Resolve [GHSA-386h-chg4-cfw9](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-386h-chg4-cfw9) + +### Bug Fixes + +- Fixhancement: config option reset [@shamoon](https://github.com/shamoon) ([#12176](https://github.com/paperless-ngx/paperless-ngx/pull/12176)) +- Fix: correct page count by separating display vs collection sizes for tags [@shamoon](https://github.com/shamoon) ([#12170](https://github.com/paperless-ngx/paperless-ngx/pull/12170)) + +### All App Changes + +
+2 changes + +- Fixhancement: config option reset [@shamoon](https://github.com/shamoon) ([#12176](https://github.com/paperless-ngx/paperless-ngx/pull/12176)) +- Fix: correct page count by separating display vs collection sizes for tags [@shamoon](https://github.com/shamoon) ([#12170](https://github.com/paperless-ngx/paperless-ngx/pull/12170)) +
+ ## paperless-ngx 2.20.8 +### Security + +- Resolve [GHSA-7qqc-wrcw-2fj9](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-7qqc-wrcw-2fj9) + ## paperless-ngx 2.20.7 +### Security + +- Resolve [GHSA-x395-6h48-wr8v](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-x395-6h48-wr8v) + ### Bug Fixes - Performance fix: use subqueries to improve object retrieval in large installs [@shamoon](https://github.com/shamoon) ([#11950](https://github.com/paperless-ngx/paperless-ngx/pull/11950)) @@ -22,6 +50,10 @@ ## paperless-ngx 2.20.6 +### Security + +- Resolve [GHSA-jqwv-hx7q-fxh3](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-jqwv-hx7q-fxh3) and [GHSA-w47q-3m69-84v8](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-w47q-3m69-84v8) + ### Bug Fixes - Fix: extract all ids for nested tags [@shamoon](https://github.com/shamoon) ([#11888](https://github.com/paperless-ngx/paperless-ngx/pull/11888)) diff --git a/docs/setup.md b/docs/setup.md index 415b3622e..3b033ed16 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -504,7 +504,8 @@ installation. Keep these points in mind: - Read the [changelog](changelog.md) and take note of breaking changes. - Decide whether to stay on SQLite or migrate to PostgreSQL. - Both work fine with + See [documentation](#sqlite_to_psql) for details on moving data + from SQLite to PostgreSQL. Both work fine with Paperless. However, if you already have a database server running for other services, you might as well use it for Paperless as well. - The task scheduler of Paperless, which is used to execute periodic diff --git a/pyproject.toml b/pyproject.toml index c69aba078..ddda5c930 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "paperless-ngx" -version = "2.20.8" +version = "2.20.9" 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 59d29ca74..aea89adce 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -1,6 +1,6 @@ { "name": "paperless-ngx-ui", - "version": "2.20.8", + "version": "2.20.9", "scripts": { "preinstall": "npx only-allow pnpm", "ng": "ng", diff --git a/src-ui/src/app/components/admin/config/config.component.html b/src-ui/src/app/components/admin/config/config.component.html index 1d38a5d32..fcb50183a 100644 --- a/src-ui/src/app/components/admin/config/config.component.html +++ b/src-ui/src/app/components/admin/config/config.component.html @@ -19,13 +19,18 @@
-
-
- {{option.title}} - - - +
+
+ {{option.title}}
+ + + + @if (isSet(option.key)) { + + }
@switch (option.type) { diff --git a/src-ui/src/app/components/admin/config/config.component.spec.ts b/src-ui/src/app/components/admin/config/config.component.spec.ts index 079bd1420..f4f4799a6 100644 --- a/src-ui/src/app/components/admin/config/config.component.spec.ts +++ b/src-ui/src/app/components/admin/config/config.component.spec.ts @@ -144,4 +144,18 @@ describe('ConfigComponent', () => { component.uploadFile(new File([], 'test.png'), 'app_logo') expect(initSpy).toHaveBeenCalled() }) + + it('should reset option to null', () => { + component.configForm.patchValue({ output_type: OutputTypeConfig.PDF_A }) + expect(component.isSet('output_type')).toBeTruthy() + component.resetOption('output_type') + expect(component.configForm.get('output_type').value).toBeNull() + expect(component.isSet('output_type')).toBeFalsy() + component.configForm.patchValue({ app_title: 'Test Title' }) + component.resetOption('app_title') + expect(component.configForm.get('app_title').value).toBeNull() + component.configForm.patchValue({ barcodes_enabled: true }) + component.resetOption('barcodes_enabled') + expect(component.configForm.get('barcodes_enabled').value).toBeNull() + }) }) diff --git a/src-ui/src/app/components/admin/config/config.component.ts b/src-ui/src/app/components/admin/config/config.component.ts index b0dcba57b..9e69c9dc6 100644 --- a/src-ui/src/app/components/admin/config/config.component.ts +++ b/src-ui/src/app/components/admin/config/config.component.ts @@ -210,4 +210,12 @@ export class ConfigComponent }, }) } + + public isSet(key: string): boolean { + return this.configForm.get(key).value != null + } + + public resetOption(key: string) { + this.configForm.get(key).setValue(null) + } } diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index 0ad536f2b..afd702263 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.8', + version: '2.20.9', 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 8a1693019..7df7e2e5e 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -80,6 +80,7 @@ from documents.parsers import is_mime_type_supported from documents.permissions import get_document_count_filter_for_user from documents.permissions import get_groups_with_only_permission from documents.permissions import get_objects_for_user_owner_aware +from documents.permissions import has_perms_owner_aware from documents.permissions import set_permissions_for_object from documents.regex import validate_regex_pattern from documents.templating.filepath import validate_filepath_template_and_render @@ -2321,6 +2322,17 @@ class ShareLinkSerializer(OwnedObjectSerializer): validated_data["slug"] = get_random_string(50) return super().create(validated_data) + def validate_document(self, document): + if self.user is not None and has_perms_owner_aware( + self.user, + "view_document", + document, + ): + return document + raise PermissionDenied( + _("Insufficient permissions."), + ) + class ShareLinkBundleSerializer(OwnedObjectSerializer): document_ids = serializers.ListField( diff --git a/src/documents/tests/test_api_bulk_edit.py b/src/documents/tests/test_api_bulk_edit.py index deb1d5586..81d972bd4 100644 --- a/src/documents/tests/test_api_bulk_edit.py +++ b/src/documents/tests/test_api_bulk_edit.py @@ -773,6 +773,22 @@ class TestBulkEditAPI(DirectoriesMixin, APITestCase): ], ) + def test_api_selection_data_requires_view_permission(self): + self.doc2.owner = self.user + self.doc2.save() + + user1 = User.objects.create(username="user1") + self.client.force_authenticate(user=user1) + + response = self.client.post( + "/api/documents/selection_data/", + json.dumps({"documents": [self.doc2.id]}), + content_type="application/json", + ) + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(response.content, b"Insufficient permissions") + @mock.patch("documents.serialisers.bulk_edit.set_permissions") def test_set_permissions(self, m) -> None: self.setup_mock(m, "set_permissions") diff --git a/src/documents/tests/test_api_documents.py b/src/documents/tests/test_api_documents.py index e8e3c407d..2d3404b28 100644 --- a/src/documents/tests/test_api_documents.py +++ b/src/documents/tests/test_api_documents.py @@ -2955,6 +2955,54 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): ) self.assertEqual(resp.status_code, status.HTTP_200_OK) + def test_create_share_link_requires_view_permission_for_document(self): + """ + GIVEN: + - A user with add_sharelink but without view permission on a document + WHEN: + - API request is made to create a share link for that document + THEN: + - Share link creation is denied until view permission is granted + """ + user1 = User.objects.create_user(username="test1") + user1.user_permissions.add(*Permission.objects.filter(codename="add_sharelink")) + user1.save() + + user2 = User.objects.create_user(username="test2") + user2.save() + + doc = Document.objects.create( + title="test", + mime_type="application/pdf", + content="this is a document which will be protected", + owner=user2, + ) + + self.client.force_authenticate(user1) + + create_resp = self.client.post( + "/api/share_links/", + data={ + "document": doc.pk, + "file_version": "original", + }, + format="json", + ) + self.assertEqual(create_resp.status_code, status.HTTP_403_FORBIDDEN) + + assign_perm("view_document", user1, doc) + + create_resp = self.client.post( + "/api/share_links/", + data={ + "document": doc.pk, + "file_version": "original", + }, + format="json", + ) + self.assertEqual(create_resp.status_code, status.HTTP_201_CREATED) + self.assertEqual(create_resp.data["document"], doc.pk) + def test_next_asn(self) -> None: """ GIVEN: diff --git a/src/documents/views.py b/src/documents/views.py index 546da6e83..27d343212 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2434,6 +2434,13 @@ class SelectionDataView(GenericAPIView): serializer.is_valid(raise_exception=True) ids = serializer.validated_data.get("documents") + permitted_documents = get_objects_for_user_owner_aware( + request.user, + "documents.view_document", + Document, + ) + if permitted_documents.filter(pk__in=ids).count() != len(ids): + return HttpResponseForbidden("Insufficient permissions") correspondents = Correspondent.objects.annotate( document_count=Count( diff --git a/src/paperless/version.py b/src/paperless/version.py index a2f677230..ab46b36db 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, 8) +__version__: Final[tuple[int, int, int]] = (2, 20, 9) # 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 add260533..78d957ad8 100644 --- a/uv.lock +++ b/uv.lock @@ -3033,7 +3033,7 @@ wheels = [ [[package]] name = "paperless-ngx" -version = "2.20.8" +version = "2.20.9" source = { virtual = "." } dependencies = [ { name = "azure-ai-documentintelligence", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },