diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index 626de8847b..f492313abe 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,11 +1,14 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 547 +INVENTREE_API_VERSION = 548 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +v548 -> 2026-09-19 : https://github.com/inventree/InvenTree/pull/12886 + - Fix API permissions for multiple endpoints + v547 -> 2026-09-12 : https://github.com/inventree/InvenTree/pull/12850 - Added more details to API token management diff --git a/src/backend/InvenTree/machine/api.py b/src/backend/InvenTree/machine/api.py index bc78c7737c..f99d8552ab 100644 --- a/src/backend/InvenTree/machine/api.py +++ b/src/backend/InvenTree/machine/api.py @@ -79,7 +79,11 @@ class MachineSettingList(APIView): - GET: return all settings for a machine config """ - permission_classes = [InvenTree.permissions.IsAuthenticatedOrReadScope] + permission_classes = [ + InvenTree.permissions.IsAuthenticatedOrReadScope, + InvenTree.permissions.RolePermission, + ] + role_required = 'admin.view' @extend_schema( responses={200: MachineSerializers.MachineSettingSerializer(many=True)} @@ -118,6 +122,15 @@ class MachineSettingDetail(RetrieveUpdateAPI): queryset = MachineSetting.objects.all() serializer_class = MachineSerializers.MachineSettingSerializer + def get_permission_model(self): + """Return the model to check for role permissions. + + Note: MachineSettingSerializer only assigns Meta.model on the + instance (not the class), so the default class-level lookup + cannot find it and RolePermission would otherwise fail open. + """ + return MachineSetting + def get_object(self): """Lookup machine setting object, based on the URL.""" pk = self.kwargs['pk'] diff --git a/src/backend/InvenTree/machine/test_api.py b/src/backend/InvenTree/machine/test_api.py index eb2e7584a9..6fc9821203 100644 --- a/src/backend/InvenTree/machine/test_api.py +++ b/src/backend/InvenTree/machine/test_api.py @@ -255,6 +255,11 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase): [(s['config_type'], s['key']) for s in response.data], ) + # A user without 'admin.view' cannot read or write machine settings + self.clearRoles() + self.get(machine_setting_url, expected_code=403) + self.patch(machine_setting_url, {'value': 'x'}, expected_code=403) + def test_machine_settings_list(self): """Test machine settings list API endpoint.""" machine = MachineConfig.objects.create( @@ -278,6 +283,10 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase): for key in ['api_url', 'pk', 'typ', 'key']: self.assertIn(key, item) + # A user without 'admin.view' cannot read machine settings + self.clearRoles() + self.get(url, expected_code=403) + def test_machine_restart(self): """Test machine restart API endpoint.""" machine = MachineConfig.objects.create( diff --git a/src/backend/InvenTree/plugin/base/barcodes/api.py b/src/backend/InvenTree/plugin/base/barcodes/api.py index 781ec19548..64be3f512e 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/api.py +++ b/src/backend/InvenTree/plugin/base/barcodes/api.py @@ -260,6 +260,9 @@ class BarcodeGenerate(CreateAPIView): except model_cls.DoesNotExist: raise ValidationError({'error': _('Model instance not found')}) + if not check_user_permission(request.user, model_cls, 'view'): + raise PermissionDenied() + barcode_data = plugin.base.barcodes.helper.generate_barcode(model_instance) return Response({'barcode': barcode_data}, status=status.HTTP_200_OK) diff --git a/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py b/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py index ef2e69d97d..1f7678dade 100644 --- a/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py +++ b/src/backend/InvenTree/plugin/base/barcodes/test_barcode.py @@ -181,6 +181,11 @@ class BarcodeAPITest(InvenTreeAPITestCase): data = self.generateBarcode('stockitem', item.pk, expected_code=200).data self.assertEqual(data['barcode'], 'INV-SI522') + # A user without 'stock.view' cannot generate a barcode for a stock item + # they cannot otherwise access + self.clearRoles() + self.generateBarcode('stockitem', item.pk, expected_code=403) + def test_barcode_generation_invalid(self): """Test barcode generation for invalid model/pk.""" self.generateBarcode('invalidmodel', 1, expected_code=400) diff --git a/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py b/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py index f6f7f0fbb1..485356cc2f 100644 --- a/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py +++ b/src/backend/InvenTree/plugin/samples/integration/test_validation_sample.py @@ -14,6 +14,8 @@ class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase): fixtures = ['part', 'category', 'location', 'build', 'stock'] + roles = ['part.view', 'build.view'] + def setUp(self): """Set up the test environment.""" super().setUp() diff --git a/src/backend/InvenTree/stock/api.py b/src/backend/InvenTree/stock/api.py index 8355fbfbed..62fb20630c 100644 --- a/src/backend/InvenTree/stock/api.py +++ b/src/backend/InvenTree/stock/api.py @@ -5,7 +5,7 @@ from datetime import timedelta from django.core.exceptions import ValidationError as DjangoValidationError from django.db import transaction -from django.db.models import F, Q +from django.db.models import F, Model, Q from django.urls import include, path from django.utils.translation import gettext_lazy as _ @@ -14,6 +14,7 @@ from django_filters.rest_framework.filterset import FilterSet from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema, extend_schema_field from rest_framework import status +from rest_framework.exceptions import PermissionDenied from rest_framework.generics import GenericAPIView from rest_framework.response import Response from rest_framework.serializers import ValidationError @@ -73,6 +74,7 @@ from stock.models import ( StockLocationType, ) from stock.status_codes import StockHistoryCode, StockStatus +from users.permissions import check_user_permission class GenerateBatchCode(GenericAPIView): @@ -86,6 +88,12 @@ class GenerateBatchCode(GenericAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + for value in serializer.validated_data.values(): + if isinstance(value, Model) and not check_user_permission( + request.user, value.__class__, 'view' + ): + raise PermissionDenied() + data = {'batch_code': generate_batch_code(**serializer.validated_data)} return Response(data, status=status.HTTP_201_CREATED) @@ -102,6 +110,11 @@ class GenerateSerialNumber(GenericAPIView): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + part = serializer.validated_data.get('part') + + if part and not check_user_permission(request.user, part.__class__, 'view'): + raise PermissionDenied() + data = {'serial_number': generate_serial_number(**serializer.validated_data)} return Response(data, status=status.HTTP_201_CREATED) diff --git a/src/backend/InvenTree/stock/test_api.py b/src/backend/InvenTree/stock/test_api.py index 7eac0ba685..b8470284ce 100644 --- a/src/backend/InvenTree/stock/test_api.py +++ b/src/backend/InvenTree/stock/test_api.py @@ -1294,6 +1294,11 @@ class StockItemListTest(StockAPITestCase): response = self.post(url, {'item': 1, 'quantity': 2}) self.assertEqual(response.data['batch_code'], '1') + # A user without 'stock.view' cannot use this endpoint to read the batch template + # rendering of a stock item they cannot otherwise access + self.clearRoles() + self.post(url, {'item': 1}, expected_code=403) + def test_serial_generate_api(self): """Test helper API for serial management.""" url = reverse('api-generate-serial-number') @@ -1317,6 +1322,11 @@ class StockItemListTest(StockAPITestCase): response.data['quantity'], ['Quantity must be greater than zero'] ) + # A user without 'part.view' cannot use this endpoint to read serial numbers + # for a part they cannot otherwise access + self.clearRoles() + self.post(url, {'part': 1, 'quantity': 1}, expected_code=403) + def test_child_items(self): """Test that the 'child_items' annotation works as expected.""" # Create a trackable part