Skip to content

Commit 7616ea8

Browse files
committed
♻️(backend) open tree endpoint to deleted documents only for owners
The tree endpoint will now return a result only for owners. For other users the endpoint still returns a 403. Also, the endpoint does look for ancestors anymore, it only stay on the current document.
1 parent 9f0cd00 commit 7616ea8

File tree

2 files changed

+88
-21
lines changed

2 files changed

+88
-21
lines changed

src/backend/core/api/viewsets.py

Lines changed: 35 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -851,33 +851,47 @@ def tree(self, request, pk, *args, **kwargs):
851851

852852
try:
853853
current_document = (
854-
self.queryset.select_related(None).only("depth", "path").get(pk=pk)
854+
self.queryset.select_related(None)
855+
.only("depth", "path", "ancestors_deleted_at")
856+
.get(pk=pk)
855857
)
856858
except models.Document.DoesNotExist as excpt:
857859
raise drf.exceptions.NotFound() from excpt
858860

859-
ancestors = (
860-
(
861-
current_document.get_ancestors()
862-
| self.queryset.select_related(None).filter(pk=pk)
863-
)
864-
.filter(ancestors_deleted_at__isnull=True)
865-
.order_by("path")
866-
)
861+
is_deleted = current_document.ancestors_deleted_at is not None
867862

868-
# Get the highest readable ancestor
869-
highest_readable = (
870-
ancestors.select_related(None)
871-
.readable_per_se(request.user)
872-
.only("depth", "path")
873-
.first()
874-
)
875-
if highest_readable is None:
876-
raise (
877-
drf.exceptions.PermissionDenied()
878-
if request.user.is_authenticated
879-
else drf.exceptions.NotAuthenticated()
863+
if is_deleted:
864+
if current_document.get_role(user) != models.RoleChoices.OWNER:
865+
raise (
866+
drf.exceptions.PermissionDenied()
867+
if request.user.is_authenticated
868+
else drf.exceptions.NotAuthenticated()
869+
)
870+
highest_readable = current_document
871+
ancestors = self.queryset.select_related(None).filter(pk=pk)
872+
else:
873+
ancestors = (
874+
(
875+
current_document.get_ancestors()
876+
| self.queryset.select_related(None).filter(pk=pk)
877+
)
878+
.filter(ancestors_deleted_at__isnull=True)
879+
.order_by("path")
880880
)
881+
# Get the highest readable ancestor
882+
highest_readable = (
883+
ancestors.select_related(None)
884+
.readable_per_se(request.user)
885+
.only("depth", "path")
886+
.first()
887+
)
888+
889+
if highest_readable is None:
890+
raise (
891+
drf.exceptions.PermissionDenied()
892+
if request.user.is_authenticated
893+
else drf.exceptions.NotAuthenticated()
894+
)
881895
paths_links_mapping = {}
882896
ancestors_links = []
883897
children_clause = db.Q()

src/backend/core/tests/documents/test_api_documents_tree.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1205,3 +1205,56 @@ def test_api_documents_tree_list_authenticated_related_team_members(
12051205
"updated_at": parent.updated_at.isoformat().replace("+00:00", "Z"),
12061206
"user_role": access.role,
12071207
}
1208+
1209+
1210+
def test_api_documents_tree_list_deleted_document():
1211+
"""
1212+
Tree of a deleted document should only be accessible to the owner.
1213+
"""
1214+
user = factories.UserFactory()
1215+
client = APIClient()
1216+
client.force_login(user)
1217+
1218+
parent = factories.DocumentFactory(link_reach="public")
1219+
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
1220+
factories.DocumentFactory(link_reach="public", parent=document)
1221+
1222+
document.soft_delete()
1223+
1224+
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
1225+
assert response.status_code == 403
1226+
1227+
1228+
def test_api_documents_tree_list_deleted_document_owner(django_assert_num_queries):
1229+
"""
1230+
Tree of a deleted document should only be accessible to the owner.
1231+
"""
1232+
user = factories.UserFactory()
1233+
client = APIClient()
1234+
client.force_login(user)
1235+
1236+
parent = factories.DocumentFactory(link_reach="public", users=[(user, "owner")])
1237+
document, _ = factories.DocumentFactory.create_batch(2, parent=parent)
1238+
child = factories.DocumentFactory(parent=document)
1239+
1240+
document.soft_delete()
1241+
document.refresh_from_db()
1242+
child.refresh_from_db()
1243+
1244+
with django_assert_num_queries(9):
1245+
client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
1246+
1247+
with django_assert_num_queries(5):
1248+
response = client.get(f"/api/v1.0/documents/{document.id!s}/tree/")
1249+
1250+
assert response.status_code == 200
1251+
content = response.json()
1252+
assert content["id"] == str(document.id)
1253+
assert content["deleted_at"] == document.deleted_at.isoformat().replace(
1254+
"+00:00", "Z"
1255+
)
1256+
assert len(content["children"]) == 1
1257+
assert content["children"][0]["id"] == str(child.id)
1258+
assert content["children"][0][
1259+
"deleted_at"
1260+
] == child.ancestors_deleted_at.isoformat().replace("+00:00", "Z")

0 commit comments

Comments
 (0)