diff --git a/InvenTree/InvenTree/admin.py b/InvenTree/InvenTree/admin.py new file mode 100644 index 0000000000..2d5798a9d1 --- /dev/null +++ b/InvenTree/InvenTree/admin.py @@ -0,0 +1,33 @@ +"""Admin classes""" + +from import_export.resources import ModelResource + + +class InvenTreeResource(ModelResource): + """Custom subclass of the ModelResource class provided by django-import-export" + + Ensures that exported data are escaped to prevent malicious formula injection. + Ref: https://owasp.org/www-community/attacks/CSV_Injection + """ + + def export_resource(self, obj): + """Custom function to override default row export behaviour. + + Specifically, strip illegal leading characters to prevent formula injection + """ + row = super().export_resource(obj) + + illegal_start_vals = ['@', '=', '+', '-', '@', '\t', '\r', '\n'] + + for idx, val in enumerate(row): + if type(val) is str: + val = val.strip() + + # If the value starts with certain 'suspicious' values, remove it! + while len(val) > 0 and val[0] in illegal_start_vals: + # Remove the first character + val = val[1:] + + row[idx] = val + + return row diff --git a/InvenTree/build/admin.py b/InvenTree/build/admin.py index 5988850fe4..01dfb4e594 100644 --- a/InvenTree/build/admin.py +++ b/InvenTree/build/admin.py @@ -2,16 +2,15 @@ from django.contrib import admin from import_export.admin import ImportExportModelAdmin from import_export.fields import Field -from import_export.resources import ModelResource import import_export.widgets as widgets from build.models import Build, BuildItem - +from InvenTree.admin import InvenTreeResource import part.models -class BuildResource(ModelResource): - """Class for managing import/export of Build data""" +class BuildResource(InvenTreeResource): + """Class for managing import/export of Build data.""" # For some reason, we need to specify the fields individually for this ModelResource, # but we don't for other ones. # TODO: 2022-05-12 - Need to investigate why this is the case! diff --git a/InvenTree/company/admin.py b/InvenTree/company/admin.py index d3bf75dab3..d3fb63358f 100644 --- a/InvenTree/company/admin.py +++ b/InvenTree/company/admin.py @@ -3,8 +3,8 @@ from django.contrib import admin import import_export.widgets as widgets from import_export.admin import ImportExportModelAdmin from import_export.fields import Field -from import_export.resources import ModelResource +from InvenTree.admin import InvenTreeResource from part.models import Part from .models import (Company, ManufacturerPart, ManufacturerPartAttachment, @@ -12,8 +12,8 @@ from .models import (Company, ManufacturerPart, ManufacturerPartAttachment, SupplierPriceBreak) -class CompanyResource(ModelResource): - """ Class for managing Company data import/export """ +class CompanyResource(InvenTreeResource): + """Class for managing Company data import/export.""" class Meta: model = Company @@ -34,10 +34,8 @@ class CompanyAdmin(ImportExportModelAdmin): ] -class SupplierPartResource(ModelResource): - """ - Class for managing SupplierPart data import/export - """ +class SupplierPartResource(InvenTreeResource): + """Class for managing SupplierPart data import/export.""" part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) @@ -70,10 +68,8 @@ class SupplierPartAdmin(ImportExportModelAdmin): autocomplete_fields = ('part', 'supplier', 'manufacturer_part',) -class ManufacturerPartResource(ModelResource): - """ - Class for managing ManufacturerPart data import/export - """ +class ManufacturerPartResource(InvenTreeResource): + """Class for managing ManufacturerPart data import/export.""" part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part)) @@ -118,10 +114,8 @@ class ManufacturerPartAttachmentAdmin(ImportExportModelAdmin): autocomplete_fields = ('manufacturer_part',) -class ManufacturerPartParameterResource(ModelResource): - """ - Class for managing ManufacturerPartParameter data import/export - """ +class ManufacturerPartParameterResource(InvenTreeResource): + """Class for managing ManufacturerPartParameter data import/export.""" class Meta: model = ManufacturerPartParameter @@ -148,8 +142,8 @@ class ManufacturerPartParameterAdmin(ImportExportModelAdmin): autocomplete_fields = ('manufacturer_part',) -class SupplierPriceBreakResource(ModelResource): - """ Class for managing SupplierPriceBreak data import/export """ +class SupplierPriceBreakResource(InvenTreeResource): + """Class for managing SupplierPriceBreak data import/export.""" part = Field(attribute='part', widget=widgets.ForeignKeyWidget(SupplierPart)) diff --git a/InvenTree/order/admin.py b/InvenTree/order/admin.py index a46fe62532..aa24c095f6 100644 --- a/InvenTree/order/admin.py +++ b/InvenTree/order/admin.py @@ -1,9 +1,12 @@ +"""Admin functionality for the 'order' app""" + from django.contrib import admin import import_export.widgets as widgets from import_export.admin import ImportExportModelAdmin from import_export.fields import Field -from import_export.resources import ModelResource + +from InvenTree.admin import InvenTreeResource from .models import (PurchaseOrder, PurchaseOrderExtraLine, PurchaseOrderLineItem, SalesOrder, SalesOrderAllocation, @@ -13,6 +16,7 @@ from .models import (PurchaseOrder, PurchaseOrderExtraLine, # region general classes class GeneralExtraLineAdmin: + """Admin class template for the 'ExtraLineItem' models""" list_display = ( 'order', 'quantity', @@ -29,6 +33,7 @@ class GeneralExtraLineAdmin: class GeneralExtraLineMeta: + """Metaclass template for the 'ExtraLineItem' models""" skip_unchanged = True report_skipped = False clean_model_instances = True @@ -36,11 +41,13 @@ class GeneralExtraLineMeta: class PurchaseOrderLineItemInlineAdmin(admin.StackedInline): + """Inline admin class for the PurchaseOrderLineItem model""" model = PurchaseOrderLineItem extra = 0 class PurchaseOrderAdmin(ImportExportModelAdmin): + """Admin class for the PurchaseOrder model""" exclude = [ 'reference_int', @@ -68,6 +75,7 @@ class PurchaseOrderAdmin(ImportExportModelAdmin): class SalesOrderAdmin(ImportExportModelAdmin): + """Admin class for the SalesOrder model""" exclude = [ 'reference_int', @@ -90,10 +98,8 @@ class SalesOrderAdmin(ImportExportModelAdmin): autocomplete_fields = ('customer',) -class PurchaseOrderResource(ModelResource): - """ - Class for managing import / export of PurchaseOrder data - """ +class PurchaseOrderResource(InvenTreeResource): + """Class for managing import / export of PurchaseOrder data.""" # Add number of line items line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True) @@ -102,6 +108,7 @@ class PurchaseOrderResource(ModelResource): overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True) class Meta: + """Metaclass""" model = PurchaseOrder skip_unchanged = True clean_model_instances = True @@ -110,8 +117,8 @@ class PurchaseOrderResource(ModelResource): ] -class PurchaseOrderLineItemResource(ModelResource): - """ Class for managing import / export of PurchaseOrderLineItem data """ +class PurchaseOrderLineItemResource(InvenTreeResource): + """Class for managing import / export of PurchaseOrderLineItem data.""" part_name = Field(attribute='part__part__name', readonly=True) @@ -122,23 +129,24 @@ class PurchaseOrderLineItemResource(ModelResource): SKU = Field(attribute='part__SKU', readonly=True) class Meta: + """Metaclass""" model = PurchaseOrderLineItem skip_unchanged = True report_skipped = False clean_model_instances = True -class PurchaseOrderExtraLineResource(ModelResource): - """ Class for managing import / export of PurchaseOrderExtraLine data """ +class PurchaseOrderExtraLineResource(InvenTreeResource): + """Class for managing import / export of PurchaseOrderExtraLine data.""" class Meta(GeneralExtraLineMeta): + """Metaclass options.""" + model = PurchaseOrderExtraLine -class SalesOrderResource(ModelResource): - """ - Class for managing import / export of SalesOrder data - """ +class SalesOrderResource(InvenTreeResource): + """Class for managing import / export of SalesOrder data.""" # Add number of line items line_items = Field(attribute='line_count', widget=widgets.IntegerWidget(), readonly=True) @@ -147,6 +155,7 @@ class SalesOrderResource(ModelResource): overdue = Field(attribute='is_overdue', widget=widgets.BooleanWidget(), readonly=True) class Meta: + """Metaclass options""" model = SalesOrder skip_unchanged = True clean_model_instances = True @@ -155,10 +164,8 @@ class SalesOrderResource(ModelResource): ] -class SalesOrderLineItemResource(ModelResource): - """ - Class for managing import / export of SalesOrderLineItem data - """ +class SalesOrderLineItemResource(InvenTreeResource): + """Class for managing import / export of SalesOrderLineItem data.""" part_name = Field(attribute='part__name', readonly=True) @@ -169,31 +176,34 @@ class SalesOrderLineItemResource(ModelResource): fulfilled = Field(attribute='fulfilled_quantity', readonly=True) def dehydrate_sale_price(self, item): - """ - Return a string value of the 'sale_price' field, rather than the 'Money' object. + """Return a string value of the 'sale_price' field, rather than the 'Money' object. + Ref: https://github.com/inventree/InvenTree/issues/2207 """ - if item.sale_price: return str(item.sale_price) else: return '' class Meta: + """Metaclass options""" model = SalesOrderLineItem skip_unchanged = True report_skipped = False clean_model_instances = True -class SalesOrderExtraLineResource(ModelResource): - """ Class for managing import / export of SalesOrderExtraLine data """ +class SalesOrderExtraLineResource(InvenTreeResource): + """Class for managing import / export of SalesOrderExtraLine data.""" class Meta(GeneralExtraLineMeta): + """Metaclass options.""" + model = SalesOrderExtraLine class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): + """Admin class for the PurchaseOrderLine model""" resource_class = PurchaseOrderLineItemResource @@ -210,11 +220,12 @@ class PurchaseOrderLineItemAdmin(ImportExportModelAdmin): class PurchaseOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin): - + """Admin class for the PurchaseOrderExtraLine model""" resource_class = PurchaseOrderExtraLineResource class SalesOrderLineItemAdmin(ImportExportModelAdmin): + """Admin class for the SalesOrderLine model""" resource_class = SalesOrderLineItemResource @@ -236,11 +247,12 @@ class SalesOrderLineItemAdmin(ImportExportModelAdmin): class SalesOrderExtraLineAdmin(GeneralExtraLineAdmin, ImportExportModelAdmin): - + """Admin class for the SalesOrderExtraLine model""" resource_class = SalesOrderExtraLineResource class SalesOrderShipmentAdmin(ImportExportModelAdmin): + """Admin class for the SalesOrderShipment model""" list_display = [ 'order', @@ -258,6 +270,7 @@ class SalesOrderShipmentAdmin(ImportExportModelAdmin): class SalesOrderAllocationAdmin(ImportExportModelAdmin): + """Admin class for the SalesOrderAllocation model""" list_display = ( 'line', diff --git a/InvenTree/part/admin.py b/InvenTree/part/admin.py index 88064ff275..e82b29ba11 100644 --- a/InvenTree/part/admin.py +++ b/InvenTree/part/admin.py @@ -3,15 +3,15 @@ from django.contrib import admin import import_export.widgets as widgets from import_export.admin import ImportExportModelAdmin from import_export.fields import Field -from import_export.resources import ModelResource import part.models as models from company.models import SupplierPart +from InvenTree.admin import InvenTreeResource from stock.models import StockLocation -class PartResource(ModelResource): - """ Class for managing Part data import/export """ +class PartResource(InvenTreeResource): + """Class for managing Part data import/export.""" # ForeignKey fields category = Field(attribute='category', widget=widgets.ForeignKeyWidget(models.PartCategory)) @@ -81,8 +81,8 @@ class PartAdmin(ImportExportModelAdmin): ] -class PartCategoryResource(ModelResource): - """ Class for managing PartCategory data import/export """ +class PartCategoryResource(InvenTreeResource): + """Class for managing PartCategory data import/export.""" parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(models.PartCategory)) @@ -157,8 +157,8 @@ class PartTestTemplateAdmin(admin.ModelAdmin): autocomplete_fields = ('part',) -class BomItemResource(ModelResource): - """ Class for managing BomItem data import/export """ +class BomItemResource(InvenTreeResource): + """Class for managing BomItem data import/export.""" level = Field(attribute='level', readonly=True) @@ -269,8 +269,8 @@ class ParameterTemplateAdmin(ImportExportModelAdmin): search_fields = ('name', 'units') -class ParameterResource(ModelResource): - """ Class for managing PartParameter data import/export """ +class ParameterResource(InvenTreeResource): + """Class for managing PartParameter data import/export.""" part = Field(attribute='part', widget=widgets.ForeignKeyWidget(models.Part)) diff --git a/InvenTree/stock/admin.py b/InvenTree/stock/admin.py index 85d7b7afe0..80db75811d 100644 --- a/InvenTree/stock/admin.py +++ b/InvenTree/stock/admin.py @@ -3,10 +3,10 @@ from django.contrib import admin import import_export.widgets as widgets from import_export.admin import ImportExportModelAdmin from import_export.fields import Field -from import_export.resources import ModelResource from build.models import Build from company.models import Company, SupplierPart +from InvenTree.admin import InvenTreeResource from order.models import PurchaseOrder, SalesOrder from part.models import Part @@ -14,8 +14,8 @@ from .models import (StockItem, StockItemAttachment, StockItemTestResult, StockItemTracking, StockLocation) -class LocationResource(ModelResource): - """ Class for managing StockLocation data import/export """ +class LocationResource(InvenTreeResource): + """Class for managing StockLocation data import/export.""" parent = Field(attribute='parent', widget=widgets.ForeignKeyWidget(StockLocation)) @@ -65,8 +65,8 @@ class LocationAdmin(ImportExportModelAdmin): ] -class StockItemResource(ModelResource): - """ Class for managing StockItem data import/export """ +class StockItemResource(InvenTreeResource): + """Class for managing StockItem data import/export.""" # Custom managers for ForeignKey fields part = Field(attribute='part', widget=widgets.ForeignKeyWidget(Part))