diff --git a/docs/env.md b/docs/env.md index 0a878302..b1291cf9 100644 --- a/docs/env.md +++ b/docs/env.md @@ -45,6 +45,7 @@ This document lists all configurable environment variables for the Drive applica | `FEATURES_ALPHA` | Enable alpha features | `False` | | `FEATURES_INDEXED_SEARCH` | Enable the search of indexed files through the API | `True` | | `FILE_EXTENSIONS_ALLOWED` | List of file extension allowed to be uploaded | See in the settings.py file | +| `FILE_MIMETYPE_ALLOWED` | List of file mimetype allowed to be uploaded | See in the setings.py file | | `FRONTEND_THEME` | Frontend theme configuration | `None` | | `FRONTEND_FEEDBACK_BUTTON_SHOW` | Show feedback button | `False` | | `FRONTEND_FEEDBACK_BUTTON_IDLE` | Make feedback button idle (e.g. to bind to external library) | `False` | diff --git a/src/backend/core/api/serializers.py b/src/backend/core/api/serializers.py index c5df90fb..a3bb5dd5 100644 --- a/src/backend/core/api/serializers.py +++ b/src/backend/core/api/serializers.py @@ -436,7 +436,11 @@ class CreateItemSerializer(ItemSerializer): if settings.RESTRICT_UPLOAD_FILE_TYPE: _root, extension = splitext(attrs["filename"]) if extension not in settings.FILE_EXTENSIONS_ALLOWED: - logger.info("create_item: file extension not allowed %s for filename %s", extension, attrs["filename"]) + logger.info( + "create_item: file extension not allowed %s for filename %s", + extension, + attrs["filename"], + ) raise serializers.ValidationError( {"filename": _("This file extension is not allowed.")}, code="item_create_file_extension_not_allowed", diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index d607a0b7..fd357256 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -682,9 +682,7 @@ class ItemViewSet( entitlements_backend = get_entitlements_backend() can_upload = entitlements_backend.can_upload(self.request.user) if not can_upload["result"]: - item.soft_delete() - item.hard_delete() - process_item_deletion.delay(item.id) + self._complete_item_deletion(item) raise drf.exceptions.PermissionDenied( detail=can_upload.get( "message", "You do not have permission to upload files." @@ -713,6 +711,21 @@ class ItemViewSet( # Use improved MIME type detection combining magic bytes and file extension mimetype = utils.detect_mimetype(file_head, filename=item.filename) + if ( + settings.RESTRICT_UPLOAD_FILE_TYPE + and mimetype not in settings.FILE_MIMETYPE_ALLOWED + ): + self._complete_item_deletion(item) + logger.info( + "upload_ended: mimetype not allowed %s for filename %s", + mimetype, + item.filename, + ) + raise drf.exceptions.ValidationError( + detail="The file type is not allowed.", + code="file_type_not_allowed", + ) + item.upload_state = models.ItemUploadStateChoices.ANALYZING item.mimetype = mimetype item.size = file_size @@ -737,6 +750,12 @@ class ItemViewSet( return drf_response.Response(serializer.data, status=status.HTTP_200_OK) + def _complete_item_deletion(self, item): + """Completely delete an item.""" + item.soft_delete() + item.hard_delete() + process_item_deletion.delay(item.id) + @drf.decorators.action( detail=False, methods=["get"], diff --git a/src/backend/core/tests/items/test_api_item_upload_ended.py b/src/backend/core/tests/items/test_api_item_upload_ended.py index 2ebffea3..df43aec3 100644 --- a/src/backend/core/tests/items/test_api_item_upload_ended.py +++ b/src/backend/core/tests/items/test_api_item_upload_ended.py @@ -1,5 +1,6 @@ """Test related to item upload ended API.""" +import logging from io import BytesIO from unittest import mock @@ -237,3 +238,67 @@ def test_api_item_upload_ended_entitlements_backend_returns_falsy_custom_message } assert not models.Item.objects.filter(id=item.id).exists() + + +def test_api_item_upload_ended_mimetype_not_allowed(settings, caplog): + """ + Test that the API returns a 400 when the mimetype is not allowed. + Item should be deleted and the file should be deleted from the storage. + """ + settings.RESTRICT_UPLOAD_FILE_TYPE = True + settings.FILE_MIMETYPE_ALLOWED = ["application/pdf"] + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + item = factories.ItemFactory(type=ItemTypeChoices.FILE, filename="my_file.txt") + factories.UserItemAccessFactory(item=item, user=user, role="owner") + + default_storage.save( + item.file_key, + BytesIO(b"my prose"), + ) + + with caplog.at_level(logging.INFO): + response = client.post(f"/api/v1.0/items/{item.id!s}/upload-ended/") + + assert response.status_code == 400 + assert ( + "upload_ended: mimetype not allowed text/plain for filename my_file.txt" + in caplog.text + ) + + assert not models.Item.objects.filter(id=item.id).exists() + assert not default_storage.exists(item.file_key) + + +def test_api_item_upload_ended_mimetype_not_allowed_not_checking_mimetype(settings): + """ + Test that the API returns a 200 when the mimetype is not allowed but not checking the mimetype. + """ + settings.RESTRICT_UPLOAD_FILE_TYPE = False + settings.FILE_MIMETYPE_ALLOWED = ["application/pdf"] + user = factories.UserFactory() + client = APIClient() + client.force_login(user) + + item = factories.ItemFactory(type=ItemTypeChoices.FILE, filename="my_file.txt") + factories.UserItemAccessFactory(item=item, user=user, role="owner") + + default_storage.save( + item.file_key, + BytesIO(b"my prose"), + ) + + with mock.patch.object(malware_detection, "analyse_file") as mock_analyse_file: + response = client.post(f"/api/v1.0/items/{item.id!s}/upload-ended/") + + mock_analyse_file.assert_called_once_with(item.file_key, item_id=item.id) + assert response.status_code == 200 + + item.refresh_from_db() + assert item.upload_state == ItemUploadStateChoices.ANALYZING + assert item.mimetype == "text/plain" + assert item.size == 8 + + assert response.json()["mimetype"] == "text/plain" diff --git a/src/backend/drive/settings.py b/src/backend/drive/settings.py index a811d4e6..2e552202 100755 --- a/src/backend/drive/settings.py +++ b/src/backend/drive/settings.py @@ -552,6 +552,166 @@ class Base(Configuration): environ_prefix=None, ) + FILE_MIMETYPE_ALLOWED = values.ListValue( + [ + "application/epub+zip", + "application/gml+xml", + "application/gpx+xml", + "application/gzip", + "application/json", + "application/msword", + "application/octet-stream", + "application/pdf", + "application/pgp-signature", + "application/pkcs10", + "application/pkcs7-signature", + "application/pkix-cert", + "application/postscript", + "application/rtf", + "application/sql", + "application/vnd.android.package-archive", + "application/vnd.businessobjects", + "application/vnd.chemdraw+xml", + "application/vnd.dna", + "application/vnd.dynageo", + "application/vnd.google-earth.kml+xml", + "application/vnd.google-earth.kmz", + "application/vnd.koan", + "application/vnd.mcd", + "application/vnd.ms-excel", + "application/vnd.ms-excel.sheet.binary.macroenabled.12", + "application/vnd.ms-excel.sheet.macroenabled.12", + "application/vnd.ms-excel.template.macroenabled.12", + "application/vnd.ms-fontobject", + "application/vnd.ms-outlook", + "application/vnd.ms-pki.stl", + "application/vnd.ms-powerpoint", + "application/vnd.ms-powerpoint.presentation.macroenabled.12", + "application/vnd.ms-powerpoint.slideshow.macroenabled.12", + "application/vnd.ms-powerpoint.template.macroenabled.12", + "application/vnd.ms-project", + "application/vnd.ms-word.document.macroenabled.12", + "application/vnd.ms-word.template.macroenabled.12", + "application/vnd.ms-xpsdocument", + "application/vnd.oasis.opendocument.chart", + "application/vnd.oasis.opendocument.chart-template", + "application/vnd.oasis.opendocument.database", + "application/vnd.oasis.opendocument.formula", + "application/vnd.oasis.opendocument.graphics", + "application/vnd.oasis.opendocument.graphics-template", + "application/vnd.oasis.opendocument.image", + "application/vnd.oasis.opendocument.image-template", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.presentation-template", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.spreadsheet-template", + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.text-master", + "application/vnd.oasis.opendocument.text-template", + "application/vnd.oasis.opendocument.text-web", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + "application/vnd.openxmlformats-officedocument.presentationml.template", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + "application/vnd.palm", + "application/vnd.realvnc.bed", + "application/vnd.sun.xml.calc", + "application/vnd.sun.xml.calc.template", + "application/vnd.sun.xml.draw", + "application/vnd.sun.xml.draw.template", + "application/vnd.sun.xml.impress", + "application/vnd.sun.xml.math", + "application/vnd.sun.xml.writer", + "application/vnd.sun.xml.writer.global", + "application/vnd.sun.xml.writer.template", + "application/vnd.visio", + "application/vnd.yamaha.openscoreformat", + "application/x-7z-compressed", + "application/x-bzip", + "application/x-bzip2", + "application/x-debian-package", + "application/x-empty", + "application/x-freearc", + "application/x-keepass", + "application/x-keepass2", + "application/x-msaccess", + "application/x-msmetafile", + "application/x-mspublisher", + "application/x-python-bytecodeapplication/x-shellscript", + "application/x-python-code", + "application/x-rar", + "application/x-rar-compressed", + "application/x-research-info-systems", + "application/x-sh", + "application/x-sql", + "application/x-subrip", + "application/x-tar", + "application/x-tex", + "application/x-tgif", + "application/x-x509-ca-cert", + "application/x-xz", + "application/xml", + "application/yaml", + "application/zip", + "audio/aac", + "audio/basic", + "audio/mp4", + "audio/mpeg", + "audio/wav", + "audio/webm", + "audio/x-aac", + "audio/x-aiff", + "audio/x-wav", + "chemical/x-cif", + "font/collection", + "font/otf", + "font/ttf", + "font/woff", + "font/woff2", + "image/bmp", + "image/gif", + "image/heic", + "image/jpeg", + "image/png", + "image/svg+xml", + "image/tiff", + "image/vnd.adobe.photoshop", + "image/vnd.djvu", + "image/vnd.dwg", + "image/vnd.dxf", + "image/webp", + "image/x-icon", + "message/rfc822", + "model/iges", + "text/calendar", + "text/csv", + "text/markdown", + "text/plain", + "text/tab-separated-values", + "text/vtt", + "text/x-c", + "text/x-python", + "text/x-vcard", + "video/3gpp", + "video/3gpp2", + "video/mp2t", + "video/mp4", + "video/mpeg", + "video/quicktime", + "video/webm", + "video/x-m4v", + "video/x-matroska", + "video/x-msvideo", + # Fallback + "application/octet-stream", + ], + environ_name="FILE_MIMETYPE_ALLOWED", + environt_prefix=None, + ) + ITEM_PREVIEWABLE_MIME_TYPES = values.ListValue( [ "image/",