mirror of
https://github.com/inventree/InvenTree.git
synced 2026-06-12 03:28:37 +00:00
[report] Printing fixes (#12142)
* Check model permissions for printing * Add unit tests * Prevent printing of disabled reports * Updated unit test * Adjust unit test for printing * Update API and CHANGELOG
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
"""InvenTree API version information."""
|
||||
|
||||
# InvenTree API version
|
||||
INVENTREE_API_VERSION = 501
|
||||
INVENTREE_API_VERSION = 502
|
||||
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||
|
||||
INVENTREE_API_TEXT = """
|
||||
|
||||
v502 -> 2026-06-10 : https://github.com/inventree/InvenTree/pull/12142
|
||||
- Prevents users from printing reports or labels against models for which they do not have adequate permissions. This change improves the security of the system by ensuring that users cannot access or print reports or labels for models they do not have permission to view.
|
||||
|
||||
v501 -> 2026-06-05 : https://github.com/inventree/InvenTree/pull/12093
|
||||
- Adds "read_only" attribute to PluginSetting API endpoint, which indicates whether a particular plugin setting is read-only (i.e. cannot be modified via the API)
|
||||
|
||||
|
||||
@@ -171,6 +171,8 @@ class TestLabelPrinterMachineType(InvenTreeAPITestCase):
|
||||
|
||||
fixtures = ['category', 'part', 'location', 'stock']
|
||||
|
||||
roles = ['part.view']
|
||||
|
||||
def test_registration(self):
|
||||
"""Test that the machine is correctly registered from the plugin."""
|
||||
PLG_KEY = 'label-printer-test-plugin'
|
||||
|
||||
@@ -9,6 +9,7 @@ from django.views.decorators.cache import never_cache
|
||||
import django_filters.rest_framework.filters as rest_filters
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from django_filters.rest_framework.filterset import FilterSet
|
||||
from rest_framework.exceptions import PermissionDenied
|
||||
from rest_framework.generics import GenericAPIView
|
||||
from rest_framework.response import Response
|
||||
|
||||
@@ -16,6 +17,7 @@ import InvenTree.permissions
|
||||
import report.helpers
|
||||
import report.models
|
||||
import report.serializers
|
||||
import users.permissions
|
||||
from common.models import DataOutput
|
||||
from common.serializers import DataOutputSerializer
|
||||
from InvenTree.api import meta_path
|
||||
@@ -161,6 +163,14 @@ class LabelPrint(GenericAPIView):
|
||||
|
||||
template = serializer.validated_data['template']
|
||||
|
||||
model_class = template.get_model()
|
||||
if model_class and not users.permissions.check_user_permission(
|
||||
request.user, model_class, 'view'
|
||||
):
|
||||
raise PermissionDenied(
|
||||
_('You do not have permission to view this model type')
|
||||
)
|
||||
|
||||
if template.width <= 0 or template.height <= 0:
|
||||
raise ValidationError({'template': _('Invalid label dimensions')})
|
||||
|
||||
@@ -174,7 +184,7 @@ class LabelPrint(GenericAPIView):
|
||||
|
||||
plugin = self.get_plugin_class(plugin_key, raise_error=True)
|
||||
|
||||
instances = template.get_model().objects.filter(pk__in=items)
|
||||
instances = model_class.objects.filter(pk__in=items)
|
||||
|
||||
# Sort the instances by the order of the provided items
|
||||
instances = sorted(instances, key=lambda item: items.index(item.pk))
|
||||
@@ -261,9 +271,18 @@ class ReportPrint(GenericAPIView):
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
template = serializer.validated_data['template']
|
||||
|
||||
model_class = template.get_model()
|
||||
if model_class and not users.permissions.check_user_permission(
|
||||
request.user, model_class, 'view'
|
||||
):
|
||||
raise PermissionDenied(
|
||||
_('You do not have permission to view this model type')
|
||||
)
|
||||
|
||||
items = serializer.validated_data['items']
|
||||
|
||||
instances = template.get_model().objects.filter(pk__in=items)
|
||||
instances = model_class.objects.filter(pk__in=items)
|
||||
|
||||
# Sort the instances by the order of the provided items
|
||||
instances = sorted(instances, key=lambda item: items.index(item.pk))
|
||||
|
||||
@@ -110,7 +110,7 @@ class ReportPrintSerializer(serializers.Serializer):
|
||||
fields = ['template', 'items']
|
||||
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=report.models.ReportTemplate.objects.all(),
|
||||
queryset=report.models.ReportTemplate.objects.filter(enabled=True),
|
||||
many=False,
|
||||
required=True,
|
||||
allow_null=False,
|
||||
@@ -151,7 +151,7 @@ class LabelPrintSerializer(serializers.Serializer):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=report.models.LabelTemplate.objects.all(),
|
||||
queryset=report.models.LabelTemplate.objects.filter(enabled=True),
|
||||
many=False,
|
||||
required=True,
|
||||
allow_null=False,
|
||||
|
||||
@@ -17,7 +17,7 @@ from build.models import Build
|
||||
from common.models import Attachment
|
||||
from common.settings import set_global_setting
|
||||
from InvenTree.unit_test import AdminTestCase, InvenTreeAPITestCase
|
||||
from order.models import ReturnOrder, SalesOrder
|
||||
from order.models import PurchaseOrder, ReturnOrder, SalesOrder
|
||||
from part.models import Part
|
||||
from plugin.registry import registry
|
||||
from report.models import LabelTemplate, ReportTemplate
|
||||
@@ -731,6 +731,140 @@ class TestReportTest(PrintTestMixins, ReportTest):
|
||||
self.run_print_test(SalesOrder, 'salesorder', label=False)
|
||||
|
||||
|
||||
class ReportPrintPermissionTest(InvenTreeAPITestCase):
|
||||
"""Test that the report print endpoint checks VIEW permission on the associated model type."""
|
||||
|
||||
fixtures = [
|
||||
'category',
|
||||
'part',
|
||||
'company',
|
||||
'location',
|
||||
'supplier_part',
|
||||
'stock',
|
||||
'order',
|
||||
]
|
||||
|
||||
superuser = False
|
||||
roles = []
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for permission tests."""
|
||||
cache.clear()
|
||||
apps.get_app_config('report').create_default_reports()
|
||||
return super().setUp()
|
||||
|
||||
def test_report_print_model_permission(self):
|
||||
"""A user without VIEW permission on the model type must receive 403; granting the role allows printing."""
|
||||
template = ReportTemplate.objects.filter(
|
||||
enabled=True, model_type='purchaseorder'
|
||||
).first()
|
||||
self.assertIsNotNone(template)
|
||||
|
||||
items = PurchaseOrder.objects.all()[:2]
|
||||
self.assertGreater(len(items), 0)
|
||||
|
||||
url = reverse('api-report-print')
|
||||
post_data = {'template': template.pk, 'items': [item.pk for item in items]}
|
||||
|
||||
# No roles assigned: expect permission denied
|
||||
self.post(url, data=post_data, expected_code=403)
|
||||
|
||||
# Grant view access to purchase orders
|
||||
self.assignRole('purchase_order.view')
|
||||
cache.clear()
|
||||
|
||||
# Should now succeed
|
||||
self.post(url, data=post_data, expected_code=201)
|
||||
|
||||
def test_report_print_disabled_template(self):
|
||||
"""Printing against a disabled report template must be rejected."""
|
||||
self.assignRole('purchase_order.view')
|
||||
cache.clear()
|
||||
|
||||
template = ReportTemplate.objects.filter(
|
||||
enabled=True, model_type='purchaseorder'
|
||||
).first()
|
||||
self.assertIsNotNone(template)
|
||||
|
||||
items = PurchaseOrder.objects.all()[:2]
|
||||
self.assertGreater(len(items), 0)
|
||||
|
||||
url = reverse('api-report-print')
|
||||
post_data = {'template': template.pk, 'items': [item.pk for item in items]}
|
||||
|
||||
# Enabled template: should succeed
|
||||
self.post(url, data=post_data, expected_code=201)
|
||||
|
||||
# Disable the template and retry: should be rejected
|
||||
template.enabled = False
|
||||
template.save()
|
||||
|
||||
self.post(url, data=post_data, expected_code=400)
|
||||
|
||||
|
||||
class LabelPrintPermissionTest(InvenTreeAPITestCase):
|
||||
"""Test that the label print endpoint checks VIEW permission on the associated model type."""
|
||||
|
||||
fixtures = ['category', 'part', 'company', 'location', 'supplier_part', 'stock']
|
||||
|
||||
superuser = False
|
||||
roles = []
|
||||
|
||||
def setUp(self):
|
||||
"""Setup for permission tests."""
|
||||
cache.clear()
|
||||
apps.get_app_config('report').create_default_labels()
|
||||
return super().setUp()
|
||||
|
||||
def test_label_print_model_permission(self):
|
||||
"""A user without VIEW permission on the model type must receive 403; granting the role allows printing."""
|
||||
template = LabelTemplate.objects.filter(enabled=True, model_type='part').first()
|
||||
self.assertIsNotNone(template)
|
||||
self.assertGreater(template.width, 0)
|
||||
self.assertGreater(template.height, 0)
|
||||
|
||||
items = Part.objects.all()[:2]
|
||||
self.assertGreater(len(items), 0)
|
||||
|
||||
url = reverse('api-label-print')
|
||||
post_data = {'template': template.pk, 'items': [item.pk for item in items]}
|
||||
|
||||
# No roles assigned: expect permission denied
|
||||
self.post(url, data=post_data, expected_code=403)
|
||||
|
||||
# Grant view access to parts
|
||||
self.assignRole('part.view')
|
||||
cache.clear()
|
||||
|
||||
# Should now succeed
|
||||
self.post(url, data=post_data, expected_code=201)
|
||||
|
||||
def test_label_print_disabled_template(self):
|
||||
"""Printing against a disabled label template must be rejected."""
|
||||
self.assignRole('part.view')
|
||||
cache.clear()
|
||||
|
||||
template = LabelTemplate.objects.filter(enabled=True, model_type='part').first()
|
||||
self.assertIsNotNone(template)
|
||||
self.assertGreater(template.width, 0)
|
||||
self.assertGreater(template.height, 0)
|
||||
|
||||
items = Part.objects.all()[:2]
|
||||
self.assertGreater(len(items), 0)
|
||||
|
||||
url = reverse('api-label-print')
|
||||
post_data = {'template': template.pk, 'items': [item.pk for item in items]}
|
||||
|
||||
# Enabled template: should succeed
|
||||
self.post(url, data=post_data, expected_code=201)
|
||||
|
||||
# Disable the template and retry: should be rejected
|
||||
template.enabled = False
|
||||
template.save()
|
||||
|
||||
self.post(url, data=post_data, expected_code=400)
|
||||
|
||||
|
||||
class AdminTest(AdminTestCase):
|
||||
"""Tests for the admin interface integration."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user