From b9c0d2808c6c6605bc0a073f44cea230beb9e27e Mon Sep 17 00:00:00 2001 From: Yavor16 Date: Tue, 8 Apr 2025 14:36:47 +0300 Subject: [PATCH] Add uploaded file validation LMCROSSITXSADEPLOY-3175 --- multiapps-controller-core/pom.xml | 6 +- .../src/main/java/module-info.java | 1 + .../multiapps/controller/core/Messages.java | 3 + .../parameters/FileMimeTypeValidator.java | 71 ++++++++++++++ .../parameters/FileMimeTypeValidatorTest.java | 67 +++++++++++++ .../steps/ValidateDeployParametersStep.java | 24 ++++- .../ValidateDeployParametersStepTest.java | 96 ++++++++++--------- .../web/api/impl/FilesApiServiceImpl.java | 10 +- .../web/api/impl/FilesApiServiceImplTest.java | 30 +++--- pom.xml | 7 ++ 10 files changed, 246 insertions(+), 69 deletions(-) create mode 100644 multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidator.java create mode 100644 multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidatorTest.java diff --git a/multiapps-controller-core/pom.xml b/multiapps-controller-core/pom.xml index 5943ef3e96..7a5e498363 100644 --- a/multiapps-controller-core/pom.xml +++ b/multiapps-controller-core/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 multiapps-controller-core @@ -13,6 +13,10 @@ + + org.apache.tika + tika-core + jakarta.xml.bind jakarta.xml.bind-api diff --git a/multiapps-controller-core/src/main/java/module-info.java b/multiapps-controller-core/src/main/java/module-info.java index f50b149a4e..f5623a01bf 100644 --- a/multiapps-controller-core/src/main/java/module-info.java +++ b/multiapps-controller-core/src/main/java/module-info.java @@ -61,6 +61,7 @@ requires org.apache.commons.lang3; requires org.apache.httpcomponents.httpclient; requires org.apache.httpcomponents.httpcore; + requires org.apache.tika.core; requires org.cloudfoundry.multiapps.common; requires org.cloudfoundry.multiapps.controller.api; requires org.slf4j; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java index 74eb64d9d1..1d1a4fb5f4 100644 --- a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java @@ -86,6 +86,9 @@ public final class Messages { public static final String OBJECT_STORE_FILE_STORAGE_HEALTH_DATABASE_HEALTH = "Object store file storage health: \"{0}\", Database health: \"{1}\""; public static final String ERROR_OCCURRED_DURING_OBJECT_STORE_HEALTH_CHECKING_FOR_INSTANCE = "Error occurred during object store health checking for instance: \"{0}\""; public static final String ERROR_OCCURRED_WHILE_CHECKING_DATABASE_INSTANCE_0 = "Error occurred while checking database instance: \"{0}\""; + public static final String THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY = "The provided multipart file cannot be empty"; + public static final String THE_PROVIDED_0_FILE_IS_INVALID = "The provided {0} file is invalid! The file format must be either yaml or mtaext"; + public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected"; // Warning messages public static final String ENVIRONMENT_VARIABLE_IS_NOT_SET_USING_DEFAULT = "Environment variable \"{0}\" is not set. Using default \"{1}\"..."; diff --git a/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidator.java b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidator.java new file mode 100644 index 0000000000..fe7d4847f7 --- /dev/null +++ b/multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidator.java @@ -0,0 +1,71 @@ +package org.cloudfoundry.multiapps.controller.core.validators.parameters; + +import java.io.IOException; +import java.io.InputStream; +import java.text.MessageFormat; + +import jakarta.inject.Named; +import org.apache.commons.io.FilenameUtils; +import org.apache.tika.Tika; +import org.cloudfoundry.multiapps.common.SLException; +import org.cloudfoundry.multiapps.controller.core.Messages; +import org.springframework.web.multipart.MultipartFile; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.YAMLException; + +@Named +public class FileMimeTypeValidator { + + private static final String APPLICATION_ZIP_MIME_TYPE = "application/zip"; + private static final String APPLICATION_OCTET_STREAM_MIME_TYPE = "application/octet-stream"; + private static final String TEXT_PLAIN_MIME_TYPE = "text/plain"; + private static final String YAML_FILE_EXTENSION = "yaml"; + private static final String EXTENSION_DESCRIPTOR_FILE_EXTENSION = "mtaext"; + private static final Tika tika = new Tika(); + + public void validateMultipartFileMimeType(MultipartFile multipartFile) { + if (multipartFile == null || multipartFile.isEmpty()) { + throw new IllegalArgumentException(Messages.THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY); + } + + try { + validateInputStreamMimeType(multipartFile.getInputStream(), multipartFile.getOriginalFilename()); + } catch (IOException e) { + throw new SLException(e); + } + } + + public void validateInputStreamMimeType(InputStream uploadedFileInputStream, String filename) throws IOException { + String detectedType = getFileMimeType(uploadedFileInputStream); + switch (detectedType) { + case TEXT_PLAIN_MIME_TYPE -> validateYamlFile(uploadedFileInputStream, filename); + case APPLICATION_ZIP_MIME_TYPE, APPLICATION_OCTET_STREAM_MIME_TYPE -> { + } + default -> throw new IllegalArgumentException(MessageFormat.format(Messages.UNSUPPORTED_FILE_FORMAT, detectedType)); + } + } + + private String getFileMimeType(InputStream uploadedFileInputStream) throws IOException { + return tika.detect(uploadedFileInputStream); + } + + private void validateYamlFile(InputStream uploadedFileInputStream, String filename) { + validateTextFileExtension(filename); + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + try { + yaml.load(uploadedFileInputStream); + } catch (YAMLException e) { + throw new IllegalArgumentException(MessageFormat.format(Messages.THE_PROVIDED_0_FILE_IS_INVALID, filename), e); + } + } + + private void validateTextFileExtension(String filename) { + String fileExtension = FilenameUtils.getExtension(filename); + + if (!(YAML_FILE_EXTENSION.equals(fileExtension) || EXTENSION_DESCRIPTOR_FILE_EXTENSION.equals(fileExtension))) { + throw new IllegalArgumentException(MessageFormat.format(Messages.THE_PROVIDED_0_FILE_IS_INVALID, filename)); + } + } +} diff --git a/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidatorTest.java b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidatorTest.java new file mode 100644 index 0000000000..235c3d101e --- /dev/null +++ b/multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/validators/parameters/FileMimeTypeValidatorTest.java @@ -0,0 +1,67 @@ +package org.cloudfoundry.multiapps.controller.core.validators.parameters; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +import org.cloudfoundry.multiapps.controller.core.Messages; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.web.multipart.MultipartFile; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +class FileMimeTypeValidatorTest { + + private MultipartFile multipartFile; + private InputStream inputStream; + private final FileMimeTypeValidator fileMimeTypeValidator = new FileMimeTypeValidator(); + + @BeforeEach + void setUp() { + multipartFile = Mockito.mock(MultipartFile.class); + } + + @Test + void testValidateMultipartFileMimeTypeWithNullFile() { + assertThrows(IllegalArgumentException.class, () -> { + fileMimeTypeValidator.validateMultipartFileMimeType(null); + }, Messages.THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY); + } + + @Test + void testValidateMultipartFileMimeTypeWithEmptyFile() { + when(multipartFile.isEmpty()).thenReturn(true); + + assertThrows(IllegalArgumentException.class, () -> { + fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile); + }, Messages.THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY); + } + + @Test + void testValidateMultipartFileMimeType_ValidYamlFile() throws Exception { + inputStream = new ByteArrayInputStream("test: test".getBytes()); + + when(multipartFile.getInputStream()).thenReturn(inputStream); + when(multipartFile.getOriginalFilename()).thenReturn("valid.yaml"); + + fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile); + } + + @Test + void testValidateMultipartFileOctetStreamMimeType() throws Exception { + when(multipartFile.getInputStream()).thenReturn(inputStream); + when(multipartFile.getOriginalFilename()).thenReturn("valid.zip"); + + fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile); + } + + @Test + void testValidateMultipartFileOApplicationZipMimeType() throws Exception { + when(multipartFile.getInputStream()).thenReturn(inputStream); + when(multipartFile.getOriginalFilename()).thenReturn("valid.zip"); + + fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile); + } +} diff --git a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStep.java b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStep.java index 256bc33faf..42677670fb 100644 --- a/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStep.java +++ b/multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStep.java @@ -1,5 +1,7 @@ package org.cloudfoundry.multiapps.controller.process.steps; +import java.io.IOException; +import java.io.InputStream; import java.math.BigInteger; import java.text.MessageFormat; import java.util.ArrayList; @@ -8,10 +10,13 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; +import jakarta.inject.Inject; +import jakarta.inject.Named; import org.apache.commons.io.IOUtils; import org.cloudfoundry.multiapps.common.ContentException; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.client.util.ResilientOperationExecutor; +import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableFileEntry; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; @@ -25,19 +30,18 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; -import jakarta.inject.Inject; -import jakarta.inject.Named; - @Named("validateDeployParametersStep") @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class ValidateDeployParametersStep extends SyncFlowableStep { private final ResilientOperationExecutor resilientOperationExecutor = new ResilientOperationExecutor(); private final ExecutorService fileStorageThreadPool; + private final FileMimeTypeValidator fileMimeTypeValidator; @Inject - public ValidateDeployParametersStep(ExecutorService fileStorageThreadPool) { + public ValidateDeployParametersStep(ExecutorService fileStorageThreadPool, FileMimeTypeValidator fileMimeTypeValidator) { this.fileStorageThreadPool = fileStorageThreadPool; + this.fileMimeTypeValidator = fileMimeTypeValidator; } @Override @@ -181,6 +185,7 @@ private void mergeArchive(ProcessContext context, List archivePartEnt archivePartEntries.size(), archiveSize); FileEntry uploadedArchive = persistArchive(archiveStreamWithName, context, archiveSize); context.setVariable(Variables.APP_ARCHIVE_ID, uploadedArchive.getId()); + validateMergedArchive(uploadedArchive); getStepLogger().infoWithoutProgressMessage(MessageFormat.format(Messages.ARCHIVE_WITH_ID_0_AND_NAME_1_WAS_STORED, uploadedArchive.getId(), archiveStreamWithName.getArchiveName())); @@ -189,13 +194,22 @@ private void mergeArchive(ProcessContext context, List archivePartEnt } } + private void validateMergedArchive(FileEntry fileEntry) { + try (InputStream fileInputStream = fileService.openInputStream(fileEntry.getSpace(), fileEntry.getId())) { + fileMimeTypeValidator.validateInputStreamMimeType(fileInputStream, + fileEntry.getName()); + } catch (FileStorageException | IOException e) { + throw new SLException(e); + } + } + private String[] getArchivePartIds(ProcessContext context) { String archiveId = context.getRequiredVariable(Variables.APP_ARCHIVE_ID); return archiveId.split(","); } private List getArchivePartEntries(ProcessContext context, String[] appArchivePartsId) { - return Arrays.stream(appArchivePartsId) + return Arrays.stream(appArchivePartsId) .map(appArchivePartId -> findFile(context, appArchivePartId)) .toList(); } diff --git a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStepTest.java b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStepTest.java index 51cf648610..56cbfc53da 100644 --- a/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStepTest.java +++ b/multiapps-controller-process/src/test/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStepTest.java @@ -1,11 +1,5 @@ package org.cloudfoundry.multiapps.controller.process.steps; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.when; - import java.io.InputStream; import java.math.BigInteger; import java.nio.file.Files; @@ -21,6 +15,7 @@ import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; +import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableFileEntry; import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException; @@ -33,6 +28,12 @@ import org.junit.jupiter.params.provider.MethodSource; import org.mockito.Mockito; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + class ValidateDeployParametersStepTest extends SyncFlowableStepTest { private static final String MERGED_ARCHIVE_TEST_MTAR = "merged-archive-test.mtar"; @@ -48,36 +49,37 @@ class ValidateDeployParametersStepTest extends SyncFlowableStepTest testExecution() { return Stream.of( - // [1] No file associated with the specified file id - Arguments.of(new StepInput(EXISTING_FILE_ID, NOT_EXISTING_FILE_ID + "," + EXISTING_FILE_ID, 1, null), - MessageFormat.format(Messages.ERROR_NO_FILE_ASSOCIATED_WITH_THE_SPECIFIED_FILE_ID_0_IN_SPACE_1, - "notExistingFileId", "space-id"), - false, ""), - - // [2] Valid parameters - Arguments.of(new StepInput(EXISTING_FILE_ID, - EXISTING_FILE_ID + "," + EXISTING_FILE_ID, - 1, - VersionRule.HIGHER.toString()), - null, false, ""), - - // [3] Max descriptor size exceeded - Arguments.of(new StepInput(EXISTING_FILE_ID, EXISTING_BIGGER_FILE_ID, 1, VersionRule.HIGHER.toString()), - MessageFormat.format(org.cloudfoundry.multiapps.mta.Messages.ERROR_SIZE_OF_FILE_EXCEEDS_CONFIGURED_MAX_SIZE_LIMIT, - "1048577", "extDescriptorFile", "1048576"), - false, ""), - - // [4] Process chunked file - Arguments.of(new StepInput(MERGED_ARCHIVE_NAME + ".part.0," + MERGED_ARCHIVE_NAME + ".part.1," - + MERGED_ARCHIVE_NAME + ".part.2", null, 1, VersionRule.HIGHER.toString()), null, true, ""), - - // [5] Max size of entries exceeded - Arguments.of(new StepInput(EXCEEDING_FILE_SIZE_ID + ".part.0," + EXCEEDING_FILE_SIZE_ID + ".part.1," - + EXCEEDING_FILE_SIZE_ID + ".part.2," + EXCEEDING_FILE_SIZE_ID + ".part.3," + EXCEEDING_FILE_SIZE_ID - + ".part.4", null, 1, VersionRule.HIGHER.toString()), - MessageFormat.format(Messages.SIZE_OF_ALL_OPERATIONS_FILES_0_EXCEEDS_MAX_UPLOAD_SIZE_1, 5368709120L, - 4294967296L), - true, "")); + // [1] No file associated with the specified file id + Arguments.of(new StepInput(EXISTING_FILE_ID, NOT_EXISTING_FILE_ID + "," + EXISTING_FILE_ID, 1, null), + MessageFormat.format(Messages.ERROR_NO_FILE_ASSOCIATED_WITH_THE_SPECIFIED_FILE_ID_0_IN_SPACE_1, + "notExistingFileId", "space-id"), + false, ""), + + // [2] Valid parameters + Arguments.of(new StepInput(EXISTING_FILE_ID, + EXISTING_FILE_ID + "," + EXISTING_FILE_ID, + 1, + VersionRule.HIGHER.toString()), + null, false, ""), + + // [3] Max descriptor size exceeded + Arguments.of(new StepInput(EXISTING_FILE_ID, EXISTING_BIGGER_FILE_ID, 1, VersionRule.HIGHER.toString()), + MessageFormat.format(org.cloudfoundry.multiapps.mta.Messages.ERROR_SIZE_OF_FILE_EXCEEDS_CONFIGURED_MAX_SIZE_LIMIT, + "1048577", "extDescriptorFile", "1048576"), + false, ""), + + // [4] Process chunked file + Arguments.of(new StepInput(MERGED_ARCHIVE_NAME + ".part.0," + MERGED_ARCHIVE_NAME + ".part.1," + + MERGED_ARCHIVE_NAME + ".part.2", null, 1, VersionRule.HIGHER.toString()), null, true, ""), + + // [5] Max size of entries exceeded + Arguments.of(new StepInput(EXCEEDING_FILE_SIZE_ID + ".part.0," + EXCEEDING_FILE_SIZE_ID + ".part.1," + + EXCEEDING_FILE_SIZE_ID + ".part.2," + EXCEEDING_FILE_SIZE_ID + ".part.3," + + EXCEEDING_FILE_SIZE_ID + + ".part.4", null, 1, VersionRule.HIGHER.toString()), + MessageFormat.format(Messages.SIZE_OF_ALL_OPERATIONS_FILES_0_EXCEEDS_MAX_UPLOAD_SIZE_1, 5368709120L, + 4294967296L), + true, "")); } private static FileEntry createFileEntry(String id, String name, long size) { @@ -125,15 +127,15 @@ private void prepareContext() { private void prepareFileService(String appArchiveId) throws FileStorageException { when(fileService.getFile("space-id", EXISTING_FILE_ID)) - .thenReturn(createFileEntry(EXISTING_FILE_ID, "some-file-entry-name", 1024 * 1024L)); + .thenReturn(createFileEntry(EXISTING_FILE_ID, "some-file-entry-name", 1024 * 1024L)); when(fileService.getFile("space-id", MERGED_ARCHIVE_NAME + ".part.0")) - .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.0", MERGED_ARCHIVE_NAME + ".part.0", 1024 * 1024L)); + .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.0", MERGED_ARCHIVE_NAME + ".part.0", 1024 * 1024L)); when(fileService.getFile("space-id", MERGED_ARCHIVE_NAME + ".part.1")) - .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.1", MERGED_ARCHIVE_NAME + ".part.1", 1024 * 1024L)); + .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.1", MERGED_ARCHIVE_NAME + ".part.1", 1024 * 1024L)); when(fileService.getFile("space-id", MERGED_ARCHIVE_NAME + ".part.2")) - .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.2", MERGED_ARCHIVE_NAME + ".part.2", 1024 * 1024L)); + .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.2", MERGED_ARCHIVE_NAME + ".part.2", 1024 * 1024L)); when(fileService.getFile("space-id", EXCEEDING_FILE_SIZE_ID + ".part.0")) .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.0", MERGED_ARCHIVE_NAME + ".part.0", 1024 * 1024 * 1024)); @@ -151,11 +153,11 @@ private void prepareFileService(String appArchiveId) throws FileStorageException .thenReturn(createFileEntry(MERGED_ARCHIVE_NAME + ".part.4", MERGED_ARCHIVE_NAME + ".part.4", 1024 * 1024 * 1024)); when(fileService.getFile("space-id", EXISTING_BIGGER_FILE_ID)) - .thenReturn(createFileEntry(EXISTING_BIGGER_FILE_ID, "extDescriptorFile", 1024 * 1024L + 1)); + .thenReturn(createFileEntry(EXISTING_BIGGER_FILE_ID, "extDescriptorFile", 1024 * 1024L + 1)); when(fileService.getFile("space-id", NOT_EXISTING_FILE_ID)) - .thenReturn(null); + .thenReturn(null); when(fileService.addFile(any(FileEntry.class), any(InputStream.class))) - .thenReturn(createFileEntry(EXISTING_FILE_ID, MERGED_ARCHIVE_TEST_MTAR, 1024 * 1024 * 1024L)); + .thenReturn(createFileEntry(EXISTING_FILE_ID, MERGED_ARCHIVE_TEST_MTAR, 1024 * 1024 * 1024L)); if (appArchiveId.contains(EXCEEDING_FILE_SIZE_ID)) { List fileEntries = List.of(createFileEntry(EXCEEDING_FILE_SIZE_ID + ".part.0", EXCEEDING_FILE_SIZE_ID + ".part.0", 1024 * 1024 * 1024), @@ -168,7 +170,7 @@ private void prepareFileService(String appArchiveId) throws FileStorageException createFileEntry(EXCEEDING_FILE_SIZE_ID + ".part.4", EXCEEDING_FILE_SIZE_ID + ".part.4", 1024 * 1024 * 1024)); when(fileService.listFilesBySpaceAndOperationId(Mockito.anyString(), Mockito.anyString())) - .thenReturn(fileEntries); + .thenReturn(fileEntries); } } @@ -179,9 +181,9 @@ private void prepareArchiveMerger() { private void prepareConfiguration() { when(configuration.getMaxMtaDescriptorSize()) - .thenReturn(ApplicationConfiguration.DEFAULT_MAX_MTA_DESCRIPTOR_SIZE); + .thenReturn(ApplicationConfiguration.DEFAULT_MAX_MTA_DESCRIPTOR_SIZE); when(configuration.getMaxUploadSize()) - .thenReturn(ApplicationConfiguration.DEFAULT_MAX_UPLOAD_SIZE); + .thenReturn(ApplicationConfiguration.DEFAULT_MAX_UPLOAD_SIZE); } private void validate() { @@ -204,7 +206,7 @@ protected ValidateDeployParametersStep createStep() { futureTask.run(); return futureTask; }); - return new ValidateDeployParametersStep(executorService); + return new ValidateDeployParametersStep(executorService, new FileMimeTypeValidator()); } private static class StepInput { diff --git a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImpl.java b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImpl.java index ef93af4f52..f892b35434 100644 --- a/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImpl.java +++ b/multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImpl.java @@ -26,6 +26,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +import jakarta.inject.Inject; +import jakarta.inject.Named; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.ProxyInputStream; import org.cloudfoundry.multiapps.common.SLException; @@ -44,6 +46,7 @@ import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.core.util.FileUtils; import org.cloudfoundry.multiapps.controller.core.util.UriUtil; +import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator; import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry; import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry.State; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; @@ -69,9 +72,6 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; -import jakarta.inject.Inject; -import jakarta.inject.Named; - @Named public class FilesApiServiceImpl implements FilesApiService { @@ -81,6 +81,7 @@ public class FilesApiServiceImpl implements FilesApiService { private static final Duration HTTP_CONNECT_TIMEOUT = Duration.ofMinutes(10); private static final String RETRY_AFTER_SECONDS = "30"; private static final String USERNAME_PASSWORD_URL_FORMAT = "{0}:{1}"; + static { System.setProperty(Constants.RETRY_LIMIT_PROPERTY, "0"); } @@ -104,6 +105,8 @@ public class FilesApiServiceImpl implements FilesApiService { private FilesApiServiceAuditLog filesApiServiceAuditLog; @Inject private ExecutorService fileStorageThreadPool; + @Inject + private FileMimeTypeValidator fileMimeTypeValidator; @Override public ResponseEntity> getFiles(String spaceGuid, String namespace) { @@ -124,6 +127,7 @@ public ResponseEntity> getFiles(String spaceGuid, String name public ResponseEntity uploadFile(MultipartHttpServletRequest request, String spaceGuid, String namespace) { LOGGER.trace(Messages.RECEIVED_UPLOAD_REQUEST, ServletUtil.decodeUri(request)); var multipartFile = getFileFromRequest(request); + fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile); try (InputStream in = new BufferedInputStream(multipartFile.getInputStream(), INPUT_STREAM_BUFFER_SIZE)) { var startTime = LocalDateTime.now(); FileEntry fileEntry = fileStorageThreadPool.submit(createUploadFileTask(spaceGuid, namespace, multipartFile, in)) diff --git a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImplTest.java b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImplTest.java index 94197f8d70..bb3431c38f 100644 --- a/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImplTest.java +++ b/multiapps-controller-web/src/test/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImplTest.java @@ -1,11 +1,5 @@ package org.cloudfoundry.multiapps.controller.web.api.impl; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - import java.io.ByteArrayInputStream; import java.io.InputStream; import java.math.BigInteger; @@ -27,6 +21,7 @@ import java.util.concurrent.Future; import java.util.concurrent.FutureTask; +import jakarta.persistence.NoResultException; import org.apache.commons.lang3.RandomStringUtils; import org.cloudfoundry.multiapps.common.SLException; import org.cloudfoundry.multiapps.controller.api.model.AsyncUploadResult; @@ -38,6 +33,7 @@ import org.cloudfoundry.multiapps.controller.core.helpers.DescriptorParserFacadeFactory; import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration; import org.cloudfoundry.multiapps.controller.core.util.UserInfo; +import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator; import org.cloudfoundry.multiapps.controller.persistence.Constants; import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry; import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry; @@ -65,13 +61,19 @@ import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartHttpServletRequest; -import jakarta.persistence.NoResultException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; class FilesApiServiceImplTest { private static final String MTA_ID = "anatz"; private static final String FILE_URL = Base64.getUrlEncoder() - .encodeToString("https://host.domain/test.mtar?query=true".getBytes(StandardCharsets.UTF_8)); + .encodeToString( + "https://host.domain/test.mtar?query=true".getBytes(StandardCharsets.UTF_8)); private static final String SPACE_GUID = "896e6be9-8217-4a1c-b938-09b30966157a"; private static final String NAMESPACE = "custom-namespace"; private static final String DIGEST_CHARACTER_TABLE = "123456789ABCDEF"; @@ -114,6 +116,8 @@ protected ResilientOperationExecutor getResilientOperationExecutor() { private DescriptorParserFacadeFactory descriptorParserFactory = new DescriptorParserFacadeFactory(configuration); @Mock private AsyncUploadJobService uploadJobService; + @Mock + private FileMimeTypeValidator fileMimeTypeValidator; @BeforeAll public static void setUser() { @@ -181,7 +185,7 @@ void testUploadMtaFile() throws Exception { ResponseEntity response = testedClass.uploadFile(request, SPACE_GUID, NAMESPACE); - Mockito.verify(file) + Mockito.verify(file, times(1)) .getInputStream(); Mockito.verify(fileService) .addFile(Mockito.eq(ImmutableFileEntry.builder() @@ -281,10 +285,10 @@ private void prepareFileStorageThreadPool() { private void prepareAsyncExecutor(Future future) { Mockito.doAnswer(invocationOnMock -> { - Runnable r = invocationOnMock.getArgument(0); - r.run(); - return future; - }) + Runnable r = invocationOnMock.getArgument(0); + r.run(); + return future; + }) .when(asyncFileUploadExecutor) .submit((Runnable) Mockito.any()); } diff --git a/pom.xml b/pom.xml index 1d2071ce4a..770215554c 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,7 @@ 3.0.0 1.78 1.27.1 + 3.1.0 multiapps-controller-client @@ -468,6 +469,12 @@ slf4j-jdk14 ${slf4j.version} + + + org.apache.tika + tika-core + ${tika-core.version} + commons-io