Skip to content

Commit 5719722

Browse files
committed
♻️(backend) scope document search by document id instead of path
The search in a document tree was triggered by the usage of the document path. The path is something guessable by incrementing it you can discover public documents. We decided to change this to use the document id which is not guessable and prevent discovering public documents. Thanks to @maboukerfa for discovering it.
1 parent 6a84430 commit 5719722

5 files changed

Lines changed: 39 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to
2525
- 🚚(frontend) move Waffle to bottom left #2455
2626
- ♿️(frontend) remove redundant aria-label on table of contents links #2459
2727
- ♻️(core) fix typo in settings COLLABORATION_WS_NOT_CONNECTED_READY_ONLY #2481
28+
- ♻️(backend) scope document search by document id instead of path #2501
2829

2930
### Fixed
3031

src/backend/core/api/serializers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1009,4 +1009,4 @@ class SearchQueryParamDocumentSerializer(serializers.Serializer):
10091009
"""Serializer for fulltext search requests through Find application"""
10101010

10111011
q = serializers.CharField(required=True, allow_blank=True, trim_whitespace=True)
1012-
path = serializers.CharField(required=False, allow_blank=False)
1012+
document = serializers.UUIDField(required=False)

src/backend/core/api/viewsets.py

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1542,15 +1542,23 @@ def _search_using_indexer(indexer, request, params, search_type):
15421542
"""
15431543
queryset = models.Document.objects.all()
15441544

1545+
# The indexer filters descendants by path prefix, so resolve the document
1546+
# id to its path before querying it.
1547+
path = None
1548+
document_id = params.validated_data.get("document")
1549+
if document_id:
1550+
try:
1551+
path = models.Document.objects.get(pk=document_id).values_list(
1552+
"path", flat=True
1553+
)
1554+
except models.Document.DoesNotExist as exc:
1555+
raise drf.exceptions.NotFound("Document not found.") from exc
1556+
15451557
results = indexer.search(
15461558
q=params.validated_data["q"],
15471559
search_type=search_type,
15481560
token=request.session.get("oidc_access_token"),
1549-
path=(
1550-
params.validated_data["path"]
1551-
if "path" in params.validated_data
1552-
else None
1553-
),
1561+
path=path,
15541562
visited=get_visited_document_ids_of(queryset, request.user),
15551563
)
15561564

@@ -1618,7 +1626,7 @@ def _search_using_database(self, request, validated_data, *args, **kwargs):
16181626
Only searches in the title field of documents.
16191627
"""
16201628

1621-
if validated_data.get("path"):
1629+
if validated_data.get("document"):
16221630
return self._list_descendants(request, validated_data)
16231631

16241632
top_level_documents = self.get_queryset()
@@ -1676,22 +1684,22 @@ def _search_using_database(self, request, validated_data, *args, **kwargs):
16761684

16771685
def _list_descendants(self, request, validated_data):
16781686
"""
1679-
List all documents whose path starts with the provided path parameter.
1680-
Includes the parent document itself.
1681-
Used internally by the search endpoint when path filtering is requested.
1687+
List all documents descending from the document identified by the provided
1688+
document id. Includes the parent document itself.
1689+
Used internally by the search endpoint when document filtering is requested.
16821690
"""
16831691
# Get parent document without access filtering
1684-
parent_path = validated_data["path"]
1692+
document_id = validated_data["document"]
16851693
user = request.user
16861694
try:
16871695
parent = (
16881696
models.Document.objects.annotate_user_roles(user)
16891697
.annotate_is_favorite(user)
16901698
.annotate_user_has_link_trace(user)
1691-
.get(path=parent_path)
1699+
.get(pk=document_id)
16921700
)
16931701
except models.Document.DoesNotExist as exc:
1694-
raise drf.exceptions.NotFound("Document not found from path.") from exc
1702+
raise drf.exceptions.NotFound("Document not found.") from exc
16951703

16961704
abilities = parent.get_abilities(user)
16971705
if not abilities.get("search"):

