2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-11 19:27:02 +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:
Oliver
2026-06-10 16:59:39 +10:00
committed by GitHub
parent a0d0dff237
commit 73bfa53a35
6 changed files with 165 additions and 6 deletions
+1
View File
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- [#12142](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.
- [#11990](https://github.com/inventree/InvenTree/pull/11990) build output operations performed via the API now offload the work to a background task, and now return a task ID which can be used to monitor the progress of the task. This allows for better performance and responsiveness when performing build output operations, as the work is performed asynchronously in the background.
- [#11825](https://github.com/inventree/InvenTree/pull/11825) adds a new "bom" ruleset and associated permissions for BOM management, separate from the "part" ruleset which remains focused on part management. This allows for more granular control over user permissions, allowing users to have different levels of access to part management and BOM management functionality.
- [#11816](https://github.com/inventree/InvenTree/pull/11816) makes the `issued_by` field on the `Build` API read only, and instead sets the `issued_by` field to the current user when a build is created. This change was made to ensure that the `issued_by` field accurately reflects the user who created the build, and to prevent users from setting this field to an arbitrary value when creating or updating a build.
@@ -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)
+2
View File
@@ -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'
+21 -2
View File
@@ -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))
+2 -2
View File
@@ -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,
+135 -1
View File
@@ -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."""