mirror of
https://github.com/suitenumerique/drive.git
synced 2026-08-17 20:15:40 +02:00
✨(backend) detach restricted folders by deleting their shortcut
Deleting a shortcut removes the entry from the containing folder without trashing anything. An owner excluded from the target acts on the container only and can neither destroy, declassify nor read it: the folder keeps its accesses, stays restricted, and surfaces in its members' top-level listing.
This commit is contained in:
@@ -689,7 +689,10 @@ class ItemViewSet(
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Override to implement a soft delete instead of dumping the record in database."""
|
||||
instance.soft_delete()
|
||||
if instance.type == models.ItemTypeChoices.SHORTCUT:
|
||||
instance.detach()
|
||||
else:
|
||||
instance.soft_delete()
|
||||
|
||||
def perform_update(self, serializer):
|
||||
"""Override to check if a file is renamed in order to rename file on storage."""
|
||||
|
||||
@@ -1683,6 +1683,20 @@ class Item(TreeModel, BaseModel):
|
||||
self.link_reach = None
|
||||
self.save(update_fields=["link_reach"])
|
||||
|
||||
def detach(self):
|
||||
"""Delete this shortcut row, leaving its restricted target untouched."""
|
||||
if self.type != ItemTypeChoices.SHORTCUT:
|
||||
raise ValidationError(
|
||||
{
|
||||
"type": ValidationError(
|
||||
_("Only shortcuts can be detached"),
|
||||
code="item_detach_not_a_shortcut",
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
self._meta.model.objects.filter(pk=self.pk).delete()
|
||||
|
||||
@transaction.atomic
|
||||
def unrestrict(self):
|
||||
"""Lift restriction and reattach the folder at its shortcut location."""
|
||||
|
||||
@@ -124,6 +124,49 @@ def test_api_items_shortcuts_non_shortcut_target_is_none():
|
||||
assert response.json()["target"] is None
|
||||
|
||||
|
||||
def test_api_items_shortcuts_delete_detaches_the_target():
|
||||
"""Deleting a shortcut detaches the restricted folder without touching it."""
|
||||
parent_owner = factories.UserFactory()
|
||||
owner = factories.UserFactory()
|
||||
parent = factories.ItemFactory(
|
||||
type=models.ItemTypeChoices.FOLDER,
|
||||
users=[(parent_owner, "owner")],
|
||||
)
|
||||
folder = _create_restricted_folder(parent, owner)
|
||||
shortcut = folder.shortcut
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(parent_owner)
|
||||
|
||||
response = client.delete(f"/api/v1.0/items/{shortcut.id!s}/")
|
||||
|
||||
assert response.status_code == 204
|
||||
assert not models.Item.objects.filter(pk=shortcut.pk).exists()
|
||||
folder.refresh_from_db()
|
||||
assert folder.is_restricted is True
|
||||
assert folder.deleted_at is None
|
||||
assert models.ItemAccess.objects.filter(item=folder, user=owner, role="owner").exists()
|
||||
|
||||
|
||||
def test_api_items_shortcuts_delete_forbidden_for_reader():
|
||||
"""A reader of the containing folder cannot detach a shortcut."""
|
||||
reader = factories.UserFactory()
|
||||
parent = factories.ItemFactory(
|
||||
type=models.ItemTypeChoices.FOLDER,
|
||||
users=[(reader, "reader")],
|
||||
)
|
||||
folder = _create_restricted_folder(parent, factories.UserFactory())
|
||||
shortcut = folder.shortcut
|
||||
|
||||
client = APIClient()
|
||||
client.force_login(reader)
|
||||
|
||||
response = client.delete(f"/api/v1.0/items/{shortcut.id!s}/")
|
||||
|
||||
assert response.status_code == 403
|
||||
assert models.Item.objects.filter(pk=shortcut.pk).exists()
|
||||
|
||||
|
||||
def test_api_items_shortcuts_children_list_constant_queries():
|
||||
"""The number of queries does not grow with the number of shortcuts listed."""
|
||||
parent_owner = factories.UserFactory()
|
||||
|
||||
@@ -63,6 +63,31 @@ def test_models_items_shortcuts_move_rejects_own_target_subtree():
|
||||
shortcut.move(folder)
|
||||
|
||||
|
||||
def test_models_items_shortcuts_detach_deletes_the_row():
|
||||
"""Detaching a shortcut deletes its row and leaves the target untouched."""
|
||||
user = factories.UserFactory()
|
||||
parent = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
|
||||
folder = factories.ItemFactory(parent=parent, type=models.ItemTypeChoices.FOLDER)
|
||||
folder = folder.restrict(user)
|
||||
shortcut = folder.shortcut
|
||||
|
||||
shortcut.detach()
|
||||
|
||||
assert not models.Item.objects.filter(pk=shortcut.pk).exists()
|
||||
folder.refresh_from_db()
|
||||
assert folder.is_restricted is True
|
||||
assert str(folder.path) == str(folder.id)
|
||||
assert models.ItemAccess.objects.filter(item=folder, user=user, role="owner").exists()
|
||||
|
||||
|
||||
def test_models_items_shortcuts_detach_rejects_other_types():
|
||||
"""Only shortcuts can be detached."""
|
||||
folder = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
|
||||
|
||||
with pytest.raises(ValidationError, match="Only shortcuts can be detached"):
|
||||
folder.detach()
|
||||
|
||||
|
||||
def test_models_items_shortcuts_item_factory_never_generates_shortcuts():
|
||||
"""The generic item factory should only draw folder and file types."""
|
||||
types = {factories.ItemFactory().type for _ in range(20)}
|
||||
|
||||
Reference in New Issue
Block a user