diff --git a/src/backend/InvenTree/InvenTree/unit_test.py b/src/backend/InvenTree/InvenTree/unit_test.py index 4576cdce6e..aee89660f3 100644 --- a/src/backend/InvenTree/InvenTree/unit_test.py +++ b/src/backend/InvenTree/InvenTree/unit_test.py @@ -142,7 +142,15 @@ class UserMixin: def setUp(self): """Run setup for individual test methods.""" if self.auto_login: - self.client.login(username=self.username, password=self.password) + self.login() + + def login(self): + """Login with the current user credentials.""" + self.client.login(username=self.username, password=self.password) + + def logout(self): + """Lougout current user.""" + self.client.logout() @classmethod def assignRole(cls, role=None, assign_all: bool = False, group=None): diff --git a/src/backend/InvenTree/report/api.py b/src/backend/InvenTree/report/api.py index 1782d95ed9..c4e566067b 100644 --- a/src/backend/InvenTree/report/api.py +++ b/src/backend/InvenTree/report/api.py @@ -18,6 +18,7 @@ from rest_framework.response import Response import common.models import InvenTree.exceptions import InvenTree.helpers +import InvenTree.permissions import report.helpers import report.models import report.serializers @@ -34,6 +35,16 @@ from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin from plugin.registry import registry +class TemplatePermissionMixin: + """Permission mixin for report and label templates.""" + + # Read only for non-staff users + permission_classes = [ + permissions.IsAuthenticated, + InvenTree.permissions.IsStaffOrReadOnly, + ] + + @method_decorator(cache_page(5), name='dispatch') class TemplatePrintBase(RetrieveAPI): """Base class for printing against templates.""" @@ -143,6 +154,7 @@ class LabelFilter(ReportFilterBase): class LabelPrint(GenericAPIView): """API endpoint for printing labels.""" + # Any authenticated user can print labels permission_classes = [permissions.IsAuthenticated] serializer_class = report.serializers.LabelPrintSerializer @@ -277,7 +289,7 @@ class LabelPrint(GenericAPIView): ) -class LabelTemplateList(ListCreateAPI): +class LabelTemplateList(TemplatePermissionMixin, ListCreateAPI): """API endpoint for viewing list of LabelTemplate objects.""" queryset = report.models.LabelTemplate.objects.all() @@ -288,7 +300,7 @@ class LabelTemplateList(ListCreateAPI): ordering_fields = ['name', 'enabled'] -class LabelTemplateDetail(RetrieveUpdateDestroyAPI): +class LabelTemplateDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI): """Detail API endpoint for label template model.""" queryset = report.models.LabelTemplate.objects.all() @@ -298,6 +310,7 @@ class LabelTemplateDetail(RetrieveUpdateDestroyAPI): class ReportPrint(GenericAPIView): """API endpoint for printing reports.""" + # Any authenticated user can print reports permission_classes = [permissions.IsAuthenticated] serializer_class = report.serializers.ReportPrintSerializer @@ -434,7 +447,7 @@ class ReportPrint(GenericAPIView): ) -class ReportTemplateList(ListCreateAPI): +class ReportTemplateList(TemplatePermissionMixin, ListCreateAPI): """API endpoint for viewing list of ReportTemplate objects.""" queryset = report.models.ReportTemplate.objects.all() @@ -445,49 +458,49 @@ class ReportTemplateList(ListCreateAPI): ordering_fields = ['name', 'enabled'] -class ReportTemplateDetail(RetrieveUpdateDestroyAPI): +class ReportTemplateDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI): """Detail API endpoint for report template model.""" queryset = report.models.ReportTemplate.objects.all() serializer_class = report.serializers.ReportTemplateSerializer -class ReportSnippetList(ListCreateAPI): +class ReportSnippetList(TemplatePermissionMixin, ListCreateAPI): """API endpoint for listing ReportSnippet objects.""" queryset = report.models.ReportSnippet.objects.all() serializer_class = report.serializers.ReportSnippetSerializer -class ReportSnippetDetail(RetrieveUpdateDestroyAPI): +class ReportSnippetDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single ReportSnippet object.""" queryset = report.models.ReportSnippet.objects.all() serializer_class = report.serializers.ReportSnippetSerializer -class ReportAssetList(ListCreateAPI): +class ReportAssetList(TemplatePermissionMixin, ListCreateAPI): """API endpoint for listing ReportAsset objects.""" queryset = report.models.ReportAsset.objects.all() serializer_class = report.serializers.ReportAssetSerializer -class ReportAssetDetail(RetrieveUpdateDestroyAPI): +class ReportAssetDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI): """API endpoint for a single ReportAsset object.""" queryset = report.models.ReportAsset.objects.all() serializer_class = report.serializers.ReportAssetSerializer -class LabelOutputList(BulkDeleteMixin, ListAPI): +class LabelOutputList(TemplatePermissionMixin, BulkDeleteMixin, ListAPI): """List endpoint for LabelOutput objects.""" queryset = report.models.LabelOutput.objects.all() serializer_class = report.serializers.LabelOutputSerializer -class ReportOutputList(BulkDeleteMixin, ListAPI): +class ReportOutputList(TemplatePermissionMixin, BulkDeleteMixin, ListAPI): """List endpoint for ReportOutput objects.""" queryset = report.models.ReportOutput.objects.all() diff --git a/src/backend/InvenTree/report/tests.py b/src/backend/InvenTree/report/tests.py index cdf115b7a2..bf30286f15 100644 --- a/src/backend/InvenTree/report/tests.py +++ b/src/backend/InvenTree/report/tests.py @@ -403,6 +403,61 @@ class ReportTest(InvenTreeAPITestCase): self.assertEqual(len(p.metadata.keys()), 4) + def test_report_template_permissions(self): + """Test that the user permissions are correctly applied. + + - For all /api/report/ endpoints, any authenticated user should have full read access + - Write access is limited to staff users + - Non authenticated users should have no access at all + """ + # First test the "report list" endpoint + url = reverse('api-report-template-list') + + template = ReportTemplate.objects.first() + + detail_url = reverse('api-report-template-detail', kwargs={'pk': template.pk}) + + # Non-authenticated user should have no access + self.logout() + + self.get(url, expected_code=401) + + # Authenticated user should have read access + self.user.is_staff = False + self.user.save() + + self.login() + + # Check read access to template list URL + self.get(url, expected_code=200) + + # Check read access to template detail URL + self.get(detail_url, expected_code=200) + + # An update to the report template should fail + self.patch( + detail_url, + data={'description': 'Some new description here?'}, + expected_code=403, + ) + + # Now, test with a staff user + self.logout() + + self.user.is_staff = True + self.user.save() + + self.login() + + self.patch( + detail_url, + data={'description': 'An updated description'}, + expected_code=200, + ) + + template.refresh_from_db() + self.assertEqual(template.description, 'An updated description') + class PrintTestMixins: """Mixin that enables e2e printing tests."""