From b3f7c9c1ea8fdbfa51b17a666538f988750005fb Mon Sep 17 00:00:00 2001 From: Nicolas Clerc Date: Mon, 27 Jul 2026 13:17:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(backend)=20normalize=20explicit=20lin?= =?UTF-8?q?k=20reach=20on=20restriction=20deactivation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reach kept from the restriction period is reset to inherit when the reattached parent already grants as much or more. A more open explicit reach survives, matching the role normalization rule. --- src/backend/core/models.py | 12 ++++++ .../tests/test_models_items_restricted.py | 43 ++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/backend/core/models.py b/src/backend/core/models.py index 65f2f1c9..c201b0a3 100644 --- a/src/backend/core/models.py +++ b/src/backend/core/models.py @@ -1672,6 +1672,17 @@ class Item(TreeModel, BaseModel): if redundant_ids: ItemAccess.objects.filter(id__in=redundant_ids).delete() + def _normalize_explicit_link_reach(self): + """Reset the link reach to inherit when inferior or equal to the inherited one.""" + # The cached ancestors definition predates the move, recompute it + self._ancestors_link_definition = None + inherited_reach = self.ancestors_link_definition["link_reach"] + if LinkReachChoices.get_priority(self.link_reach) <= LinkReachChoices.get_priority( + inherited_reach + ): + self.link_reach = None + self.save(update_fields=["link_reach"]) + @transaction.atomic def unrestrict(self): """Lift restriction and reattach the folder at its shortcut location.""" @@ -1706,6 +1717,7 @@ class Item(TreeModel, BaseModel): self.save(update_fields=["title"]) self.move(parent) self._normalize_explicit_accesses() + self._normalize_explicit_link_reach() self.invalidate_nb_accesses_cache() diff --git a/src/backend/core/tests/test_models_items_restricted.py b/src/backend/core/tests/test_models_items_restricted.py index 8e9f4e85..a43ac647 100644 --- a/src/backend/core/tests/test_models_items_restricted.py +++ b/src/backend/core/tests/test_models_items_restricted.py @@ -429,7 +429,48 @@ def test_models_items_restricted_unrestrict_keeps_access_without_inheritance(): assert models.ItemAccess.objects.filter(item=folder, user=user, role="owner").exists() -def test_models_items_restricted_deactivate_restriction_deduplicates_title(): +def test_models_items_restricted_unrestrict_resets_redundant_link_reach(): + """Deactivating restriction resets a link reach inferior or equal to inherited.""" + user = factories.UserFactory() + parent = factories.ItemFactory( + type=models.ItemTypeChoices.FOLDER, + link_reach=LinkReachChoices.PUBLIC, + link_role="reader", + ) + folder = factories.ItemFactory( + parent=parent, + type=models.ItemTypeChoices.FOLDER, + link_reach=None, + ) + folder = folder.restrict(user) + assert folder.link_reach == LinkReachChoices.RESTRICTED + + folder = folder.unrestrict() + + assert folder.link_reach is None + + +def test_models_items_restricted_unrestrict_keeps_superior_link_reach(): + """Deactivating restriction keeps a link reach superior to inherited.""" + user = factories.UserFactory() + parent = factories.ItemFactory( + type=models.ItemTypeChoices.FOLDER, + link_reach=LinkReachChoices.AUTHENTICATED, + link_role="reader", + ) + folder = factories.ItemFactory( + parent=parent, + type=models.ItemTypeChoices.FOLDER, + link_reach=LinkReachChoices.PUBLIC, + ) + folder = folder.restrict(user) + + folder = folder.unrestrict() + + assert folder.link_reach == LinkReachChoices.PUBLIC + + +def test_models_items_restricted_unrestrict_deduplicates_title(): """Deactivating restriction renames the folder when its title was reused.""" user = factories.UserFactory() parent = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)