From f21bc2d06f906838e760d2face15cc50cc9df984 Mon Sep 17 00:00:00 2001 From: Matthias Mair Date: Wed, 24 Jun 2026 02:45:26 +0200 Subject: [PATCH] extend barcode scans API (#12233) * extend barcode scans with user perm check * fix import * fix call * align error message * add missing permissions to test * remove erronous assign * ensure permission erros knock through --- docs/docs/plugins/mixins/barcode.md | 4 ++-- src/backend/InvenTree/InvenTree/models.py | 11 ++++++++++- src/backend/InvenTree/plugin/base/barcodes/api.py | 12 ++++++++---- .../InvenTree/plugin/base/barcodes/mixins.py | 6 +++--- .../InvenTree/plugin/base/barcodes/test_barcode.py | 8 +++++++- .../plugin/builtin/barcodes/inventree_barcode.py | 14 ++++++++------ .../builtin/barcodes/test_inventree_barcode.py | 1 + .../builtin/suppliers/test_supplier_barcodes.py | 9 +++++++++ 8 files changed, 48 insertions(+), 17 deletions(-) diff --git a/docs/docs/plugins/mixins/barcode.md b/docs/docs/plugins/mixins/barcode.md index 962e36da31..3985990141 100644 --- a/docs/docs/plugins/mixins/barcode.md +++ b/docs/docs/plugins/mixins/barcode.md @@ -54,14 +54,14 @@ class InvenTreeBarcodePlugin(BarcodeMixin, InvenTreePlugin): VERSION = "0.0.1" AUTHOR = "Michael" - def scan(self, barcode_data): + def scan(self, barcode_data, user, **kwargs): if barcode_data.startswith("PART-"): try: pk = int(barcode_data.split("PART-")[1]) instance = Part.objects.get(pk=pk) label = Part.barcode_model_type() - return {label: instance.format_matched_response()} + return {label: instance.format_matched_response(user=user)} except Part.DoesNotExist: pass ``` diff --git a/src/backend/InvenTree/InvenTree/models.py b/src/backend/InvenTree/InvenTree/models.py index 1043fb6db9..31c68e8730 100644 --- a/src/backend/InvenTree/InvenTree/models.py +++ b/src/backend/InvenTree/InvenTree/models.py @@ -23,6 +23,7 @@ from django_q.models import Task from error_report.models import Error from mptt.exceptions import InvalidMove from mptt.models import MPTTModel, TreeForeignKey +from rest_framework.exceptions import PermissionDenied from stdimage.models import StdImageField from taggit.managers import TaggableManager @@ -1334,8 +1335,16 @@ class InvenTreeBarcodeMixin(models.Model): return generate_barcode(self) - def format_matched_response(self): + def format_matched_response(self, user, **kwargs): """Format a standard response for a matched barcode.""" + # Check permission for this object + from users.permissions import check_user_permission + + if not check_user_permission(user, self, 'view'): + raise PermissionDenied( + _('User does not have permission to view this model') + ) + data = {'pk': self.pk} if hasattr(self, 'get_api_url'): diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index cf9b0d8dd6..af84a5b4e8 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -153,7 +153,9 @@ class BarcodeView(CreateAPIView): for current_plugin in plugins: try: - result = current_plugin.scan(barcode) + result = current_plugin.scan(barcode, user=request.user, **kwargs) + except PermissionDenied as exc: + raise exc except Exception: log_error('BarcodeView.scan_barcode', plugin=current_plugin.slug) continue @@ -282,7 +284,7 @@ class BarcodeAssign(BarcodeView): # First check if the provided barcode matches an existing database entry if inventree_barcode_plugin: - result = inventree_barcode_plugin.scan(barcode) + result = inventree_barcode_plugin.scan(barcode, user=request.user, **kwargs) if result is not None: result['error'] = _('Barcode matches existing item') @@ -459,7 +461,9 @@ class BarcodePOAllocate(BarcodeView): manufacturer_part=response.get('manufacturerpart', None), ) response['success'] = _('Matched supplier part') - response['supplierpart'] = supplier_part.format_matched_response() + response['supplierpart'] = supplier_part.format_matched_response( + user=request.user + ) except ValidationError as e: response['error'] = str(e) @@ -524,7 +528,7 @@ class BarcodePOReceive(BarcodeView): filter(lambda plugin: plugin.name == 'InvenTreeBarcode', plugins) ) - if result := internal_barcode_plugin.scan(barcode): + if result := internal_barcode_plugin.scan(barcode, user=request.user, **kwargs): if 'stockitem' in result: response['error'] = _('Item has already been received') self.log_scan(request, response, False) diff --git a/src/backend/InvenTree/plugin/base/barcodes/mixins.py b/src/backend/InvenTree/plugin/base/barcodes/mixins.py index fe9d2e684b..9189e6f3ba 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/mixins.py +++ b/src/backend/InvenTree/plugin/base/barcodes/mixins.py @@ -42,7 +42,7 @@ class BarcodeMixin: """Does this plugin have everything needed to process a barcode.""" return True - def scan(self, barcode_data): + def scan(self, barcode_data: str, user, **kwargs) -> dict | None: """Scan a barcode against this plugin. This method is explicitly called from the /scan/ API endpoint, @@ -261,7 +261,7 @@ class SupplierBarcodeMixin(BarcodeMixin): 'extract_barcode_fields must be implemented by each plugin' ) - def scan(self, barcode_data: str) -> dict | None: + def scan(self, barcode_data: str, user, **kwargs) -> dict | None: """Perform a generic 'scan' operation on a supplier barcode. The supplier barcode may provide sufficient information to match against @@ -297,7 +297,7 @@ class SupplierBarcodeMixin(BarcodeMixin): for k, v in matches.items(): if v and hasattr(v, 'pk'): has_match = True - data[k] = v.format_matched_response() + data[k] = v.format_matched_response(user=user) if not has_match: return None diff --git a/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py b/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py index cec6fc68ad..667ef8d4ed 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py +++ b/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py @@ -15,6 +15,7 @@ class BarcodeAPITest(InvenTreeAPITestCase): """Tests for barcode api.""" fixtures = ['category', 'part', 'location', 'stock'] + roles = ['stock.view', 'stock_location.view', 'part.view'] def setUp(self): """Setup for all tests.""" @@ -259,6 +260,7 @@ class SOAllocateTest(InvenTreeAPITestCase): """Unit tests for the barcode endpoint for allocating items to a sales order.""" fixtures = ['category', 'company', 'part', 'location', 'stock'] + roles = ['stock.view'] @classmethod def setUpTestData(cls): @@ -343,10 +345,14 @@ class SOAllocateTest(InvenTreeAPITestCase): # Test with barcode which points to a *part* instance item.part.assign_barcode(barcode_data='abcde') + # missing permission for viewing the part - error + self.postBarcode('abcde', sales_order=self.sales_order.pk, expected_code=403) + + # Add part.view role and test again + self.assignRole('part.view') result = self.postBarcode( 'abcde', sales_order=self.sales_order.pk, expected_code=400 ) - self.assertIn('does not match an existing stock item', str(result['error'])) def test_submit(self): diff --git a/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py b/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py index 10cf3189c8..c2f163fe8f 100644 --- a/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py +++ b/src/backend/InvenTree/plugin/builtin/barcodes/inventree_barcode.py @@ -48,11 +48,11 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi }, } - def format_matched_response(self, label, model, instance): + def format_matched_response(self, label, model, instance, user, **kwargs): """Format a response for the scanned data.""" - return {label: instance.format_matched_response()} + return {label: instance.format_matched_response(user=user, **kwargs)} - def scan(self, barcode_data): + def scan(self, barcode_data, user, **kwargs): """Scan a barcode against this plugin. Here we are looking for a dict object which contains a reference to a particular InvenTree database object @@ -79,7 +79,7 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi try: instance = model.objects.get(pk=int(pk)) - return self.format_matched_response(label, model, instance) + return self.format_matched_response(label, model, instance, user=user) except (ValueError, model.DoesNotExist): pass @@ -111,7 +111,9 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi instance = model.objects.get(pk=pk) return { - **self.format_matched_response(label, model, instance), + **self.format_matched_response( + label, model, instance, user=user + ), 'success': succcess_message, } except (ValueError, model.DoesNotExist): @@ -129,7 +131,7 @@ class InvenTreeInternalBarcodePlugin(SettingsMixin, BarcodeMixin, InvenTreePlugi if instance is not None: return { - **self.format_matched_response(label, model, instance), + **self.format_matched_response(label, model, instance, user=user), 'success': succcess_message, } diff --git a/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py b/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py index 1dc1eb90b2..4ae09064af 100644 --- a/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py +++ b/src/backend/InvenTree/plugin/builtin/barcodes/test_inventree_barcode.py @@ -11,6 +11,7 @@ class TestInvenTreeBarcode(InvenTreeAPITestCase): """Tests for the integrated InvenTreeBarcode barcode plugin.""" fixtures = ['category', 'part', 'location', 'stock', 'company', 'supplier_part'] + roles = ['stock.view', 'stock_location.view', 'part.view'] def setUp(self): """Set up the test case.""" diff --git a/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py b/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py index d194102581..d04993bef6 100644 --- a/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py +++ b/src/backend/InvenTree/plugin/builtin/suppliers/test_supplier_barcodes.py @@ -14,6 +14,13 @@ class SupplierBarcodeTests(InvenTreeAPITestCase): """Tests barcode parsing for all suppliers.""" SCAN_URL = reverse('api-barcode-scan') + roles = [ + 'stock.view', + 'stock_location.view', + 'part.view', + 'company.view', + 'order.view', + ] @classmethod def setUpTestData(cls): @@ -176,6 +183,8 @@ class SupplierBarcodeTests(InvenTreeAPITestCase): class SupplierBarcodePOReceiveTests(InvenTreeAPITestCase): """Tests barcode scanning to receive a purchase order item.""" + roles = ['stock.view', 'stock_location.view'] + def setUp(self): """Create supplier part and purchase_order.""" super().setUp()