From 1238daa783ef53d4fac910081084443c0bd993ac Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 2 Sep 2026 14:47:34 +1000 Subject: [PATCH] Permissions fix for Calendar views (#12764) --- src/backend/InvenTree/order/api.py | 31 +++++++++++++ src/backend/InvenTree/order/test_api.py | 60 +++++++++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index a357ed61eb..34c4d73f59 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -71,6 +71,7 @@ from order.status_codes import ( ) from part.models import Part from users.models import Owner +from users.permissions import check_user_permission order_router = InvenTreeApiRouter() @@ -2497,6 +2498,32 @@ class OrderCalendarExport(ICalFeed): timezone = settings.TIME_ZONE file_name = 'calendar.ics' + # Map the URL 'ordertype' kwarg to the corresponding order model, + # so that access can be checked against the matching RuleSet + ORDER_MODELS = { + 'purchase-order': models.PurchaseOrder, + 'sales-order': models.SalesOrder, + 'return-order': models.ReturnOrder, + 'transfer-order': models.TransferOrder, + } + + def check_permission(self, request, **kwargs): + """Check that the requesting user has 'view' permission for the requested order type. + + Returns a 403 JsonResponse if the user lacks the required RuleSet permission, + or None if access is permitted. + """ + model = self.ORDER_MODELS.get(kwargs.get('ordertype')) + + if model is not None and not check_user_permission(request.user, model, 'view'): + response = JsonResponse({ + 'detail': 'You do not have permission to view this resource.' + }) + response.status_code = 403 + return response + + return None + def __call__(self, request, *args, **kwargs): """Overload call in order to check for authentication. @@ -2513,6 +2540,8 @@ class OrderCalendarExport(ICalFeed): if request.user.is_authenticated: # Authenticated on first try - maybe normal browser call? + if forbidden := self.check_permission(request, **kwargs): + return forbidden return super().__call__(request, *args, **kwargs) # No login yet - check in headers @@ -2531,6 +2560,8 @@ class OrderCalendarExport(ICalFeed): # Check again if request.user.is_authenticated: # Authenticated after second try + if forbidden := self.check_permission(request, **kwargs): + return forbidden return super().__call__(request, *args, **kwargs) # Still nothing - return Unauth. header with info on how to authenticate diff --git a/src/backend/InvenTree/order/test_api.py b/src/backend/InvenTree/order/test_api.py index bcb47df50f..421b64ebf2 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -921,6 +921,21 @@ class PurchaseOrderTest(OrderTest): ) self.assertEqual(response.status_code, 200) + def test_po_calendar_no_permission(self): + """Test that an authenticated user without purchase_order view permission is denied.""" + self.clearRoles() + + response = self.get( + reverse('api-po-so-calendar', kwargs={'ordertype': 'purchase-order'}), + expected_code=403, + format=None, + ) + + resp_dict = response.json() + self.assertEqual( + resp_dict['detail'], 'You do not have permission to view this resource.' + ) + def test_po_custom_status_query_count(self): """Test that listing PurchaseOrders with custom statuses does not cause N+1 queries. @@ -2291,6 +2306,21 @@ class SalesOrderTest(OrderTest): self.assertGreaterEqual(n_events, 1) self.assertEqual(number_orders_incl_complete, n_events) + def test_so_calendar_no_permission(self): + """Test that an authenticated user without sales_order view permission is denied.""" + self.clearRoles() + + response = self.get( + reverse('api-po-so-calendar', kwargs={'ordertype': 'sales-order'}), + expected_code=403, + format=None, + ) + + resp_dict = response.json() + self.assertEqual( + resp_dict['detail'], 'You do not have permission to view this resource.' + ) + def test_export(self): """Test we can export the SalesOrder list.""" set_global_setting(models.SalesOrder.UNLOCK_SETTING, True) @@ -3793,6 +3823,21 @@ class ReturnOrderTests(InvenTreeAPITestCase): calendar = Calendar.from_ical(response.content) self.assertIsInstance(calendar, Calendar) + def test_ro_calendar_no_permission(self): + """Test that an authenticated user without return_order view permission is denied.""" + self.clearRoles() + + response = self.get( + reverse('api-po-so-calendar', kwargs={'ordertype': 'return-order'}), + expected_code=403, + format=None, + ) + + resp_dict = response.json() + self.assertEqual( + resp_dict['detail'], 'You do not have permission to view this resource.' + ) + def test_export(self): """Test data export for the ReturnOrder API endpoints.""" # Export return orders @@ -4380,6 +4425,21 @@ class TransferOrderTest(OrderTest): self.assertGreaterEqual(n_events, 1) self.assertEqual(number_orders_incl_complete, n_events) + def test_transfer_order_calendar_no_permission(self): + """Test that an authenticated user without transfer_order view permission is denied.""" + self.clearRoles() + + response = self.get( + reverse('api-po-so-calendar', kwargs={'ordertype': 'transfer-order'}), + expected_code=403, + format=None, + ) + + resp_dict = response.json() + self.assertEqual( + resp_dict['detail'], 'You do not have permission to view this resource.' + ) + def test_export(self): """Test we can export the TransferOrder list.""" n = models.TransferOrder.objects.count()