Repository: techmatters/terraso-backend

Issue:
Restrict upload file types to only allowed types
## Description
Currently, the list of allowed file types/extensions is only enforced on the client. It should be enforced on the backend.


Discussion:

Resolving patch:
diff --git a/terraso_backend/apps/shared_data/views.py b/terraso_backend/apps/shared_data/views.py
--- a/terraso_backend/apps/shared_data/views.py
+++ b/terraso_backend/apps/shared_data/views.py
@@ -15,9 +15,10 @@
 
 import mimetypes
 from dataclasses import asdict
+from pathlib import Path
 
 import structlog
-from config.settings import MEDIA_UPLOAD_MAX_FILE_SIZE
+from config.settings import DATA_ENTRY_ACCEPTED_EXTENSIONS, MEDIA_UPLOAD_MAX_FILE_SIZE
 from django.http import JsonResponse
 from django.views.generic.edit import FormView
 
@@ -52,6 +53,12 @@ def post(self, request, **kwargs):
                 context=ErrorContext(model="DataEntry", field="data_file"),
             )
             return JsonResponse({"errors": [{"message": [asdict(error_message)]}]}, status=400)
+        if not is_valid_shared_data_type(request.FILES.getlist("data_file")):
+            error_message = ErrorMessage(
+                code="invalid_media_type",
+                context=ErrorContext(model="Shared Data", field="context_type"),
+            )
+            return JsonResponse({"errors": [{"message": [asdict(error_message)]}]}, status=400)
 
         entry_form = DataEntryForm(data=form_data, files=request.FILES)
 
@@ -66,6 +73,10 @@ def post(self, request, **kwargs):
         return JsonResponse(data_entry.to_dict(), status=201)
 
 
+def is_valid_shared_data_type(files):
+    return all(Path(str(file)).suffix in DATA_ENTRY_ACCEPTED_EXTENSIONS for file in files)
+
+
 def get_error_messages(validation_errors):
     error_messages = []
 
diff --git a/terraso_backend/apps/story_map/views.py b/terraso_backend/apps/story_map/views.py
--- a/terraso_backend/apps/story_map/views.py
+++ b/terraso_backend/apps/story_map/views.py
@@ -67,7 +67,7 @@ def post(self, request, **kwargs):
             )
             return JsonResponse({"errors": [{"message": [asdict(error_message)]}]}, status=400)
 
-        if invalid_media_type(config):
+        if not is_valid_media_type(config):
             error_message = ErrorMessage(
                 code="Invalid Media Type",
                 context=ErrorContext(model="StoryMap", field="configuration"),
@@ -117,7 +117,7 @@ def post(self, request, **kwargs):
 
         new_config = json.loads(form_data["configuration"])
 
-        if invalid_media_type(new_config):
+        if not is_valid_media_type(new_config):
             error_message = ErrorMessage(
                 code="Invalid Media Type",
                 context=ErrorContext(model="StoryMap", field="configuration"),
@@ -194,13 +194,13 @@ def handle_config_media(new_config, current_config, request):
     return new_config
 
 
-def invalid_media_type(config):
+def is_valid_media_type(config):
     if "chapters" in config:
-        for chapter in config["chapters"]:
-            media = chapter.get("media")
-            if media and not (media["type"].startswith(("image", "audio", "video", "embedded"))):
-                return True
-        return False
+        return all(
+            chapter.get("media") is None
+            or chapter["media"]["type"].startswith(("image", "audio", "video", "embedded"))
+            for chapter in config["chapters"]
+        )
 
 
 def handle_integrity_error(exc):
diff --git a/terraso_backend/config/settings.py b/terraso_backend/config/settings.py
--- a/terraso_backend/config/settings.py
+++ b/terraso_backend/config/settings.py
@@ -326,6 +326,7 @@
 DATA_ENTRY_ACCEPTED_TYPES = (
     DATA_ENTRY_DOCUMENT_TYPES | DATA_ENTRY_SPREADSHEET_TYPES | DATA_ENTRY_GIS_TYPES
 )
+DATA_ENTRY_ACCEPTED_EXTENSIONS = list(DATA_ENTRY_ACCEPTED_TYPES.keys())
 
 DB_BACKUP_S3_BUCKET = config("DB_BACKUP_S3_BUCKET", default="")
 


Test patch:
diff --git a/terraso_backend/tests/story_map/test_views.py b/terraso_backend/tests/story_map/test_views.py
--- a/terraso_backend/tests/story_map/test_views.py
+++ b/terraso_backend/tests/story_map/test_views.py
@@ -278,3 +278,31 @@ def test_update_upload_multiple_media_invalid(logged_client, users):
     assert response.status_code == 400
     assert response_data["errors"][0]["message"][0]["code"] == "Invalid Media Type"
     assert "errors" in response_data
+
+
+def test_add_null_media_valid(logged_client, users):
+    story_map = mixer.blend("story_map.StoryMap", created_by=users[0])
+    url = reverse("story_map:add")
+    data = {
+        "id": story_map.pk,
+        "title": "Test StoryMap Updated",
+        "is_published": "true",
+        "configuration": json.dumps(
+            {
+                "title": "Test StoryMap Updated",
+                "chapters": [
+                    {
+                        "id": "chapter-1",
+                        "title": "Chapter 1",
+                        "description": "Chapter 1 description",
+                    },
+                ],
+            }
+        ),
+    }
+    response = logged_client.post(url, data=data)
+
+    assert response.status_code == 201
+    assert response.json()["title"] == data["title"]
+    assert response.json()["created_by"] == str(users[0].id)
+    assert response.json()["is_published"]