From 01a7d8657701d60ac612d286973eab39f86dcbdb Mon Sep 17 00:00:00 2001 From: "eric.quintero@trailofbits.com" Date: Tue, 2 Jun 2026 16:26:05 +0000 Subject: [PATCH] Scope release-file bulk deletion to release --- apps/downloads/api.py | 8 +++++-- apps/downloads/tests/test_views.py | 36 ++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/apps/downloads/api.py b/apps/downloads/api.py index 4d8f3010c..d6c8938e8 100644 --- a/apps/downloads/api.py +++ b/apps/downloads/api.py @@ -177,10 +177,14 @@ class ReleaseFileViewSet(viewsets.ModelViewSet): def delete_by_release(self, request): """Delete all release files associated with a given release.""" release = request.query_params.get("release") - if release is None: + if not release: + return Response(status=status.HTTP_400_BAD_REQUEST) + try: + release_id = int(release) + except ValueError: return Response(status=status.HTTP_400_BAD_REQUEST) # TODO: We can add support for pagination in the future. - queryset = self.filter_queryset(self.get_queryset()) + queryset = self.get_queryset().filter(release_id=release_id) # This calls 'mixins.DestroyModelMixin.perform_destroy()'. self.perform_destroy(queryset) return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/apps/downloads/tests/test_views.py b/apps/downloads/tests/test_views.py index 1a05dfd40..15384ce7a 100644 --- a/apps/downloads/tests/test_views.py +++ b/apps/downloads/tests/test_views.py @@ -547,18 +547,37 @@ def test_filter_release_file_delete_by_release(self): # http://www.django-rest-framework.org/api-guide/viewsets/#reversing-action-urls self.create_url( "release_file/delete_by_release", - filters={"release": self.release_275.pk}, + filters={ + "release": self.release_275.pk, + "os": self.linux.pk, + }, ), HTTP_AUTHORIZATION=self.Authorization, ) self.assertEqual(response.status_code, 204) # Making a GET request after the deletion shouldn't return any results. - response = self.client.get(self.create_url("release_file", filters={"release": self.release_275.pk})) + response = self.client.get( + self.create_url( + "release_file", + filters={"release": self.release_275.pk}, + ) + ) self.assertEqual(response.status_code, 200) content = self.get_json(response) self.assertEqual(len(content), 0) + # Files for other releases should be left intact. + response = self.client.get( + self.create_url( + "release_file", + filters={"release": self.draft_release.pk}, + ) + ) + self.assertEqual(response.status_code, 200) + content = self.get_json(response) + self.assertEqual(len(content), 1) + # Making a valid request should return 403 Forbidden if it # comes from a non-staff user. response = self.json_client( @@ -579,6 +598,19 @@ def test_filter_release_file_delete_by_release(self): ) self.assertEqual(response.status_code, 400) + # Blank or malformed release values should also return 400 instead of + # reaching queryset evaluation with an invalid primary-key value. + for invalid_release in ("", "not-a-release-id"): + response = self.json_client( + "delete", + self.create_url( + "release_file/delete_by_release", + filters={"release": invalid_release}, + ), + HTTP_AUTHORIZATION=self.Authorization, + ) + self.assertEqual(response.status_code, 400) + # /release_file/delete_by_release/ should only accept DELETE requests. response = self.client.get( self.create_url(