diff --git a/src/backend/InvenTree/order/api.py b/src/backend/InvenTree/order/api.py index d6b66ae4c4..eed3f866eb 100644 --- a/src/backend/InvenTree/order/api.py +++ b/src/backend/InvenTree/order/api.py @@ -64,6 +64,7 @@ from order.status_codes import ( ) from part.models import Part from users.models import Owner +from users.permissions import check_user_permission class GeneralExtraLineListOutputOptions(OutputConfiguration): @@ -2386,6 +2387,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. @@ -2402,6 +2429,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 @@ -2420,6 +2449,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 28ee156c1e..418bb1e1c2 100644 --- a/src/backend/InvenTree/order/test_api.py +++ b/src/backend/InvenTree/order/test_api.py @@ -853,6 +853,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. @@ -2111,6 +2126,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) @@ -3564,6 +3594,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 @@ -4114,6 +4159,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()