mirror of
https://github.com/inventree/InvenTree.git
synced 2026-10-15 14:40:58 +00:00
Check user permissions before locating stock (#13000)
* Check user permissions before locating stock * Update unit test
This commit is contained in:
1 parent
c3917c343b
commit
445fa3e125
3 files changed
+65
-5
No files matched your search
@@ -1,7 +1,12 @@
|
|||||||
"""API for location plugins."""
|
"""API for location plugins."""
|
||||||
|
|
||||||
from rest_framework import serializers
|
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.generics import GenericAPIView
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
|
||||||
@@ -11,6 +16,7 @@ from InvenTree.tasks import offload_task
|
|||||||
from plugin import PluginMixinEnum, registry
|
from plugin import PluginMixinEnum, registry
|
||||||
from plugin.registry import call_plugin_function
|
from plugin.registry import call_plugin_function
|
||||||
from stock.models import StockItem, StockLocation
|
from stock.models import StockItem, StockLocation
|
||||||
|
from users.permissions import check_user_permission
|
||||||
|
|
||||||
|
|
||||||
class LocatePluginSerializer(serializers.Serializer):
|
class LocatePluginSerializer(serializers.Serializer):
|
||||||
@@ -58,6 +64,12 @@ class LocatePluginView(GenericAPIView):
|
|||||||
|
|
||||||
# StockItem takes priority
|
# StockItem takes priority
|
||||||
if item_pk:
|
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:
|
try:
|
||||||
StockItem.objects.get(pk=item_pk)
|
StockItem.objects.get(pk=item_pk)
|
||||||
|
|
||||||
@@ -76,10 +88,16 @@ class LocatePluginView(GenericAPIView):
|
|||||||
except (ValueError, StockItem.DoesNotExist):
|
except (ValueError, StockItem.DoesNotExist):
|
||||||
raise NotFound(f"StockItem matching PK '{item_pk}' not found")
|
raise NotFound(f"StockItem matching PK '{item_pk}' not found")
|
||||||
except Exception:
|
except Exception:
|
||||||
log_error('locate_stock_item', plugin=plugin.slug)
|
log_error('locate_stock_item', plugin=plugin)
|
||||||
return ValidationError('Error locating stock item')
|
raise ValidationError('Error locating stock item')
|
||||||
|
|
||||||
elif location_pk:
|
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:
|
try:
|
||||||
StockLocation.objects.get(pk=location_pk)
|
StockLocation.objects.get(pk=location_pk)
|
||||||
|
|
||||||
@@ -98,7 +116,7 @@ class LocatePluginView(GenericAPIView):
|
|||||||
except (ValueError, StockLocation.DoesNotExist):
|
except (ValueError, StockLocation.DoesNotExist):
|
||||||
raise NotFound(f"StockLocation matching PK '{location_pk}' not found")
|
raise NotFound(f"StockLocation matching PK '{location_pk}' not found")
|
||||||
except Exception:
|
except Exception:
|
||||||
log_error('locate_stock_location', plugin=plugin.slug)
|
log_error('locate_stock_location', plugin=plugin)
|
||||||
return ValidationError('Error locating stock location')
|
raise ValidationError('Error locating stock location')
|
||||||
else:
|
else:
|
||||||
raise ParseError("Must supply either 'item' or 'location' parameter")
|
raise ParseError("Must supply either 'item' or 'location' parameter")
|
||||||
@@ -20,6 +20,8 @@ class LocatePluginTests(InvenTreeAPITestCase):
|
|||||||
|
|
||||||
fixtures = ['category', 'part', 'location', 'stock']
|
fixtures = ['category', 'part', 'location', 'stock']
|
||||||
|
|
||||||
|
roles = ['stock.view', 'stock_location.view']
|
||||||
|
|
||||||
def test_installed(self):
|
def test_installed(self):
|
||||||
"""Test that a locate plugin is actually installed."""
|
"""Test that a locate plugin is actually installed."""
|
||||||
plugins = registry.with_mixin(PluginMixinEnum.LOCATE)
|
plugins = registry.with_mixin(PluginMixinEnum.LOCATE)
|
||||||
@@ -69,6 +71,39 @@ class LocatePluginTests(InvenTreeAPITestCase):
|
|||||||
f"StockLocation matching PK '{pk}' not found", str(response.data)
|
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):
|
def test_locate_item(self):
|
||||||
"""Test that the plugin correctly 'locates' a StockItem.
|
"""Test that the plugin correctly 'locates' a StockItem.
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ class SampleLocatePluginTests(InvenTreeAPITestCase):
|
|||||||
# Right plugin - no search item
|
# Right plugin - no search item
|
||||||
self.post(url, {'plugin': 'samplelocate'}, expected_code=400)
|
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
|
# Right plugin - wrong reference
|
||||||
self.post(url, {'plugin': 'samplelocate', 'item': 999}, expected_code=404)
|
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)
|
self.post(url, {'plugin': 'samplelocate', 'item': 1}, expected_code=200)
|
||||||
|
|
||||||
# Right plugin - wrong reference
|
# 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)
|
self.post(url, {'plugin': 'samplelocate', 'location': 999}, expected_code=404)
|
||||||
|
|
||||||
# Right plugin - right reference
|
# Right plugin - right reference
|
||||||
|
|||||||
Reference in new issue
Block a user