From 36dced1e56f2dd589719eab125755fdfe98b018c Mon Sep 17 00:00:00 2001 From: Amine BOUKERFA Date: Mon, 29 Jun 2026 15:04:04 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B(backend)=20prevent=20owner=20from?= =?UTF-8?q?=20leaving=20a=20soft-deleted=20document?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owners could leave trashed documents via the leave endpoint, deleting their `DocumentAccess` and all subtree access. When the last owner leaves a document it creates an orphan as no owners remains in the doc. Signed-off-by: BOUKERFA Mohamed El Amine --- CHANGELOG.md | 1 + src/backend/core/models.py | 14 +++++--- .../documents/test_api_documents_leave.py | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0cc6445c5..f2c6db545 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to ### Fixed +- 🐛(backend) prevent owner from leaving a soft-deleted document - 🐛(frontend) fix removed item in the tree #2420 - 🐛(frontend) fix service worker causing reload on tab focus #2454 diff --git a/src/backend/core/models.py b/src/backend/core/models.py index e2cd88de2..8cf912c7d 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -1326,11 +1326,15 @@ class Document(MP_Node, BaseModel): ) and not is_deleted # compute can_leave - # A user can leave a document if it has non privileged role on the document or it has - # access to it with a link_trace - can_leave = user.is_authenticated and ( - (has_access_role and not is_owner_or_admin) - or (not has_access_role and self.has_link_trace(user)) + # An authenticated user can leave a document if it has a non + # privileged role on the document or access to it with a link_trace + can_leave = ( + user.is_authenticated + and not is_deleted + and ( + (has_access_role and not is_owner_or_admin) + or (not has_access_role and self.has_link_trace(user)) + ) ) link_select_options = LinkReachChoices.get_select_options( diff --git a/src/backend/core/tests/documents/test_api_documents_leave.py b/src/backend/core/tests/documents/test_api_documents_leave.py index 4f9144bd5..b371c6812 100644 --- a/src/backend/core/tests/documents/test_api_documents_leave.py +++ b/src/backend/core/tests/documents/test_api_documents_leave.py @@ -183,6 +183,39 @@ def test_api_documents_leave_connected_access_with_privileged_role_not_allowed( assert models.DocumentAccess.objects.count() == 5 +@pytest.mark.parametrize( + "link_reach", + models.LinkReachChoices.values, +) +def test_api_documents_leave_connected_owner_on_deleted_document_not_allowed( + link_reach, +): + """ + An owner can not leave a soft-deleted document, even when it also has a link_trace. + """ + user = factories.UserFactory() + + document = factories.DocumentFactory( + link_reach=link_reach, + link_traces=[user], + users=[(user, models.RoleChoices.OWNER)], + ) + document.soft_delete() + + assert models.DocumentAccess.objects.filter(document=document, user=user).exists() + assert models.LinkTrace.objects.filter(document=document, user=user).exists() + + client = APIClient() + client.force_login(user) + response = client.post(f"/api/v1.0/documents/{document.id!s}/leave/") + + assert response.status_code == status.HTTP_403_FORBIDDEN + + # The owner kept its access and link_trace: it did not leave the trashed document. + assert models.DocumentAccess.objects.filter(document=document, user=user).exists() + assert models.LinkTrace.objects.filter(document=document, user=user).exists() + + @pytest.mark.parametrize( "link_reach", models.LinkReachChoices.values,