mirror of
https://github.com/inventree/InvenTree.git
synced 2026-09-27 14:16:02 +00:00
API permission tweaks (#12886)
* API permission tweaks - Explicitly require admin role for MachineSettingList - Permission check for barcode generation * Adjust existing unit test * Update API version
This commit is contained in:
@@ -1,11 +1,14 @@
|
|||||||
"""InvenTree API version information."""
|
"""InvenTree API version information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
v547 -> 2026-09-12 : https://github.com/inventree/InvenTree/pull/12850
|
||||||
- Added more details to API token management
|
- Added more details to API token management
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ class MachineSettingList(APIView):
|
|||||||
- GET: return all settings for a machine config
|
- 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(
|
@extend_schema(
|
||||||
responses={200: MachineSerializers.MachineSettingSerializer(many=True)}
|
responses={200: MachineSerializers.MachineSettingSerializer(many=True)}
|
||||||
@@ -118,6 +122,15 @@ class MachineSettingDetail(RetrieveUpdateAPI):
|
|||||||
queryset = MachineSetting.objects.all()
|
queryset = MachineSetting.objects.all()
|
||||||
serializer_class = MachineSerializers.MachineSettingSerializer
|
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):
|
def get_object(self):
|
||||||
"""Lookup machine setting object, based on the URL."""
|
"""Lookup machine setting object, based on the URL."""
|
||||||
pk = self.kwargs['pk']
|
pk = self.kwargs['pk']
|
||||||
|
|||||||
@@ -255,6 +255,11 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
|||||||
[(s['config_type'], s['key']) for s in response.data],
|
[(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):
|
def test_machine_settings_list(self):
|
||||||
"""Test machine settings list API endpoint."""
|
"""Test machine settings list API endpoint."""
|
||||||
machine = MachineConfig.objects.create(
|
machine = MachineConfig.objects.create(
|
||||||
@@ -278,6 +283,10 @@ class MachineAPITest(TestMachineRegistryMixin, InvenTreeAPITestCase):
|
|||||||
for key in ['api_url', 'pk', 'typ', 'key']:
|
for key in ['api_url', 'pk', 'typ', 'key']:
|
||||||
self.assertIn(key, item)
|
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):
|
def test_machine_restart(self):
|
||||||
"""Test machine restart API endpoint."""
|
"""Test machine restart API endpoint."""
|
||||||
machine = MachineConfig.objects.create(
|
machine = MachineConfig.objects.create(
|
||||||
|
|||||||
@@ -260,6 +260,9 @@ class BarcodeGenerate(CreateAPIView):
|
|||||||
except model_cls.DoesNotExist:
|
except model_cls.DoesNotExist:
|
||||||
raise ValidationError({'error': _('Model instance not found')})
|
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)
|
barcode_data = plugin.base.barcodes.helper.generate_barcode(model_instance)
|
||||||
|
|
||||||
return Response({'barcode': barcode_data}, status=status.HTTP_200_OK)
|
return Response({'barcode': barcode_data}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -181,6 +181,11 @@ class BarcodeAPITest(InvenTreeAPITestCase):
|
|||||||
data = self.generateBarcode('stockitem', item.pk, expected_code=200).data
|
data = self.generateBarcode('stockitem', item.pk, expected_code=200).data
|
||||||
self.assertEqual(data['barcode'], 'INV-SI522')
|
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):
|
def test_barcode_generation_invalid(self):
|
||||||
"""Test barcode generation for invalid model/pk."""
|
"""Test barcode generation for invalid model/pk."""
|
||||||
self.generateBarcode('invalidmodel', 1, expected_code=400)
|
self.generateBarcode('invalidmodel', 1, expected_code=400)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ class SampleValidatorPluginTest(InvenTreeAPITestCase, InvenTreeTestCase):
|
|||||||
|
|
||||||
fixtures = ['part', 'category', 'location', 'build', 'stock']
|
fixtures = ['part', 'category', 'location', 'build', 'stock']
|
||||||
|
|
||||||
|
roles = ['part.view', 'build.view']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Set up the test environment."""
|
"""Set up the test environment."""
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from datetime import timedelta
|
|||||||
|
|
||||||
from django.core.exceptions import ValidationError as DjangoValidationError
|
from django.core.exceptions import ValidationError as DjangoValidationError
|
||||||
from django.db import transaction
|
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.urls import include, path
|
||||||
from django.utils.translation import gettext_lazy as _
|
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.types import OpenApiTypes
|
||||||
from drf_spectacular.utils import extend_schema, extend_schema_field
|
from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.generics import GenericAPIView
|
from rest_framework.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.serializers import ValidationError
|
from rest_framework.serializers import ValidationError
|
||||||
@@ -73,6 +74,7 @@ from stock.models import (
|
|||||||
StockLocationType,
|
StockLocationType,
|
||||||
)
|
)
|
||||||
from stock.status_codes import StockHistoryCode, StockStatus
|
from stock.status_codes import StockHistoryCode, StockStatus
|
||||||
|
from users.permissions import check_user_permission
|
||||||
|
|
||||||
|
|
||||||
class GenerateBatchCode(GenericAPIView):
|
class GenerateBatchCode(GenericAPIView):
|
||||||
@@ -86,6 +88,12 @@ class GenerateBatchCode(GenericAPIView):
|
|||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
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)}
|
data = {'batch_code': generate_batch_code(**serializer.validated_data)}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
@@ -102,6 +110,11 @@ class GenerateSerialNumber(GenericAPIView):
|
|||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
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)}
|
data = {'serial_number': generate_serial_number(**serializer.validated_data)}
|
||||||
|
|
||||||
return Response(data, status=status.HTTP_201_CREATED)
|
return Response(data, status=status.HTTP_201_CREATED)
|
||||||
|
|||||||
@@ -1294,6 +1294,11 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
response = self.post(url, {'item': 1, 'quantity': 2})
|
response = self.post(url, {'item': 1, 'quantity': 2})
|
||||||
self.assertEqual(response.data['batch_code'], '1')
|
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):
|
def test_serial_generate_api(self):
|
||||||
"""Test helper API for serial management."""
|
"""Test helper API for serial management."""
|
||||||
url = reverse('api-generate-serial-number')
|
url = reverse('api-generate-serial-number')
|
||||||
@@ -1317,6 +1322,11 @@ class StockItemListTest(StockAPITestCase):
|
|||||||
response.data['quantity'], ['Quantity must be greater than zero']
|
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):
|
def test_child_items(self):
|
||||||
"""Test that the 'child_items' annotation works as expected."""
|
"""Test that the 'child_items' annotation works as expected."""
|
||||||
# Create a trackable part
|
# Create a trackable part
|
||||||
|
|||||||
Reference in New Issue
Block a user