diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index bd61d76ee2a..27444b33fc7 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -1546,6 +1546,7 @@ def get_duplicate_cluster(self, request, pk): ) @action(detail=True, methods=["post"], url_path=r"duplicate/reset", permission_classes=(IsAuthenticated, permissions.UserHasFindingRelatedObjectPermission)) def reset_finding_duplicate_status(self, request, pk): + self.get_object() checked_duplicate_id = reset_finding_duplicate_status_internal( request.user, pk, ) @@ -1566,6 +1567,7 @@ def reset_finding_duplicate_status(self, request, pk): detail=True, methods=["post"], url_path=r"original/(?P\d+)", permission_classes=(IsAuthenticated, permissions.UserHasFindingRelatedObjectPermission), ) def set_finding_as_original(self, request, pk, new_fid): + self.get_object() success = set_finding_as_original_internal(request.user, pk, new_fid) if not success: return Response(status=status.HTTP_400_BAD_REQUEST) diff --git a/unittests/test_permissions_audit.py b/unittests/test_permissions_audit.py index 9d4ca0a746e..7e4ead33580 100644 --- a/unittests/test_permissions_audit.py +++ b/unittests/test_permissions_audit.py @@ -1571,18 +1571,16 @@ def test_finding_metadata_reader_allowed(self): self.assertEqual(response.status_code, 200, response.content) # ── Finding: duplicate status actions ────────────────────────────── - # NOTE: reset_finding_duplicate_status and set_finding_as_original - # bypass self.get_object(), so DRF object-level permission checks - # (has_object_permission) never fire. The internal helpers do their - # own permission checks. These tests verify current behaviour. + # reset_finding_duplicate_status and set_finding_as_original call + # self.get_object() so DRF's object-level permission check runs via + # UserHasFindingRelatedObjectPermission (POST -> Finding_Edit). def test_finding_reset_duplicate_reader(self): - """View bypasses get_object() — internal helper checks permissions.""" + """Reader lacks Finding_Edit — POST must be denied before the helper runs.""" client = self._client_for_user(self.reader_user) url = reverse("finding-reset-finding-duplicate-status", args=(self.finding.id,)) response = client.post(url, format="json") - # Returns 400 (not a duplicate) — internal helper runs before perm check - self.assertEqual(response.status_code, 400, response.content) + self.assertEqual(response.status_code, 403, response.content) def test_finding_reset_duplicate_writer(self): client = self._client_for_user(self.writer_user) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 43483a13e3d..0c15703e646 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -1849,6 +1849,70 @@ def test_post_without_finding_returns_4xx(self): self.assertLess(response.status_code, 500) +@versioned_fixtures +class FindingActionAuthzTest(DojoAPITestCase): + + fixtures = ["dojo_testdata.json"] + + def _client_for(self, username): + user = User.objects.get(username=username) + token = Token.objects.get(user=user) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Token " + token.key) + return client + + def test_admin_can_reset_finding_duplicate_status(self): + client = self._client_for("admin") + # Mark finding 2 as a duplicate of finding 3 first, then reset. + set_response = client.post("/api/v2/findings/2/original/3/") + self.assertEqual(set_response.status_code, status.HTTP_204_NO_CONTENT, set_response.content[:500]) + reset_response = client.post("/api/v2/findings/2/duplicate/reset/") + self.assertEqual(reset_response.status_code, status.HTTP_204_NO_CONTENT, reset_response.content[:500]) + refreshed = Finding.objects.get(pk=2) + self.assertFalse(refreshed.duplicate) + self.assertIsNone(refreshed.duplicate_finding) + + def test_admin_can_set_finding_as_original(self): + client = self._client_for("admin") + response = client.post("/api/v2/findings/2/original/3/") + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT, response.content[:500]) + refreshed = Finding.objects.get(pk=2) + self.assertTrue(refreshed.duplicate) + self.assertEqual(refreshed.duplicate_finding_id, 3) + + def test_unrelated_user_cannot_reset_finding_duplicate_status(self): + client = self._client_for("user2") + # Sanity: finding 7 is not visible to this user. + self.assertEqual(client.get("/api/v2/findings/7/").status_code, 404) + + before = Finding.objects.get(pk=7) + before_duplicate = before.duplicate + before_duplicate_finding_id = before.duplicate_finding_id + + response = client.post("/api/v2/findings/7/duplicate/reset/") + self.assertIn(response.status_code, (403, 404), response.content[:500]) + + after = Finding.objects.get(pk=7) + self.assertEqual(after.duplicate, before_duplicate) + self.assertEqual(after.duplicate_finding_id, before_duplicate_finding_id) + + def test_unrelated_user_cannot_set_finding_as_original(self): + client = self._client_for("user2") + # Sanity: finding 7 is not visible to this user. + self.assertEqual(client.get("/api/v2/findings/7/").status_code, 404) + + before = Finding.objects.get(pk=7) + before_duplicate = before.duplicate + before_duplicate_finding_id = before.duplicate_finding_id + + response = client.post("/api/v2/findings/7/original/2/") + self.assertIn(response.status_code, (403, 404), response.content[:500]) + + after = Finding.objects.get(pk=7) + self.assertEqual(after.duplicate, before_duplicate) + self.assertEqual(after.duplicate_finding_id, before_duplicate_finding_id) + + @versioned_fixtures class FilesTest(DojoAPITestCase): fixtures = ["dojo_testdata.json"]