src/backend/core/tests/documents/test_api_documents_search.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,15 +547,17 @@ def test_api_documents_search_indexer_crashes(
547547
parent = factories.DocumentFactory(title="parent", users=[user])
548548
q = "alpha"
549549
response = client.get(
550-
"/api/v1.0/documents/search/", data={"q": "alpha", "path": parent.path}
550+
"/api/v1.0/documents/search/", data={"q": "alpha", "document": parent.id}
551551
)
552552

553553
# the search endpoint did not crash
554554
assert response.status_code == 200
555555
# fallback on title_search
556556
assert mock_search_using_database.call_count == 1
557557
assert mock_search_using_database.call_args[0][0].GET.get("q") == q
558-
assert mock_search_using_database.call_args[0][0].GET.get("path") == parent.path
558+
assert mock_search_using_database.call_args[0][0].GET.get("document") == str(
559+
parent.id
560+
)
559561
assert response.json() == mocked_response
560562

561563

src/backend/core/tests/documents/test_api_documents_search_descendants.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
Tests for search API endpoint in impress's core app when indexer is not
3-
available and a path param is given.
3+
available and a document param is given.
44
"""
55
# pylint: disable=too-many-lines
66

@@ -34,7 +34,7 @@ def test_api_documents_search_descendants_list_anonymous_public_standalone():
3434
factories.UserDocumentAccessFactory(document=child1)
3535

3636
response = APIClient().get(
37-
"/api/v1.0/documents/search/", data={"q": "doc", "path": document.path}
37+
"/api/v1.0/documents/search/", data={"q": "doc", "document": document.id}
3838
)
3939

4040
assert response.status_code == 200
@@ -248,7 +248,7 @@ def test_api_documents_search_descendants_list_anonymous_public_parent():
248248
factories.UserDocumentAccessFactory(document=child1)
249249

250250
response = APIClient().get(
251-
"/api/v1.0/documents/search/", data={"q": "doc", "path": document.path}
251+
"/api/v1.0/documents/search/", data={"q": "doc", "document": document.id}
252252
)
253253

254254
assert response.status_code == 200
@@ -448,7 +448,7 @@ def test_api_documents_search_descendants_list_anonymous_restricted_or_authentic
448448
_grand_child = factories.DocumentFactory(title="grand child", parent=child)
449449

450450
response = APIClient().get(
451-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
451+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
452452
)
453453

454454
assert response.status_code == 403
@@ -478,7 +478,7 @@ def test_api_documents_search_descendants_list_authenticated_unrelated_public_or
478478
factories.UserDocumentAccessFactory(document=child1)
479479

480480
response = client.get(
481-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
481+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
482482
)
483483

484484
assert response.status_code == 200
@@ -669,7 +669,7 @@ def test_api_documents_search_descendants_list_authenticated_public_or_authentic
669669
factories.UserDocumentAccessFactory(document=child1)
670670

671671
response = client.get(
672-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
672+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
673673
)
674674

675675
assert response.status_code == 200
@@ -850,7 +850,7 @@ def test_api_documents_search_descendants_list_authenticated_unrelated_restricte
850850
factories.UserDocumentAccessFactory(document=child1)
851851

852852
response = client.get(
853-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
853+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
854854
)
855855

856856
assert response.status_code == 403
@@ -881,7 +881,7 @@ def test_api_documents_search_descendants_list_authenticated_related_direct():
881881
grand_child = factories.DocumentFactory(parent=child1, title="grand child")
882882

883883
response = client.get(
884-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
884+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
885885
)
886886
assert response.status_code == 200
887887
assert response.json() == {
@@ -1073,7 +1073,7 @@ def test_api_documents_search_descendants_list_authenticated_related_parent():
10731073
grand_child = factories.DocumentFactory(parent=child1, title="grand child")
10741074

10751075
response = client.get(
1076-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
1076+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
10771077
)
10781078
assert response.status_code == 200
10791079
assert response.json() == {
@@ -1252,7 +1252,7 @@ def test_api_documents_search_descendants_list_authenticated_related_child():
12521252
factories.UserDocumentAccessFactory(document=document)
12531253

12541254
response = client.get(
1255-
"/api/v1.0/documents/search/", data={"q": "doc", "path": document.path}
1255+
"/api/v1.0/documents/search/", data={"q": "doc", "document": document.id}
12561256
)
12571257
assert response.status_code == 403
12581258
assert response.json() == {
@@ -1279,7 +1279,7 @@ def test_api_documents_search_descendants_list_authenticated_related_team_none(
12791279
factories.TeamDocumentAccessFactory(document=document, team="myteam")
12801280

12811281
response = client.get(
1282-
"/api/v1.0/documents/search/", data={"q": "doc", "path": document.path}
1282+
"/api/v1.0/documents/search/", data={"q": "doc", "document": document.id}
12831283
)
12841284

12851285
assert response.status_code == 403
@@ -1310,7 +1310,7 @@ def test_api_documents_search_descendants_list_authenticated_related_team_member
13101310
access = factories.TeamDocumentAccessFactory(document=document, team="myteam")
13111311

13121312
response = client.get(
1313-
"/api/v1.0/documents/search/", data={"q": "child", "path": document.path}
1313+
"/api/v1.0/documents/search/", data={"q": "child", "document": document.id}
13141314
)
13151315

13161316
# pylint: disable=R0801
@@ -1509,7 +1509,7 @@ def test_api_documents_search_descendants_search_on_title(query, nb_results):
15091509

15101510
# Perform the search query
15111511
response = client.get(
1512-
"/api/v1.0/documents/search/", data={"q": query, "path": parent.path}
1512+
"/api/v1.0/documents/search/", data={"q": query, "document": parent.id}
15131513
)
15141514

15151515
assert response.status_code == 200

0 commit comments

Comments
 (0)