mirror of
https://github.com/inventree/InvenTree.git
synced 2025-04-28 11:36:44 +00:00
Importer permissions (#8419)
* Improved permission checking for data importing - Permission checks on the imported model type * Improved validation for DateField * Fix for unit test
This commit is contained in:
parent
504655c92d
commit
abad36786f
@ -5,6 +5,7 @@ from django.urls import include, path
|
|||||||
|
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import permissions
|
from rest_framework import permissions
|
||||||
|
from rest_framework.exceptions import PermissionDenied
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
@ -20,6 +21,39 @@ from InvenTree.mixins import (
|
|||||||
RetrieveUpdateAPI,
|
RetrieveUpdateAPI,
|
||||||
RetrieveUpdateDestroyAPI,
|
RetrieveUpdateDestroyAPI,
|
||||||
)
|
)
|
||||||
|
from users.models import check_user_permission
|
||||||
|
|
||||||
|
|
||||||
|
class DataImporterPermission(permissions.BasePermission):
|
||||||
|
"""Mixin class for determining if the user has correct permissions."""
|
||||||
|
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
"""Class level permission checks are handled via permissions.IsAuthenticated."""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_object_permission(self, request, view, obj):
|
||||||
|
"""Check if the user has permission to access the imported object."""
|
||||||
|
# For safe methods (GET, HEAD, OPTIONS), allow access
|
||||||
|
if request.method in permissions.SAFE_METHODS:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if isinstance(obj, importer.models.DataImportSession):
|
||||||
|
session = obj
|
||||||
|
else:
|
||||||
|
session = getattr(obj, 'session', None)
|
||||||
|
|
||||||
|
if session:
|
||||||
|
if model_class := session.model_class:
|
||||||
|
return check_user_permission(request.user, model_class, 'change')
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class DataImporterPermissionMixin:
|
||||||
|
"""Mixin class for checking permissions on DataImporter objects."""
|
||||||
|
|
||||||
|
# Default permissions: User must be authenticated
|
||||||
|
permission_classes = [permissions.IsAuthenticated, DataImporterPermission]
|
||||||
|
|
||||||
|
|
||||||
class DataImporterModelList(APIView):
|
class DataImporterModelList(APIView):
|
||||||
@ -44,11 +78,9 @@ class DataImporterModelList(APIView):
|
|||||||
return Response(models)
|
return Response(models)
|
||||||
|
|
||||||
|
|
||||||
class DataImportSessionList(BulkDeleteMixin, ListCreateAPI):
|
class DataImportSessionList(DataImporterPermission, BulkDeleteMixin, ListCreateAPI):
|
||||||
"""API endpoint for accessing a list of DataImportSession objects."""
|
"""API endpoint for accessing a list of DataImportSession objects."""
|
||||||
|
|
||||||
permission_classes = [permissions.IsAuthenticated]
|
|
||||||
|
|
||||||
queryset = importer.models.DataImportSession.objects.all()
|
queryset = importer.models.DataImportSession.objects.all()
|
||||||
serializer_class = importer.serializers.DataImportSessionSerializer
|
serializer_class = importer.serializers.DataImportSessionSerializer
|
||||||
|
|
||||||
@ -59,7 +91,7 @@ class DataImportSessionList(BulkDeleteMixin, ListCreateAPI):
|
|||||||
ordering_fields = ['timestamp', 'status', 'model_type']
|
ordering_fields = ['timestamp', 'status', 'model_type']
|
||||||
|
|
||||||
|
|
||||||
class DataImportSessionDetail(RetrieveUpdateDestroyAPI):
|
class DataImportSessionDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
|
||||||
"""Detail endpoint for a single DataImportSession object."""
|
"""Detail endpoint for a single DataImportSession object."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportSession.objects.all()
|
queryset = importer.models.DataImportSession.objects.all()
|
||||||
@ -78,13 +110,18 @@ class DataImportSessionAcceptFields(APIView):
|
|||||||
"""Accept the field mapping for a DataImportSession."""
|
"""Accept the field mapping for a DataImportSession."""
|
||||||
session = get_object_or_404(importer.models.DataImportSession, pk=pk)
|
session = get_object_or_404(importer.models.DataImportSession, pk=pk)
|
||||||
|
|
||||||
|
# Check that the user has permission to accept the field mapping
|
||||||
|
if model_class := session.model_class:
|
||||||
|
if not check_user_permission(request.user, model_class, 'change'):
|
||||||
|
raise PermissionDenied()
|
||||||
|
|
||||||
# Attempt to accept the mapping (may raise an exception if the mapping is invalid)
|
# Attempt to accept the mapping (may raise an exception if the mapping is invalid)
|
||||||
session.accept_mapping()
|
session.accept_mapping()
|
||||||
|
|
||||||
return Response(importer.serializers.DataImportSessionSerializer(session).data)
|
return Response(importer.serializers.DataImportSessionSerializer(session).data)
|
||||||
|
|
||||||
|
|
||||||
class DataImportSessionAcceptRows(CreateAPI):
|
class DataImportSessionAcceptRows(DataImporterPermission, CreateAPI):
|
||||||
"""API endpoint to accept the rows for a DataImportSession."""
|
"""API endpoint to accept the rows for a DataImportSession."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportSession.objects.all()
|
queryset = importer.models.DataImportSession.objects.all()
|
||||||
@ -105,7 +142,7 @@ class DataImportSessionAcceptRows(CreateAPI):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class DataImportColumnMappingList(ListAPI):
|
class DataImportColumnMappingList(DataImporterPermissionMixin, ListAPI):
|
||||||
"""API endpoint for accessing a list of DataImportColumnMap objects."""
|
"""API endpoint for accessing a list of DataImportColumnMap objects."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportColumnMap.objects.all()
|
queryset = importer.models.DataImportColumnMap.objects.all()
|
||||||
@ -116,14 +153,14 @@ class DataImportColumnMappingList(ListAPI):
|
|||||||
filterset_fields = ['session']
|
filterset_fields = ['session']
|
||||||
|
|
||||||
|
|
||||||
class DataImportColumnMappingDetail(RetrieveUpdateAPI):
|
class DataImportColumnMappingDetail(DataImporterPermissionMixin, RetrieveUpdateAPI):
|
||||||
"""Detail endpoint for a single DataImportColumnMap object."""
|
"""Detail endpoint for a single DataImportColumnMap object."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportColumnMap.objects.all()
|
queryset = importer.models.DataImportColumnMap.objects.all()
|
||||||
serializer_class = importer.serializers.DataImportColumnMapSerializer
|
serializer_class = importer.serializers.DataImportColumnMapSerializer
|
||||||
|
|
||||||
|
|
||||||
class DataImportRowList(BulkDeleteMixin, ListAPI):
|
class DataImportRowList(DataImporterPermission, BulkDeleteMixin, ListAPI):
|
||||||
"""API endpoint for accessing a list of DataImportRow objects."""
|
"""API endpoint for accessing a list of DataImportRow objects."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportRow.objects.all()
|
queryset = importer.models.DataImportRow.objects.all()
|
||||||
@ -138,7 +175,7 @@ class DataImportRowList(BulkDeleteMixin, ListAPI):
|
|||||||
ordering = 'row_index'
|
ordering = 'row_index'
|
||||||
|
|
||||||
|
|
||||||
class DataImportRowDetail(RetrieveUpdateDestroyAPI):
|
class DataImportRowDetail(DataImporterPermission, RetrieveUpdateDestroyAPI):
|
||||||
"""Detail endpoint for a single DataImportRow object."""
|
"""Detail endpoint for a single DataImportRow object."""
|
||||||
|
|
||||||
queryset = importer.models.DataImportRow.objects.all()
|
queryset = importer.models.DataImportRow.objects.all()
|
||||||
|
@ -123,6 +123,14 @@ class DataImportSession(models.Model):
|
|||||||
|
|
||||||
return mapping
|
return mapping
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model_class(self):
|
||||||
|
"""Return the model class for this importer."""
|
||||||
|
serializer = self.serializer_class
|
||||||
|
|
||||||
|
if serializer:
|
||||||
|
return serializer.Meta.model
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def serializer_class(self):
|
def serializer_class(self):
|
||||||
"""Return the serializer class for this importer."""
|
"""Return the serializer class for this importer."""
|
||||||
|
@ -1487,10 +1487,9 @@ class PurchaseOrderLineItem(OrderLineItem):
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Render a string representation of a PurchaseOrderLineItem instance."""
|
"""Render a string representation of a PurchaseOrderLineItem instance."""
|
||||||
return '{n} x {part} from {supplier} (for {po})'.format(
|
return '{n} x {part} - {po}'.format(
|
||||||
n=decimal2string(self.quantity),
|
n=decimal2string(self.quantity),
|
||||||
part=self.part.SKU if self.part else 'unknown part',
|
part=self.part.SKU if self.part else 'unknown part',
|
||||||
supplier=self.order.supplier.name if self.order.supplier else _('deleted'),
|
|
||||||
po=self.order,
|
po=self.order,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class OrderTest(TestCase):
|
|||||||
self.assertEqual(order.reference, f'PO-{pk:04d}')
|
self.assertEqual(order.reference, f'PO-{pk:04d}')
|
||||||
|
|
||||||
line = PurchaseOrderLineItem.objects.get(pk=1)
|
line = PurchaseOrderLineItem.objects.get(pk=1)
|
||||||
self.assertEqual(str(line), '100 x ACME0001 from ACME (for PO-0001 - ACME)')
|
self.assertEqual(str(line), '100 x ACME0001 - PO-0001 - ACME')
|
||||||
|
|
||||||
def test_rebuild_reference(self):
|
def test_rebuild_reference(self):
|
||||||
"""Test that the reference_int field is correctly updated when the model is saved."""
|
"""Test that the reference_int field is correctly updated when the model is saved."""
|
||||||
|
@ -687,6 +687,9 @@ def check_user_permission(user: User, model, permission):
|
|||||||
model: The model class to check (e.g. Part)
|
model: The model class to check (e.g. Part)
|
||||||
permission: The permission to check (e.g. 'view' / 'delete')
|
permission: The permission to check (e.g. 'view' / 'delete')
|
||||||
"""
|
"""
|
||||||
|
if user.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
permission_name = f'{model._meta.app_label}.{permission}_{model._meta.model_name}'
|
permission_name = f'{model._meta.app_label}.{permission}_{model._meta.model_name}'
|
||||||
return user.has_perm(permission_name)
|
return user.has_perm(permission_name)
|
||||||
|
|
||||||
|
@ -39,11 +39,18 @@ export default function DateField({
|
|||||||
[field.onChange, definition]
|
[field.onChange, definition]
|
||||||
);
|
);
|
||||||
|
|
||||||
const dateValue = useMemo(() => {
|
const dateValue: Date | null = useMemo(() => {
|
||||||
|
let dv: Date | null = null;
|
||||||
|
|
||||||
if (field.value) {
|
if (field.value) {
|
||||||
return new Date(field.value);
|
dv = new Date(field.value) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the date is valid
|
||||||
|
if (dv instanceof Date && !isNaN(dv.getTime())) {
|
||||||
|
return dv;
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
}, [field.value]);
|
}, [field.value]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user