mirror of
https://github.com/suitenumerique/drive.git
synced 2026-09-28 20:45:13 +02:00
✨(backend) exclude shortcuts from search, export and indexing
Shortcuts are tree entries, not content: they never match a search, never reach the search index, and leave no entry in an exported archive. The target itself is indexed and exported through its own root, so users excluded from a restricted folder cannot find its content through search or an ancestor export.
This commit is contained in:
@@ -1433,6 +1433,9 @@ class ItemViewSet(
|
||||
queryset = self._filter_suspicious_items(queryset, user)
|
||||
queryset = self._exclude_pending_items(queryset)
|
||||
|
||||
# Shortcuts are tree entries, not searchable content
|
||||
queryset = queryset.exclude(type=models.ItemTypeChoices.SHORTCUT)
|
||||
|
||||
queryset = queryset.annotate_is_favorite(user)
|
||||
|
||||
if workspace:
|
||||
|
||||
@@ -43,6 +43,10 @@ def export_descendants(folder):
|
||||
|
||||
relative_paths = {str(folder.path): ""}
|
||||
for descendant in descendants:
|
||||
# Shortcuts are tree entries: their target lives in another subtree
|
||||
if descendant.type == models.ItemTypeChoices.SHORTCUT:
|
||||
continue
|
||||
|
||||
parent_key = str(descendant.path).rsplit(".", 1)[0]
|
||||
parent_relative = relative_paths.get(parent_key)
|
||||
if parent_relative is None:
|
||||
|
||||
@@ -196,6 +196,8 @@ class BaseItemIndexer(ABC):
|
||||
queryset = queryset or models.Item.objects.filter(
|
||||
main_workspace=False,
|
||||
)
|
||||
# Shortcuts are tree entries, not indexable content
|
||||
queryset = queryset.exclude(type=models.ItemTypeChoices.SHORTCUT)
|
||||
queryset = queryset.order_by("id")
|
||||
|
||||
while True:
|
||||
|
||||
@@ -260,6 +260,32 @@ def test_api_items_export_file_missing_from_storage():
|
||||
assert archive.read("gone.txt") == b""
|
||||
|
||||
|
||||
def test_api_items_export_skips_shortcuts():
|
||||
"""Shortcuts leave no entry in the exported archive."""
|
||||
user = factories.UserFactory()
|
||||
folder = factories.ItemFactory(
|
||||
type=models.ItemTypeChoices.FOLDER,
|
||||
users=[(user, models.RoleChoices.OWNER)],
|
||||
)
|
||||
factories.ItemFactory(
|
||||
parent=folder,
|
||||
type=models.ItemTypeChoices.FILE,
|
||||
update_upload_state=models.ItemUploadStateChoices.READY,
|
||||
upload_bytes=b"kept",
|
||||
upload_bytes__filename="kept.txt",
|
||||
)
|
||||
restricted = factories.ItemFactory(parent=folder, type=models.ItemTypeChoices.FOLDER)
|
||||
restricted.restrict(factories.UserFactory())
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get(f"/api/v1.0/items/{folder.pk}/export/")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert _zip_names(response) == ["kept.txt"]
|
||||
|
||||
|
||||
def test_api_items_export_empty_folder():
|
||||
"""Exporting an empty folder returns an empty zip archive."""
|
||||
user = factories.UserFactory()
|
||||
|
||||
@@ -197,6 +197,27 @@ def test_api_items_shortcuts_children_list_constant_queries():
|
||||
assert len(many) == len(single)
|
||||
|
||||
|
||||
def test_api_items_shortcuts_excluded_from_search():
|
||||
"""Shortcuts never show up as search results."""
|
||||
user = factories.UserFactory()
|
||||
parent = factories.ItemFactory(
|
||||
type=models.ItemTypeChoices.FOLDER,
|
||||
users=[(user, "owner")],
|
||||
)
|
||||
folder = _create_restricted_folder(parent, factories.UserFactory())
|
||||
shortcut = folder.shortcut
|
||||
shortcut.title = "shortcut"
|
||||
shortcut.save()
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(user)
|
||||
|
||||
response = client.get("/api/v1.0/items/search/", {"title": "shortcut"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["results"] == []
|
||||
|
||||
|
||||
def test_api_items_shortcuts_tree_includes_shortcuts():
|
||||
"""The tree endpoint includes shortcut entries so excluded users see them."""
|
||||
parent_owner = factories.UserFactory()
|
||||
|
||||
@@ -484,6 +484,22 @@ def test_services_search_indexers_index_errors(indexer_settings):
|
||||
indexer.index()
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("indexer_settings")
|
||||
@patch.object(SearchIndexer, "push")
|
||||
def test_services_search_indexers_index_skips_shortcuts(mock_push):
|
||||
"""Shortcuts are never sent to the search index."""
|
||||
user = factories.UserFactory()
|
||||
parent = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
|
||||
folder = factories.ItemFactory(parent=parent, type=models.ItemTypeChoices.FOLDER)
|
||||
folder = folder.restrict(user)
|
||||
|
||||
count = SearchIndexer().index()
|
||||
|
||||
assert count == 2
|
||||
indexed_ids = {doc["id"] for call in mock_push.call_args_list for doc in call.args[0]}
|
||||
assert indexed_ids == {str(parent.id), str(folder.id)}
|
||||
|
||||
|
||||
@patch.object(SearchIndexer, "push")
|
||||
def test_services_search_indexers_batches_pass_only_batch_accesses(mock_push, indexer_settings):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user