From 5adf33d3549810a306e594686e4d15c2b217ff2e Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 21 Mar 2026 17:17:35 +1100 Subject: [PATCH] Improvements for get_bulk_queryset (#11581) * Improvements for get_bulk_queryset - Limit scope to base view queryset - Remove ability to provide arbitrary filters - Remove feedback if zero items are found * Adjust unit test * Remove filter test * Update CHANGELOG.md --- CHANGELOG.md | 2 ++ src/backend/InvenTree/InvenTree/api.py | 29 +++++---------------- src/backend/InvenTree/InvenTree/test_api.py | 8 +----- src/backend/InvenTree/common/tests.py | 14 +++++++--- src/backend/InvenTree/stock/test_api.py | 12 +-------- 5 files changed, 20 insertions(+), 45 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d930e4a09..0b71fd37a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- [#11581](https://github.com/inventree/InvenTree/pull/11581) removes the ability to specify arbitrary filters when performing bulk operations via the API. This functionality represented a significant security risk, and was not required for any existing use cases. Bulk operations now only work with a provided list of primary keys. + ## 1.2.0 - 2026-02-12 ### Breaking Changes diff --git a/src/backend/InvenTree/InvenTree/api.py b/src/backend/InvenTree/InvenTree/api.py index fc2b7711d9..369d6ccdea 100644 --- a/src/backend/InvenTree/InvenTree/api.py +++ b/src/backend/InvenTree/InvenTree/api.py @@ -423,23 +423,19 @@ class BulkOperationMixin: def get_bulk_queryset(self, request): """Return a queryset based on the selection made in the request. - Selection can be made by providing either: - - - items: A list of primary key values - - filters: A dictionary of filter values + Selection can be made by providing a list of primary key values, + which will be used to filter the queryset. """ - model = self.serializer_class.Meta.model - items = request.data.pop('items', None) - filters = request.data.pop('filters', None) all_filter = request.GET.get('all', None) - queryset = model.objects.all() + # Return the base queryset for this model + queryset = self.get_queryset() - if not items and not filters and all_filter is None: + if not items and all_filter is None: raise ValidationError({ 'non_field_errors': _( - 'List of items or filters must be provided for bulk operation' + 'List of items must be provided for bulk operation' ) }) @@ -457,19 +453,6 @@ class BulkOperationMixin: 'non_field_errors': _('Invalid items list provided') }) - if filters: - if type(filters) is not dict: - raise ValidationError({ - 'non_field_errors': _('Filters must be provided as a dict') - }) - - try: - queryset = queryset.filter(**filters) - except Exception: - raise ValidationError({ - 'non_field_errors': _('Invalid filters provided') - }) - if all_filter and not helpers.str2bool(all_filter): raise ValidationError({ 'non_field_errors': _('All filter must only be used with true') diff --git a/src/backend/InvenTree/InvenTree/test_api.py b/src/backend/InvenTree/InvenTree/test_api.py index 55da6ae0f3..5475edd03a 100644 --- a/src/backend/InvenTree/InvenTree/test_api.py +++ b/src/backend/InvenTree/InvenTree/test_api.py @@ -278,8 +278,7 @@ class BulkDeleteTests(InvenTreeAPITestCase): response = self.delete(url, {}, expected_code=400) self.assertIn( - 'List of items or filters must be provided for bulk operation', - str(response.data), + 'List of items must be provided for bulk operation', str(response.data) ) # DELETE with invalid 'items' @@ -287,11 +286,6 @@ class BulkDeleteTests(InvenTreeAPITestCase): self.assertIn('Items must be provided as a list', str(response.data)) - # DELETE with invalid 'filters' - response = self.delete(url, {'filters': [1, 2, 3]}, expected_code=400) - - self.assertIn('Filters must be provided as a dict', str(response.data)) - class SearchTests(InvenTreeAPITestCase): """Unit tests for global search endpoint.""" diff --git a/src/backend/InvenTree/common/tests.py b/src/backend/InvenTree/common/tests.py index 6255c867ab..e6c850e0c3 100644 --- a/src/backend/InvenTree/common/tests.py +++ b/src/backend/InvenTree/common/tests.py @@ -1352,12 +1352,18 @@ class NotificationTest(InvenTreeAPITestCase): # Now, let's bulk delete all 'unread' notifications via the API, # but only associated with the logged in user - response = self.delete(url, {'filters': {'read': False}}, expected_code=200) + read_notifications = NotificationMessage.objects.filter(read=True) + response = self.delete( + url, {'items': [ntf.pk for ntf in read_notifications]}, expected_code=200 + ) - # Only 7 notifications should have been deleted, + # Only 3 notifications should have been deleted, # as the notifications associated with other users must remain untouched - self.assertEqual(NotificationMessage.objects.count(), 13) - self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 3) + self.assertEqual(NotificationMessage.objects.count(), 17) + self.assertEqual(NotificationMessage.objects.filter(user=self.user).count(), 7) + self.assertEqual( + NotificationMessage.objects.filter(user=self.user, read=True).count(), 0 + ) def test_simple(self): """Test that a simple notification can be created.""" diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 4f62c170ce..1434a0e3f0 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -2434,17 +2434,7 @@ class StockTestResultTest(StockAPITestCase): self.delete(url, {}, expected_code=400) # Now, let's delete all the newly created items with a single API request - # However, we will provide incorrect filters - response = self.delete( - url, {'items': tests, 'filters': {'stock_item': 10}}, expected_code=400 - ) - - self.assertEqual(StockItemTestResult.objects.count(), n + 50) - - # Try again, but with the correct filters this time - response = self.delete( - url, {'items': tests, 'filters': {'stock_item': 1}}, expected_code=200 - ) + response = self.delete(url, {'items': tests}, expected_code=200) self.assertEqual(StockItemTestResult.objects.count(), n)