mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-11-04 15:15:42 +00:00 
			
		
		
		
	Template permission fix (#7391)
* Update API permissions for report templates - Allow reading for any authenticated user - Write permissions for staff users * Update unit tests
This commit is contained in:
		@@ -142,8 +142,16 @@ class UserMixin:
 | 
				
			|||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        """Run setup for individual test methods."""
 | 
					        """Run setup for individual test methods."""
 | 
				
			||||||
        if self.auto_login:
 | 
					        if self.auto_login:
 | 
				
			||||||
 | 
					            self.login()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def login(self):
 | 
				
			||||||
 | 
					        """Login with the current user credentials."""
 | 
				
			||||||
        self.client.login(username=self.username, password=self.password)
 | 
					        self.client.login(username=self.username, password=self.password)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def logout(self):
 | 
				
			||||||
 | 
					        """Lougout current user."""
 | 
				
			||||||
 | 
					        self.client.logout()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @classmethod
 | 
					    @classmethod
 | 
				
			||||||
    def assignRole(cls, role=None, assign_all: bool = False, group=None):
 | 
					    def assignRole(cls, role=None, assign_all: bool = False, group=None):
 | 
				
			||||||
        """Set the user roles for the registered user.
 | 
					        """Set the user roles for the registered user.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,6 +18,7 @@ from rest_framework.response import Response
 | 
				
			|||||||
import common.models
 | 
					import common.models
 | 
				
			||||||
import InvenTree.exceptions
 | 
					import InvenTree.exceptions
 | 
				
			||||||
import InvenTree.helpers
 | 
					import InvenTree.helpers
 | 
				
			||||||
 | 
					import InvenTree.permissions
 | 
				
			||||||
import report.helpers
 | 
					import report.helpers
 | 
				
			||||||
import report.models
 | 
					import report.models
 | 
				
			||||||
import report.serializers
 | 
					import report.serializers
 | 
				
			||||||
@@ -34,6 +35,16 @@ from plugin.builtin.labels.inventree_label import InvenTreeLabelPlugin
 | 
				
			|||||||
from plugin.registry import registry
 | 
					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')
 | 
					@method_decorator(cache_page(5), name='dispatch')
 | 
				
			||||||
class TemplatePrintBase(RetrieveAPI):
 | 
					class TemplatePrintBase(RetrieveAPI):
 | 
				
			||||||
    """Base class for printing against templates."""
 | 
					    """Base class for printing against templates."""
 | 
				
			||||||
@@ -143,6 +154,7 @@ class LabelFilter(ReportFilterBase):
 | 
				
			|||||||
class LabelPrint(GenericAPIView):
 | 
					class LabelPrint(GenericAPIView):
 | 
				
			||||||
    """API endpoint for printing labels."""
 | 
					    """API endpoint for printing labels."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Any authenticated user can print labels
 | 
				
			||||||
    permission_classes = [permissions.IsAuthenticated]
 | 
					    permission_classes = [permissions.IsAuthenticated]
 | 
				
			||||||
    serializer_class = report.serializers.LabelPrintSerializer
 | 
					    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."""
 | 
					    """API endpoint for viewing list of LabelTemplate objects."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.LabelTemplate.objects.all()
 | 
					    queryset = report.models.LabelTemplate.objects.all()
 | 
				
			||||||
@@ -288,7 +300,7 @@ class LabelTemplateList(ListCreateAPI):
 | 
				
			|||||||
    ordering_fields = ['name', 'enabled']
 | 
					    ordering_fields = ['name', 'enabled']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LabelTemplateDetail(RetrieveUpdateDestroyAPI):
 | 
					class LabelTemplateDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
 | 
				
			||||||
    """Detail API endpoint for label template model."""
 | 
					    """Detail API endpoint for label template model."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.LabelTemplate.objects.all()
 | 
					    queryset = report.models.LabelTemplate.objects.all()
 | 
				
			||||||
@@ -298,6 +310,7 @@ class LabelTemplateDetail(RetrieveUpdateDestroyAPI):
 | 
				
			|||||||
class ReportPrint(GenericAPIView):
 | 
					class ReportPrint(GenericAPIView):
 | 
				
			||||||
    """API endpoint for printing reports."""
 | 
					    """API endpoint for printing reports."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Any authenticated user can print reports
 | 
				
			||||||
    permission_classes = [permissions.IsAuthenticated]
 | 
					    permission_classes = [permissions.IsAuthenticated]
 | 
				
			||||||
    serializer_class = report.serializers.ReportPrintSerializer
 | 
					    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."""
 | 
					    """API endpoint for viewing list of ReportTemplate objects."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportTemplate.objects.all()
 | 
					    queryset = report.models.ReportTemplate.objects.all()
 | 
				
			||||||
@@ -445,49 +458,49 @@ class ReportTemplateList(ListCreateAPI):
 | 
				
			|||||||
    ordering_fields = ['name', 'enabled']
 | 
					    ordering_fields = ['name', 'enabled']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportTemplateDetail(RetrieveUpdateDestroyAPI):
 | 
					class ReportTemplateDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
 | 
				
			||||||
    """Detail API endpoint for report template model."""
 | 
					    """Detail API endpoint for report template model."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportTemplate.objects.all()
 | 
					    queryset = report.models.ReportTemplate.objects.all()
 | 
				
			||||||
    serializer_class = report.serializers.ReportTemplateSerializer
 | 
					    serializer_class = report.serializers.ReportTemplateSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportSnippetList(ListCreateAPI):
 | 
					class ReportSnippetList(TemplatePermissionMixin, ListCreateAPI):
 | 
				
			||||||
    """API endpoint for listing ReportSnippet objects."""
 | 
					    """API endpoint for listing ReportSnippet objects."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportSnippet.objects.all()
 | 
					    queryset = report.models.ReportSnippet.objects.all()
 | 
				
			||||||
    serializer_class = report.serializers.ReportSnippetSerializer
 | 
					    serializer_class = report.serializers.ReportSnippetSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportSnippetDetail(RetrieveUpdateDestroyAPI):
 | 
					class ReportSnippetDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
 | 
				
			||||||
    """API endpoint for a single ReportSnippet object."""
 | 
					    """API endpoint for a single ReportSnippet object."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportSnippet.objects.all()
 | 
					    queryset = report.models.ReportSnippet.objects.all()
 | 
				
			||||||
    serializer_class = report.serializers.ReportSnippetSerializer
 | 
					    serializer_class = report.serializers.ReportSnippetSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportAssetList(ListCreateAPI):
 | 
					class ReportAssetList(TemplatePermissionMixin, ListCreateAPI):
 | 
				
			||||||
    """API endpoint for listing ReportAsset objects."""
 | 
					    """API endpoint for listing ReportAsset objects."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportAsset.objects.all()
 | 
					    queryset = report.models.ReportAsset.objects.all()
 | 
				
			||||||
    serializer_class = report.serializers.ReportAssetSerializer
 | 
					    serializer_class = report.serializers.ReportAssetSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportAssetDetail(RetrieveUpdateDestroyAPI):
 | 
					class ReportAssetDetail(TemplatePermissionMixin, RetrieveUpdateDestroyAPI):
 | 
				
			||||||
    """API endpoint for a single ReportAsset object."""
 | 
					    """API endpoint for a single ReportAsset object."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportAsset.objects.all()
 | 
					    queryset = report.models.ReportAsset.objects.all()
 | 
				
			||||||
    serializer_class = report.serializers.ReportAssetSerializer
 | 
					    serializer_class = report.serializers.ReportAssetSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class LabelOutputList(BulkDeleteMixin, ListAPI):
 | 
					class LabelOutputList(TemplatePermissionMixin, BulkDeleteMixin, ListAPI):
 | 
				
			||||||
    """List endpoint for LabelOutput objects."""
 | 
					    """List endpoint for LabelOutput objects."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.LabelOutput.objects.all()
 | 
					    queryset = report.models.LabelOutput.objects.all()
 | 
				
			||||||
    serializer_class = report.serializers.LabelOutputSerializer
 | 
					    serializer_class = report.serializers.LabelOutputSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ReportOutputList(BulkDeleteMixin, ListAPI):
 | 
					class ReportOutputList(TemplatePermissionMixin, BulkDeleteMixin, ListAPI):
 | 
				
			||||||
    """List endpoint for ReportOutput objects."""
 | 
					    """List endpoint for ReportOutput objects."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = report.models.ReportOutput.objects.all()
 | 
					    queryset = report.models.ReportOutput.objects.all()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -403,6 +403,61 @@ class ReportTest(InvenTreeAPITestCase):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        self.assertEqual(len(p.metadata.keys()), 4)
 | 
					        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:
 | 
					class PrintTestMixins:
 | 
				
			||||||
    """Mixin that enables e2e printing tests."""
 | 
					    """Mixin that enables e2e printing tests."""
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user