(backend) hide reachable restricted roots from the top-level listing

A restricted folder lives at the tree root but its members reach it
through the shortcut when they can open the containing folder. The
listing hides the root in that case so the folder shows up in a
single location, and keeps it for members without container access.
This commit is contained in:
Nicolas Clerc
2026-07-28 16:23:29 +02:00
parent 8b2ae71043
commit 6aae4bde88
2 changed files with 208 additions and 0 deletions
+26
View File
@@ -759,6 +759,32 @@ class ItemViewSet(
)
queryset = queryset.filter(path__in=root_paths)
# Hide restricted roots the user already reaches through a live
# shortcut, so the folder shows up in a single location
if user.is_authenticated:
reachable_shortcuts = models.Item.objects.filter(
type=models.ItemTypeChoices.SHORTCUT,
target_id=db.OuterRef("pk"),
ancestors_deleted_at__isnull=True,
).filter(
db.Exists(
models.ItemAccess.objects.filter(
db.Q(user=user) | db.Q(team__in=user.teams),
item__path__ancestors=db.OuterRef("path"),
)
)
| db.Exists(
models.Item.objects.filter(
path__ancestors=db.OuterRef("path"),
link_reach__in=[
LinkReachChoices.PUBLIC,
LinkReachChoices.AUTHENTICATED,
],
)
)
)
queryset = queryset.exclude(db.Q(is_restricted=True) & db.Exists(reachable_shortcuts))
# Annotate the queryset with an attribute marking instances as highest ancestor
# in order to save some time while computing abilities in the instance
queryset = queryset.annotate(
@@ -0,0 +1,182 @@
"""Tests for restricted roots visibility in the items list API endpoint."""
import pytest
from rest_framework.test import APIClient
from core import factories, models
pytestmark = pytest.mark.django_db
def _create_restricted_folder(parent, user):
"""Create a folder under parent and restrict it as user."""
folder = factories.ItemFactory(parent=parent, type=models.ItemTypeChoices.FOLDER)
return folder.restrict(user)
def _listed_ids(client):
"""Return the ids listed on the user's root listing."""
response = client.get("/api/v1.0/items/")
assert response.status_code == 200
return {result["id"] for result in response.json()["results"]}
def test_api_items_list_restricted_hidden_when_shortcut_reachable():
"""A member reaching the live shortcut does not see the restricted root."""
user = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "owner")],
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
client = APIClient()
client.force_login(user)
listed = _listed_ids(client)
assert str(parent.id) in listed
assert str(folder.id) not in listed
def test_api_items_list_restricted_visible_without_container_access():
"""A member without access to the container sees the restricted root."""
user = factories.UserFactory()
parent = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
client = APIClient()
client.force_login(user)
listed = _listed_ids(client)
assert str(folder.id) in listed
assert str(parent.id) not in listed
def test_api_items_list_restricted_hidden_via_team_access(mock_user_teams):
"""A team access on the container hides the restricted root."""
user = factories.UserFactory()
mock_user_teams.return_value = ["lasuite"]
parent = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
factories.TeamItemAccessFactory(item=parent, team="lasuite", role="reader")
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
client = APIClient()
client.force_login(user)
assert str(folder.id) not in _listed_ids(client)
def test_api_items_list_restricted_follows_moved_shortcut():
"""Moving the shortcut out of reach makes the restricted root visible again."""
user = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "owner")],
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
other = factories.ItemFactory(type=models.ItemTypeChoices.FOLDER)
folder.shortcut.move(other)
client = APIClient()
client.force_login(user)
assert str(folder.id) in _listed_ids(client)
def test_api_items_list_restricted_visible_when_shortcut_deleted():
"""Without a live shortcut the restricted root shows up for its members."""
user = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "owner")],
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
models.Item.objects.filter(target=folder).delete()
client = APIClient()
client.force_login(user)
assert str(folder.id) in _listed_ids(client)
def test_api_items_list_restricted_visible_when_shortcut_trashed():
"""A shortcut under a soft deleted ancestor does not hide the restricted root."""
user = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(user, "owner")],
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
parent.soft_delete()
client = APIClient()
client.force_login(user)
assert str(folder.id) in _listed_ids(client)
@pytest.mark.parametrize("reach", ["public", "authenticated"])
def test_api_items_list_restricted_hidden_via_link_on_container(reach):
"""An open link on the container makes the shortcut reachable and hides the root."""
user = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
link_reach=reach,
link_role="reader",
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
client = APIClient()
client.force_login(user)
assert str(folder.id) not in _listed_ids(client)
def test_api_items_list_restricted_hidden_via_inherited_link():
"""A link inherited from a grandparent counts to reach the shortcut."""
user = factories.UserFactory()
grandparent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
link_reach="public",
link_role="reader",
)
parent = factories.ItemFactory(
parent=grandparent,
type=models.ItemTypeChoices.FOLDER,
link_reach=None,
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=user, role="reader")
client = APIClient()
client.force_login(user)
assert str(folder.id) not in _listed_ids(client)
def test_api_items_list_restricted_differs_per_user():
"""The same restricted root is hidden or visible depending on the user."""
insider = factories.UserFactory()
outsider = factories.UserFactory()
parent = factories.ItemFactory(
type=models.ItemTypeChoices.FOLDER,
users=[(insider, "owner")],
)
folder = _create_restricted_folder(parent, factories.UserFactory())
factories.UserItemAccessFactory(item=folder, user=insider, role="reader")
factories.UserItemAccessFactory(item=folder, user=outsider, role="reader")
insider_client = APIClient()
insider_client.force_login(insider)
outsider_client = APIClient()
outsider_client.force_login(outsider)
assert str(folder.id) not in _listed_ids(insider_client)
assert str(folder.id) in _listed_ids(outsider_client)