Check user permissions before locating stock (#13000)

* Check user permissions before locating stock

* Update unit test
This commit is contained in:
Oliver authored and GitHub committed 2026-10-13 22:59:17 +11:00
1 parent c3917c343b
commit 445fa3e125
3 files changed
+65 -5

No files matched your search

@@ -1,7 +1,12 @@
"""API for location plugins."""
from rest_framework import serializers
from rest_framework.exceptions import NotFound, ParseError, ValidationError
from rest_framework.exceptions import (
NotFound,
ParseError,
PermissionDenied,
ValidationError,
)
from rest_framework.generics import GenericAPIView
from rest_framework.response import Response
@@ -11,6 +16,7 @@ from InvenTree.tasks import offload_task
from plugin import PluginMixinEnum, registry
from plugin.registry import call_plugin_function
from stock.models import StockItem, StockLocation
from users.permissions import check_user_permission
class LocatePluginSerializer(serializers.Serializer):
@@ -58,6 +64,12 @@ class LocatePluginView(GenericAPIView):
# StockItem takes priority
if item_pk:
# Check permissions *before* lookup, to prevent existence leaks
if not check_user_permission(request.user, StockItem, 'view'):
raise PermissionDenied(
'User does not have permission to view stock items'
)
try:
StockItem.objects.get(pk=item_pk)
@@ -76,10 +88,16 @@ class LocatePluginView(GenericAPIView):
except (ValueError, StockItem.DoesNotExist):
raise NotFound(f"StockItem matching PK '{item_pk}' not found")
except Exception:
log_error('locate_stock_item', plugin=plugin.slug)
return ValidationError('Error locating stock item')
log_error('locate_stock_item', plugin=plugin)
raise ValidationError('Error locating stock item')
elif location_pk:
# Check permissions *before* lookup, to prevent existence leaks
if not check_user_permission(request.user, StockLocation, 'view'):
raise PermissionDenied(
'User does not have permission to view stock locations'
)
try:
StockLocation.objects.get(pk=location_pk)
@@ -98,7 +116,7 @@ class LocatePluginView(GenericAPIView):
except (ValueError, StockLocation.DoesNotExist):
raise NotFound(f"StockLocation matching PK '{location_pk}' not found")
except Exception:
log_error('locate_stock_location', plugin=plugin.slug)
return ValidationError('Error locating stock location')
log_error('locate_stock_location', plugin=plugin)
raise ValidationError('Error locating stock location')
else:
raise ParseError("Must supply either 'item' or 'location' parameter")
@@ -20,6 +20,8 @@ class LocatePluginTests(InvenTreeAPITestCase):
fixtures = ['category', 'part', 'location', 'stock']
roles = ['stock.view', 'stock_location.view']
def test_installed(self):
"""Test that a locate plugin is actually installed."""
plugins = registry.with_mixin(PluginMixinEnum.LOCATE)
@@ -69,6 +71,39 @@ class LocatePluginTests(InvenTreeAPITestCase):
f"StockLocation matching PK '{pk}' not found", str(response.data)
)
def test_locate_permissions(self):
"""Test that the locate endpoint enforces role permissions.
A user without the required view permission must be denied,
for both existing and non-existent targets (no existence oracle).
"""
url = reverse('api-locate-plugin')
item = StockItem.objects.get(pk=1)
item.set_metadata('located', False)
self.clearRoles()
for pk in [1, 99999]:
self.post(url, {'plugin': 'samplelocate', 'item': pk}, expected_code=403)
self.post(
url, {'plugin': 'samplelocate', 'location': pk}, expected_code=403
)
# The plugin must not have been triggered
item.refresh_from_db()
self.assertFalse(item.metadata['located'])
# Item permission does not grant location access (and vice versa)
self.assignRole('stock.view')
self.post(url, {'plugin': 'samplelocate', 'item': 1}, expected_code=200)
self.post(url, {'plugin': 'samplelocate', 'location': 1}, expected_code=403)
self.clearRoles()
self.assignRole('stock_location.view')
self.post(url, {'plugin': 'samplelocate', 'item': 1}, expected_code=403)
self.post(url, {'plugin': 'samplelocate', 'location': 1}, expected_code=200)
def test_locate_item(self):
"""Test that the plugin correctly 'locates' a StockItem.
@@ -32,6 +32,11 @@ class SampleLocatePluginTests(InvenTreeAPITestCase):
# Right plugin - no search item
self.post(url, {'plugin': 'samplelocate'}, expected_code=400)
# No permissions
self.post(url, {'plugin': 'samplelocate', 'item': 1}, expected_code=403)
self.assignRole('stock.view')
# Right plugin - wrong reference
self.post(url, {'plugin': 'samplelocate', 'item': 999}, expected_code=404)
@@ -39,6 +44,8 @@ class SampleLocatePluginTests(InvenTreeAPITestCase):
self.post(url, {'plugin': 'samplelocate', 'item': 1}, expected_code=200)
# Right plugin - wrong reference
self.post(url, {'plugin': 'samplelocate', 'location': 999}, expected_code=403)
self.assignRole('stock_location.view')
self.post(url, {'plugin': 'samplelocate', 'location': 999}, expected_code=404)
# Right plugin - right reference