From cb4ed3c9d77fd048c0c853765e6f32a2cc4a8ffd Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Thu, 12 Mar 2026 14:57:41 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A9=B9(backend)=20ignore=20non-recording?= =?UTF-8?q?=20uploads=20in=20storage=20webhook=20handler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the introduction of background file uploads, a misconfigured MinIO webhook could trigger the storage hook endpoint for unrelated files. While the dev setup now filters events via the MinIO lifecycle configuration, add a safeguard at the application level. Enforce a stricter filepath regex when parsing storage hook events and ignore files outside the recording output directory. Return a clean 200 response to acknowledge the webhook while avoiding unnecessary processing. --- CHANGELOG.md | 1 + src/backend/core/api/viewsets.py | 6 +++++ src/backend/core/recording/event/parsers.py | 2 +- .../tests/recording/event/test_parsers.py | 18 +++++++------- .../test_api_recordings_storage_hook.py | 24 +++++++++++++++++++ 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c0bb56..7ebf87ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to - 🐛(frontend) fix hand icon and queue position alignment and position #1119 - 🩹(backend) add page_size to pagination for room endpoints #1131 - 🐛(backend) refactor lobby throttling to use participant id #1129 +- 🩹(backend) ignore non-recording uploads in storage webhook handler #1142 ## [1.10.0] - 2026-03-05 diff --git a/src/backend/core/api/viewsets.py b/src/backend/core/api/viewsets.py index 36705659..aafada92 100644 --- a/src/backend/core/api/viewsets.py +++ b/src/backend/core/api/viewsets.py @@ -38,6 +38,7 @@ from core.recording.enums import FileExtension from core.recording.event.authentication import StorageEventAuthentication from core.recording.event.exceptions import ( InvalidBucketError, + InvalidFilepathError, InvalidFileTypeError, ParsingEventDataError, ) @@ -761,6 +762,11 @@ class RecordingViewSet( except InvalidBucketError as e: raise drf_exceptions.PermissionDenied("Invalid bucket specified") from e + except InvalidFilepathError as e: + return drf_response.Response( + {"message": f"Ignore this filepath, {e}"}, + ) + except InvalidFileTypeError as e: return drf_response.Response( {"message": f"Ignore this file type, {e}"}, diff --git a/src/backend/core/recording/event/parsers.py b/src/backend/core/recording/event/parsers.py index 8398beff..9cd93059 100644 --- a/src/backend/core/recording/event/parsers.py +++ b/src/backend/core/recording/event/parsers.py @@ -88,7 +88,7 @@ class MinioParser: # pylint: disable=line-too-long self._filepath_regex = re.compile( - rf"(?P(?:[^%]+%2F)+)?(?P{UUID_REGEX})\.(?P{FILE_EXT_REGEX})" + rf"(?P(?:[^%]+%2F)+)?{settings.RECORDING_OUTPUT_FOLDER}%2F(?P{UUID_REGEX})\.(?P{FILE_EXT_REGEX})" ) @staticmethod diff --git a/src/backend/core/tests/recording/event/test_parsers.py b/src/backend/core/tests/recording/event/test_parsers.py index 8316609b..5071ed54 100644 --- a/src/backend/core/tests/recording/event/test_parsers.py +++ b/src/backend/core/tests/recording/event/test_parsers.py @@ -32,7 +32,7 @@ def valid_minio_event(): "s3": { "bucket": {"name": "test-bucket"}, "object": { - "key": "recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", + "key": "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", "contentType": "audio/ogg", }, } @@ -51,7 +51,7 @@ def test_parse_valid_event(minio_parser, valid_minio_event): """Test parsing a valid Minio event.""" event = minio_parser.parse(valid_minio_event) assert isinstance(event, StorageEvent) - assert event.filepath == "recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg" + assert event.filepath == "recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg" assert event.filetype == "audio/ogg" assert event.bucket_name == "test-bucket" assert event.metadata is None @@ -130,11 +130,13 @@ def test_validate_invalid_filetype(minio_parser): "invalid_filepath", [ "invalid_filepath", # totally invalid string - "recording/46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", - "recording/46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing extension + "recordings/46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", + "recordings/46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing extension "46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing url_encoded_folder_path and extension "", # empty string - "recording%2F46d1a1212426484d8fb309b5d886f7a8.ogg", + "46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # no folder at all + "uploads%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # wrong folder name + "folder%2Fuploads%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", # nested but no recordings/ ], ) def test_validate_invalid_filepath(invalid_filepath, minio_parser): @@ -152,7 +154,7 @@ def test_validate_invalid_filepath(invalid_filepath, minio_parser): def test_validate_valid_event(minio_parser): """Test validation with valid event data.""" event = StorageEvent( - filepath="recording%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", + filepath="recordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", filetype="audio/ogg", bucket_name="test-bucket", metadata=None, @@ -170,7 +172,7 @@ def test_get_recording_id_success(minio_parser, valid_minio_event): def test_validate_filepath_with_folder(minio_parser): """Test validation of filepath with folder structure.""" event = StorageEvent( - filepath="parent_folder%2Ffolder%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", + filepath="parent_folder%2Frecordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", filetype="audio/ogg", bucket_name="test-bucket", metadata=None, @@ -219,7 +221,7 @@ def test_validate_custom_filetypes(): parser = MinioParser(bucket_name="test-bucket", allowed_filetypes={"audio/mp3"}) event = StorageEvent( - filepath="parent_folder%2Ffolder%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", + filepath="parent_folder%2Frecordings%2F46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg", filetype="audio/mp3", bucket_name="test-bucket", metadata=None, diff --git a/src/backend/core/tests/recording/test_api_recordings_storage_hook.py b/src/backend/core/tests/recording/test_api_recordings_storage_hook.py index f822a70c..008372e8 100644 --- a/src/backend/core/tests/recording/test_api_recordings_storage_hook.py +++ b/src/backend/core/tests/recording/test_api_recordings_storage_hook.py @@ -14,6 +14,7 @@ from ...factories import RecordingFactory from ...models import Recording, RecordingStatusChoices from ...recording.event.exceptions import ( InvalidBucketError, + InvalidFilepathError, InvalidFileTypeError, ParsingEventDataError, ) @@ -135,6 +136,29 @@ def test_save_recording_filetype_error(recording_settings, mock_get_parser): assert response.json() == {"message": "Ignore this file type, unsupported '.json'"} +def test_save_recording_filepath_error(recording_settings, mock_get_parser): + """Test handling of unsupported filepath in recording event data.""" + + mock_parser = mock.Mock() + mock_parser.get_recording_id.side_effect = InvalidFilepathError( + "Invalid filepath structure: parent/folder/recording.jpeg" + ) + mock_get_parser.return_value = mock_parser + + client = APIClient() + + response = client.post( + "/api/v1.0/recordings/storage-hook/", + {"recording_data": "valid-data"}, + HTTP_AUTHORIZATION="Bearer testAuthToken", + ) + + assert response.status_code == 200 + assert response.json() == { + "message": "Ignore this filepath, Invalid filepath structure: parent/folder/recording.jpeg" + } + + def test_save_recording_unknown_recording(recording_settings, mock_get_parser, client): """Test handling of events for non-existent recordings."""