diff --git a/api/src/integrationTest/kotlin/com/cosmotech/api/home/workspace/WorkspaceControllerTests.kt b/api/src/integrationTest/kotlin/com/cosmotech/api/home/workspace/WorkspaceControllerTests.kt index aa0b95365..9df5dae56 100644 --- a/api/src/integrationTest/kotlin/com/cosmotech/api/home/workspace/WorkspaceControllerTests.kt +++ b/api/src/integrationTest/kotlin/com/cosmotech/api/home/workspace/WorkspaceControllerTests.kt @@ -34,6 +34,7 @@ import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequ import org.springframework.test.context.ActiveProfiles import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import org.springframework.test.web.servlet.result.MockMvcResultHandlers +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status @@ -658,4 +659,40 @@ class WorkspaceControllerTests : ControllerTestBase() { document( "organizations/{organization_id}/workspaces/{workspace_id}/files/download/GET")) } + + @Test + @WithMockOauth2User + fun `download workspace file with wrong file name`() { + + val workspaceId = + createWorkspaceAndReturnId( + mvc, organizationId, constructWorkspaceCreateRequest(solutionId = solutionId)) + + val fileName = "test.txt" + val fileToUpload = + this::class.java.getResourceAsStream("/workspace/$fileName") + ?: throw IllegalStateException( + "$fileName file used for organizations/{organization_id}/workspaces/{workspace_id}/files/POST endpoint documentation cannot be null") + + val mockFile = + MockMultipartFile( + "file", fileName, MediaType.TEXT_PLAIN_VALUE, IOUtils.toByteArray(fileToUpload)) + + val destination = "path/to/a/directory/" + mvc.perform( + multipart("/organizations/$organizationId/workspaces/$workspaceId/files") + .file(mockFile) + .param("overwrite", "true") + .param("destination", destination) + .accept(MediaType.APPLICATION_JSON) + .with(csrf())) + + mvc.perform( + get("/organizations/$organizationId/workspaces/$workspaceId/files/download") + .param("file_name", "Wrong file name") + .accept(MediaType.APPLICATION_OCTET_STREAM)) + .andExpect(status().is4xxClientError) + .andExpect(jsonPath("$.detail").value("Wrong file name does not exist.")) + .andDo(MockMvcResultHandlers.print()) + } } diff --git a/common/src/main/kotlin/com/cosmotech/common/exceptions/CsmExceptionHandling.kt b/common/src/main/kotlin/com/cosmotech/common/exceptions/CsmExceptionHandling.kt index b80c4811c..c89677491 100644 --- a/common/src/main/kotlin/com/cosmotech/common/exceptions/CsmExceptionHandling.kt +++ b/common/src/main/kotlin/com/cosmotech/common/exceptions/CsmExceptionHandling.kt @@ -2,6 +2,7 @@ // Licensed under the MIT license. package com.cosmotech.common.exceptions +import io.awspring.cloud.s3.S3Exception import java.net.URI import org.apache.commons.lang3.NotImplementedException import org.springframework.core.Ordered @@ -21,7 +22,9 @@ import org.springframework.web.bind.annotation.RestControllerAdvice import org.springframework.web.context.request.WebRequest import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler import org.springframework.web.util.BindErrorUtils +import software.amazon.awssdk.services.s3.model.NoSuchKeyException +@Suppress("TooManyFunctions") @Order(Ordered.HIGHEST_PRECEDENCE) @RestControllerAdvice open class CsmExceptionHandling : ResponseEntityExceptionHandler() { @@ -160,4 +163,24 @@ open class CsmExceptionHandling : ResponseEntityExceptionHandler() { } return response } + + @ExceptionHandler(NoSuchKeyException::class) + fun handleNoSuchKeyException(): ProblemDetail { + val response = ProblemDetail.forStatus(HttpStatus.NOT_FOUND) + val noSuchKeyErrorStatus = HttpStatus.NOT_FOUND + response.type = URI.create(httpStatusCodeTypePrefix + noSuchKeyErrorStatus.value()) + response.detail = "The specified file name does not exist." + return response + } + + @ExceptionHandler(S3Exception::class) + fun handleS3Exception(exception: S3Exception): ProblemDetail { + val response = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR) + val internalServerErrorStatus = HttpStatus.INTERNAL_SERVER_ERROR + response.type = URI.create(httpStatusCodeTypePrefix + internalServerErrorStatus.value()) + if (exception.message != null) { + response.detail = exception.message + } + return response + } } diff --git a/workspace/src/integrationTest/kotlin/com/cosmotech/workspace/service/WorkspaceServiceIntegrationTest.kt b/workspace/src/integrationTest/kotlin/com/cosmotech/workspace/service/WorkspaceServiceIntegrationTest.kt index 50515266b..77c55da89 100644 --- a/workspace/src/integrationTest/kotlin/com/cosmotech/workspace/service/WorkspaceServiceIntegrationTest.kt +++ b/workspace/src/integrationTest/kotlin/com/cosmotech/workspace/service/WorkspaceServiceIntegrationTest.kt @@ -48,7 +48,6 @@ import org.springframework.mock.web.MockMultipartFile import org.springframework.test.context.ActiveProfiles import org.springframework.test.context.junit.jupiter.SpringExtension import org.springframework.test.context.junit4.SpringRunner -import software.amazon.awssdk.services.s3.model.NoSuchKeyException @ActiveProfiles(profiles = ["workspace-test"]) @ExtendWith(MockKExtension::class) @@ -177,6 +176,24 @@ class WorkspaceServiceIntegrationTest : CsmTestBase() { assertEquals(expectedText, retrievedText) } + @Test + fun `test get workspace file with wrong name`() { + val resourceTestFile = resourceLoader.getResource("classpath:/$fileName").file + val input = FileInputStream(resourceTestFile) + val multipartFile = + MockMultipartFile( + "file", resourceTestFile.getName(), "text/plain", IOUtils.toByteArray(input)) + workspaceApiService.createWorkspaceFile( + organizationSaved.id, workspaceSaved.id, multipartFile, true, null) + + val exception = + assertThrows { + workspaceApiService.getWorkspaceFile(organizationSaved.id, workspaceSaved.id, "WrongName") + } + + assertEquals("WrongName does not exist.", exception.message) + } + @Test fun `test list workspace files`() { every { getCurrentAuthenticatedRoles(any()) } returns listOf("Platform.Admin") @@ -204,9 +221,9 @@ class WorkspaceServiceIntegrationTest : CsmTestBase() { logger.info("should delete a workspace file") val resourceTestFile = resourceLoader.getResource("classpath:/$fileName").file val input = FileInputStream(resourceTestFile) + val originalFilename = resourceTestFile.getName() val multipartFile = - MockMultipartFile( - "file", resourceTestFile.getName(), "text/plain", IOUtils.toByteArray(input)) + MockMultipartFile("file", originalFilename, "text/plain", IOUtils.toByteArray(input)) workspaceApiService.createWorkspaceFile( organizationSaved.id, workspaceSaved.id, multipartFile, true, null) @@ -214,11 +231,11 @@ class WorkspaceServiceIntegrationTest : CsmTestBase() { workspaceApiService.deleteWorkspaceFile(organizationSaved.id, workspaceSaved.id, fileName) val exception = - assertThrows { + assertThrows { workspaceApiService.getWorkspaceFile(organizationSaved.id, workspaceSaved.id, fileName) } - assertEquals("The specified key does not exist.", exception.awsErrorDetails().errorMessage()) + assertEquals("$originalFilename does not exist.", exception.message) } @Test diff --git a/workspace/src/main/kotlin/com/cosmotech/workspace/service/WorkspaceServiceImpl.kt b/workspace/src/main/kotlin/com/cosmotech/workspace/service/WorkspaceServiceImpl.kt index 3d5fb730b..9baacb0b0 100644 --- a/workspace/src/main/kotlin/com/cosmotech/workspace/service/WorkspaceServiceImpl.kt +++ b/workspace/src/main/kotlin/com/cosmotech/workspace/service/WorkspaceServiceImpl.kt @@ -56,6 +56,7 @@ import org.springframework.web.multipart.MultipartFile import software.amazon.awssdk.awscore.exception.AwsServiceException import software.amazon.awssdk.services.s3.S3Client import software.amazon.awssdk.services.s3.model.ListObjectsV2Request +import software.amazon.awssdk.services.s3.model.NoSuchKeyException private const val WORKSPACE_FILES_BASE_FOLDER = "workspace-files" @@ -233,12 +234,20 @@ internal class WorkspaceServiceImpl( workspace.id, workspace.name, fileName) - return InputStreamResource( - s3Template - .download( - csmPlatformProperties.s3.bucketName, - "$organizationId/$workspaceId/$WORKSPACE_FILES_BASE_FOLDER/$fileName") - .inputStream) + var fileResource: Resource + try { + fileResource = + InputStreamResource( + s3Template + .download( + csmPlatformProperties.s3.bucketName, + "$organizationId/$workspaceId/$WORKSPACE_FILES_BASE_FOLDER/$fileName") + .inputStream) + } catch (exception: NoSuchKeyException) { + throw CsmResourceNotFoundException("$fileName does not exist.", exception) + } + + return fileResource } override fun createWorkspaceFile( @@ -326,13 +335,23 @@ internal class WorkspaceServiceImpl( .prefix(prefix) .build() - return s3Client - .listObjectsV2Paginator(listObjectsRequest) - .stream() - .flatMap { - it.contents().stream().map { WorkspaceFile(fileName = it.key().removePrefix(prefix)) } - } - .toList() + try { + return s3Client + .listObjectsV2Paginator(listObjectsRequest) + .stream() + .flatMap { s3Object -> + s3Object.contents().stream().map { + WorkspaceFile(fileName = it.key().removePrefix(prefix)) + } + } + .toList() + } catch (e: AwsServiceException) { + throw S3Exception("Something wrong happened when listing workspace files", e) + } catch (e: SdkClientException) { + throw S3Exception("Something wrong happened when listing workspace files", e) + } catch (e: S3Exception) { + throw S3Exception("Something wrong happened when listing workspace files", e) + } } private fun deleteS3WorkspaceObject(