diff --git a/dojo/api_v3/subresources.py b/dojo/api_v3/subresources.py index 1978b852781..02288264cbf 100644 --- a/dojo/api_v3/subresources.py +++ b/dojo/api_v3/subresources.py @@ -58,6 +58,7 @@ from dojo.api_v3.refs import Ref, to_ref from dojo.authorization.authorization import user_has_permission from dojo.file_uploads.models import FileUpload +from dojo.notes.helper import visible_notes from dojo.notes.models import NoteHistory, Notes from dojo.utils import generate_file_response @@ -208,10 +209,7 @@ def build_notes_router( def list_notes(request: HttpRequest, parent_id: int): parent = _resolve_parent(request, get_parent_queryset, parent_label, parent_id) _require(request, parent, view_permission) - # Mirror v2 exactly: return every note incl. private ones. In v2 the notes @action returns - # `parent.notes.all()`; `private` only excludes a note from generated reports, it is not a - # per-user read filter (§12). `select_related("author")` keeps the list query count flat. - notes = parent.notes + notes = visible_notes(parent.notes, request.user) page_qs = notes.select_related("author").order_by("-date", "-id") envelope = paginate(request, count_qs=notes.all(), page_qs=page_qs, serialize=_serialize_note) return json_response(envelope) diff --git a/unittests/api_v3/test_apiv3_subresources.py b/unittests/api_v3/test_apiv3_subresources.py index cd2288880b2..0cf72e77367 100644 --- a/unittests/api_v3/test_apiv3_subresources.py +++ b/unittests/api_v3/test_apiv3_subresources.py @@ -2,7 +2,7 @@ Generic notes / tags / files sub-resource tests for API v3 (§4.12, OS5). Covers the storage support matrix (notes/files: finding/engagement/test; tags: those + asset), -note privacy (v2 parity: private notes are returned, not per-user filtered), parent-authorization +note privacy (v2 parity: `private` is a per-user read filter), parent-authorization inheritance (404 unknown-or-unauthorized parent, 403 write), multipart upload + streamed download roundtrip, tag replace/append/delete semantics + normalization, the pagination envelope on the list endpoints, and the MANDATORY constant-query guarantee for the finding notes/tags/files lists. @@ -105,17 +105,44 @@ def test_private_note_returned_and_flagged(self): match = next(n for n in listing["results"] if n["id"] == created["id"]) self.assertTrue(match["private"]) - def test_private_note_visible_to_other_authorized_user_v2_parity(self): - """v2 parity: the notes endpoint returns *all* notes; `private` is not a per-user read filter.""" + def test_private_note_hidden_from_other_authorized_user(self): + """v2 parity: `private` is a per-user read filter, so only the author sees their own.""" member = Dojo_User.objects.create_user(username="v3_note_member", password="x") # noqa: S106 self.finding.test.engagement.product.authorized_users.add(member) - created = self.client.post( + member_client = self.token_client(user=member) + + for resource, parent in self.note_file_parents: + with self.subTest(resource=resource): + private = self.client.post( + self.v3_url(f"{resource}/{parent.pk}/notes"), + {"entry": f"private on {resource}", "private": True}, format="json", + ).json() + public = self.client.post( + self.v3_url(f"{resource}/{parent.pk}/notes"), + {"entry": f"public on {resource}", "private": False}, format="json", + ).json() + + member_view = self.get_json(f"{resource}/{parent.pk}/notes", client=member_client) + member_ids = [n["id"] for n in member_view["results"]] + self.assertNotIn(private["id"], member_ids) + self.assertIn(public["id"], member_ids) + self.assertNotIn(f"private on {resource}", str(member_view)) + + author_ids = [n["id"] for n in self.get_json(f"{resource}/{parent.pk}/notes")["results"]] + self.assertIn(private["id"], author_ids) + + def test_private_note_hidden_from_member_count_and_pagination(self): + member = Dojo_User.objects.create_user(username="v3_note_counter", password="x") # noqa: S106 + self.finding.test.engagement.product.authorized_users.add(member) + before = self.get_json(f"findings/{self.finding.pk}/notes", client=self.token_client(user=member))["count"] + + self.client.post( self.v3_url(f"findings/{self.finding.pk}/notes"), - {"entry": "private-but-visible", "private": True}, format="json", - ).json() + {"entry": "counted?", "private": True}, format="json", + ) - member_view = self.get_json(f"findings/{self.finding.pk}/notes", client=self.token_client(user=member)) - self.assertIn(created["id"], [n["id"] for n in member_view["results"]]) + after = self.get_json(f"findings/{self.finding.pk}/notes", client=self.token_client(user=member)) + self.assertEqual(before, after["count"]) class TestApiV3SubresourcesFiles(_SubResourceBase):