diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 1455f216..2cd45e62 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -851,33 +851,47 @@ class DocumentViewSet( try: current_document = ( - self.queryset.select_related(None).only("depth", "path").get(pk=pk) + self.queryset.select_related(None) + .only("depth", "path", "ancestors_deleted_at") + .get(pk=pk) ) except models.Document.DoesNotExist as excpt: raise drf.exceptions.NotFound() from excpt - ancestors = ( - ( - current_document.get_ancestors() - | self.queryset.select_related(None).filter(pk=pk) - ) - .filter(ancestors_deleted_at__isnull=True) - .order_by("path") - ) + is_deleted = current_document.ancestors_deleted_at is not None - # Get the highest readable ancestor - highest_readable = ( - ancestors.select_related(None) - .readable_per_se(request.user) - .only("depth", "path") - .first() - ) - if highest_readable is None: - raise ( - drf.exceptions.PermissionDenied() - if request.user.is_authenticated - else drf.exceptions.NotAuthenticated() + if is_deleted: + if current_document.get_role(user) != models.RoleChoices.OWNER: + raise ( + drf.exceptions.PermissionDenied() + if request.user.is_authenticated + else drf.exceptions.NotAuthenticated() + ) + highest_readable = current_document + ancestors = self.queryset.select_related(None).filter(pk=pk) + else: + ancestors = ( + ( + current_document.get_ancestors() + | self.queryset.select_related(None).filter(pk=pk) + ) + .filter(ancestors_deleted_at__isnull=True) + .order_by("path") ) + # Get the highest readable ancestor + highest_readable = ( + ancestors.select_related(None) + .readable_per_se(request.user) + .only("depth", "path") + .first() + ) + + if highest_readable is None: + raise ( + drf.exceptions.PermissionDenied() + if request.user.is_authenticated + else drf.exceptions.NotAuthenticated() + ) paths_links_mapping = {} ancestors_links = [] children_clause = db.Q() diff --git a/src/backend/core/tests/documents/test_api_documents_tree.py b/src/backend/core/tests/documents/test_api_documents_tree.py index bf0221e5..c86eebc1 100644 --- a/src/backend/core/tests/documents/test_api_documents_tree.py +++ b/src/backend/core/tests/documents/test_api_documents_tree.py @@ -1205,3 +1205,56 @@ def test_api_documents_tree_list_authenticated_related_team_members( "updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"), "user_role": access.role, } + + +def test_api_documents_tree_list_deleted_document(): + """ + Tree of a deleted document should only be accessible to the owner. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + parent = factories.DocumentFactory(link_reach="public") + document, _ = factories.DocumentFactory.create_batch(2, parent=parent) + factories.DocumentFactory(link_reach="public", parent=document) + + document.soft_delete() + + response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") + assert response.status_code == 403 + + +def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries): + """ + Tree of a deleted document should only be accessible to the owner. + """ + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")]) + document, _ = factories.DocumentFactory.create_batch(2, parent=parent) + child = factories.DocumentFactory(parent=document) + + document.soft_delete() + document.refresh_from_db() + child.refresh_from_db() + + with django_assert_num_queries(9): + client.get(f"/api/v1.0/documents/{document.id!s}/tree/") + + with django_assert_num_queries(5): + response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/") + + assert response.status_code == 200 + content = response.json() + assert content["id"] == str(document.id) + assert content["deleted_at"] == document.deleted_at.isoformat().replace( + "+00:00", "Z" + ) + assert len(content["children"]) == 1 + assert content["children"][0]["id"] == str(child.id) + assert content["children"][0][ + "deleted_at" + ] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z")