From 2ab2064a72ca9ebb840c0dcd1627502e4d6fcaa1 Mon Sep 17 00:00:00 2001 From: Will Ho Date: Fri, 28 Apr 2023 02:08:55 +0800 Subject: [PATCH 01/23] Fix ALLOWED_HOSTS logic being overwritten when * is set --- src/paperless/settings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index 0301fffad..ce498ec58 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -422,11 +422,12 @@ if _paperless_url: _paperless_uri = urlparse(_paperless_url) CSRF_TRUSTED_ORIGINS.append(_paperless_url) CORS_ALLOWED_ORIGINS.append(_paperless_url) - if ["*"] != ALLOWED_HOSTS: + +if ["*"] != ALLOWED_HOSTS: + # always allow localhost. Necessary e.g. for healthcheck in docker. + ALLOWED_HOSTS.append(["localhost"]) + if _paperless_url: ALLOWED_HOSTS.append(_paperless_uri.hostname) - else: - # always allow localhost. Necessary e.g. for healthcheck in docker. - ALLOWED_HOSTS = [_paperless_uri.hostname] + ["localhost"] # For use with trusted proxies TRUSTED_PROXIES = __get_list("PAPERLESS_TRUSTED_PROXIES") From c25698dfa7fe215904c3fa38a2a7a7fd6a0e777a Mon Sep 17 00:00:00 2001 From: Will Ho Date: Fri, 28 Apr 2023 02:09:26 +0800 Subject: [PATCH 02/23] Update docs to reflect localhost being always included in ALLOWED_HOSTS --- docs/configuration.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index bde170e5c..cb5af9d86 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -322,8 +322,7 @@ You can read more about this in [the Django project's documentation](https://doc Can also be set using PAPERLESS_URL (see above). - If manually set, please remember to include "localhost". Otherwise - docker healthcheck will fail. + "localhost" is always allowed for docker healthcheck Defaults to "\*", which is all hosts. From 1d5dbc454ddd0436a36d9dc47e6c96763d48db33 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 27 Apr 2023 11:43:59 -0700 Subject: [PATCH 03/23] Update version string for dev --- src-ui/src/environments/environment.prod.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-ui/src/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts index db7622932..fb2153883 100644 --- a/src-ui/src/environments/environment.prod.ts +++ b/src-ui/src/environments/environment.prod.ts @@ -5,7 +5,7 @@ export const environment = { apiBaseUrl: document.baseURI + 'api/', apiVersion: '2', appTitle: 'Paperless-ngx', - version: '1.14.2', + version: '1.14.2-dev', webSocketHost: window.location.host, webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:', webSocketBaseUrl: base_url.pathname + 'ws/', From 83344f748f03ae27c5482833f9a4ff56f3325b90 Mon Sep 17 00:00:00 2001 From: Will Ho Date: Fri, 28 Apr 2023 03:28:19 +0800 Subject: [PATCH 04/23] Fix appends to ALLOWED_HOSTS should be string instead of list --- src/paperless/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/paperless/settings.py b/src/paperless/settings.py index ce498ec58..122806516 100644 --- a/src/paperless/settings.py +++ b/src/paperless/settings.py @@ -425,7 +425,7 @@ if _paperless_url: if ["*"] != ALLOWED_HOSTS: # always allow localhost. Necessary e.g. for healthcheck in docker. - ALLOWED_HOSTS.append(["localhost"]) + ALLOWED_HOSTS.append("localhost") if _paperless_url: ALLOWED_HOSTS.append(_paperless_uri.hostname) From e275a2736af18412cab59feb9f6a304b0bbdb697 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 27 Apr 2023 15:00:03 -0700 Subject: [PATCH 05/23] Respect superuser for advanced queries, test coverage for object perms --- src/documents/index.py | 16 +++--- src/documents/tests/test_api.py | 89 ++++++++++++++++++++++++++++++++- src/documents/views.py | 3 ++ 3 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/documents/index.py b/src/documents/index.py index 973c99f4d..6aef2c047 100644 --- a/src/documents/index.py +++ b/src/documents/index.py @@ -225,15 +225,19 @@ class DelayedQuery: user_criterias = [query.Term("has_owner", False)] if "user" in self.query_params: - user_criterias.append(query.Term("owner_id", self.query_params["user"])) - user_criterias.append( - query.Term("viewer_id", str(self.query_params["user"])), - ) + if self.query_params["is_superuser"]: # superusers see all docs + user_criterias = [] + else: + user_criterias.append(query.Term("owner_id", self.query_params["user"])) + user_criterias.append( + query.Term("viewer_id", str(self.query_params["user"])), + ) if len(criterias) > 0: - criterias.append(query.Or(user_criterias)) + if len(user_criterias) > 0: + criterias.append(query.Or(user_criterias)) return query.And(criterias) else: - return query.Or(user_criterias) + return query.Or(user_criterias) if len(user_criterias) > 0 else None def _get_query_sortedby(self): if "ordering" not in self.query_params: diff --git a/src/documents/tests/test_api.py b/src/documents/tests/test_api.py index d6158cd7d..b9989ee86 100644 --- a/src/documents/tests/test_api.py +++ b/src/documents/tests/test_api.py @@ -27,6 +27,7 @@ from django.contrib.auth.models import Permission from django.contrib.auth.models import User from django.test import override_settings from django.utils import timezone +from guardian.shortcuts import assign_perm from rest_framework import status from rest_framework.test import APITestCase from whoosh.writing import AsyncWriter @@ -253,8 +254,6 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): response = self.client.get(f"/api/documents/{doc.pk}/thumb/") self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - from guardian.shortcuts import assign_perm - assign_perm("view_document", user2, doc) response = self.client.get(f"/api/documents/{doc.pk}/download/") @@ -1064,6 +1063,92 @@ class TestDocumentApi(DirectoriesMixin, DocumentConsumeDelayMixin, APITestCase): ), ) + def test_search_filtering_respect_owner(self): + """ + GIVEN: + - Documents with owners set & without + WHEN: + - API reuqest for advanced query (search) is made by non-superuser + - API reuqest for advanced query (search) is made by superuser + THEN: + - Only owned docs are returned for regular users + - All docs are returned for superuser + """ + superuser = User.objects.create_superuser("superuser") + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + + Document.objects.create(checksum="1", content="test 1", owner=u1) + Document.objects.create(checksum="2", content="test 2", owner=u2) + Document.objects.create(checksum="3", content="test 3", owner=u2) + Document.objects.create(checksum="4", content="test 4") + + with AsyncWriter(index.open_index()) as writer: + for doc in Document.objects.all(): + index.update_document(writer, doc) + + self.client.force_authenticate(user=u1) + r = self.client.get("/api/documents/?query=test") + self.assertEqual(r.data["count"], 2) + r = self.client.get("/api/documents/?query=test&document_type__id__none=1") + self.assertEqual(r.data["count"], 2) + + self.client.force_authenticate(user=u2) + r = self.client.get("/api/documents/?query=test") + self.assertEqual(r.data["count"], 3) + r = self.client.get("/api/documents/?query=test&document_type__id__none=1") + self.assertEqual(r.data["count"], 3) + + self.client.force_authenticate(user=superuser) + r = self.client.get("/api/documents/?query=test") + self.assertEqual(r.data["count"], 4) + r = self.client.get("/api/documents/?query=test&document_type__id__none=1") + self.assertEqual(r.data["count"], 4) + + def test_search_filtering_with_object_perms(self): + """ + GIVEN: + - Documents with granted view permissions to others + WHEN: + - API reuqest for advanced query (search) is made by user + THEN: + - Only docs with granted view permissions are returned + """ + u1 = User.objects.create_user("user1") + u2 = User.objects.create_user("user2") + u1.user_permissions.add(*Permission.objects.filter(codename="view_document")) + u2.user_permissions.add(*Permission.objects.filter(codename="view_document")) + + Document.objects.create(checksum="1", content="test 1", owner=u1) + d2 = Document.objects.create(checksum="2", content="test 2", owner=u2) + d3 = Document.objects.create(checksum="3", content="test 3", owner=u2) + Document.objects.create(checksum="4", content="test 4") + + with AsyncWriter(index.open_index()) as writer: + for doc in Document.objects.all(): + index.update_document(writer, doc) + + self.client.force_authenticate(user=u1) + r = self.client.get("/api/documents/?query=test") + self.assertEqual(r.data["count"], 2) + r = self.client.get("/api/documents/?query=test&document_type__id__none=1") + self.assertEqual(r.data["count"], 2) + + assign_perm("view_document", u1, d2) + assign_perm("view_document", u1, d3) + + with AsyncWriter(index.open_index()) as writer: + for doc in [d2, d3]: + index.update_document(writer, doc) + + self.client.force_authenticate(user=u1) + r = self.client.get("/api/documents/?query=test") + self.assertEqual(r.data["count"], 4) + r = self.client.get("/api/documents/?query=test&document_type__id__none=1") + self.assertEqual(r.data["count"], 4) + def test_search_sorting(self): c1 = Correspondent.objects.create(name="corres Ax") c2 = Correspondent.objects.create(name="corres Cx") diff --git a/src/documents/views.py b/src/documents/views.py index 234c4dda1..0b450c3b3 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -604,6 +604,9 @@ class UnifiedSearchViewSet(DocumentViewSet): # pass user to query for perms self.request.query_params._mutable = True self.request.query_params["user"] = self.request.user.id + self.request.query_params[ + "is_superuser" + ] = self.request.user.is_superuser self.request.query_params._mutable = False if "query" in self.request.query_params: From bbfc244f1674417bfb15cbac4ea4d992baf3f5bd Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:51:09 -0700 Subject: [PATCH 06/23] Better keyboard nav for filter/edit dropdowns --- .../filterable-dropdown.component.html | 10 +- .../filterable-dropdown.component.ts | 93 ++++++++++++++++--- 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html index 5bf75d62d..57197d1ea 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.html @@ -1,4 +1,4 @@ -
+
-
- - +
+ + +