Skip to content

Commit ad8971a

Browse files
committed
Add uploaded file validation
LMCROSSITXSADEPLOY-3175
1 parent d5699f9 commit ad8971a

File tree

10 files changed

+246
-69
lines changed

10 files changed

+246
-69
lines changed

multiapps-controller-core/pom.xml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
2-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
33
<modelVersion>4.0.0</modelVersion>
44

55
<artifactId>multiapps-controller-core</artifactId>
@@ -13,6 +13,10 @@
1313
</parent>
1414

1515
<dependencies>
16+
<dependency>
17+
<groupId>org.apache.tika</groupId>
18+
<artifactId>tika-core</artifactId>
19+
</dependency>
1620
<dependency>
1721
<groupId>jakarta.xml.bind</groupId>
1822
<artifactId>jakarta.xml.bind-api</artifactId>

multiapps-controller-core/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
requires org.apache.commons.lang3;
6262
requires org.apache.httpcomponents.httpclient;
6363
requires org.apache.httpcomponents.httpcore;
64+
requires org.apache.tika.core;
6465
requires org.cloudfoundry.multiapps.common;
6566
requires org.cloudfoundry.multiapps.controller.api;
6667
requires org.slf4j;

multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/Messages.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ public final class Messages {
8686
public static final String OBJECT_STORE_FILE_STORAGE_HEALTH_DATABASE_HEALTH = "Object store file storage health: \"{0}\", Database health: \"{1}\"";
8787
public static final String ERROR_OCCURRED_DURING_OBJECT_STORE_HEALTH_CHECKING_FOR_INSTANCE = "Error occurred during object store health checking for instance: \"{0}\"";
8888
public static final String ERROR_OCCURRED_WHILE_CHECKING_DATABASE_INSTANCE_0 = "Error occurred while checking database instance: \"{0}\"";
89+
public static final String THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY = "The provided multipart file cannot be empty";
90+
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";
91+
public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected";
8992

9093
// Warning messages
9194
public static final String ENVIRONMENT_VARIABLE_IS_NOT_SET_USING_DEFAULT = "Environment variable \"{0}\" is not set. Using default \"{1}\"...";
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.cloudfoundry.multiapps.controller.core.validators.parameters;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.text.MessageFormat;
6+
7+
import jakarta.inject.Named;
8+
import org.apache.commons.io.FilenameUtils;
9+
import org.apache.tika.Tika;
10+
import org.cloudfoundry.multiapps.common.SLException;
11+
import org.cloudfoundry.multiapps.controller.core.Messages;
12+
import org.springframework.web.multipart.MultipartFile;
13+
import org.yaml.snakeyaml.LoaderOptions;
14+
import org.yaml.snakeyaml.Yaml;
15+
import org.yaml.snakeyaml.constructor.SafeConstructor;
16+
import org.yaml.snakeyaml.error.YAMLException;
17+
18+
@Named
19+
public class FileMimeTypeValidator {
20+
21+
private static final String APPLICATION_ZIP_MIME_TYPE = "application/zip";
22+
private static final String APPLICATION_OCTET_STREAM_MIME_TYPE = "application/octet-stream";
23+
private static final String TEXT_PLAIN_MIME_TYPE = "text/plain";
24+
private static final String YAML_FILE_EXTENSION = "yaml";
25+
private static final String EXTENSION_DESCRIPTOR_FILE_EXTENSION = "mtaext";
26+
private static final Tika tika = new Tika();
27+
28+
public void validateMultipartFileMimeType(MultipartFile multipartFile) {
29+
if (multipartFile == null || multipartFile.isEmpty()) {
30+
throw new IllegalArgumentException(Messages.THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY);
31+
}
32+
33+
try {
34+
validateInputStreamMimeType(multipartFile.getInputStream(), multipartFile.getOriginalFilename());
35+
} catch (IOException e) {
36+
throw new SLException(e);
37+
}
38+
}
39+
40+
public void validateInputStreamMimeType(InputStream uploadedFileInputStream, String filename) throws IOException {
41+
String detectedType = getFileMimeType(uploadedFileInputStream);
42+
switch (detectedType) {
43+
case TEXT_PLAIN_MIME_TYPE -> validateYamlFile(uploadedFileInputStream, filename);
44+
case APPLICATION_ZIP_MIME_TYPE, APPLICATION_OCTET_STREAM_MIME_TYPE -> {
45+
}
46+
default -> throw new IllegalArgumentException(MessageFormat.format(Messages.UNSUPPORTED_FILE_FORMAT, detectedType));
47+
}
48+
}
49+
50+
private String getFileMimeType(InputStream uploadedFileInputStream) throws IOException {
51+
return tika.detect(uploadedFileInputStream);
52+
}
53+
54+
private void validateYamlFile(InputStream uploadedFileInputStream, String filename) {
55+
validateTextFileExtension(filename);
56+
Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions()));
57+
try {
58+
yaml.load(uploadedFileInputStream);
59+
} catch (YAMLException e) {
60+
throw new IllegalArgumentException(MessageFormat.format(Messages.THE_PROVIDED_0_FILE_IS_INVALID, filename), e);
61+
}
62+
}
63+
64+
private void validateTextFileExtension(String filename) {
65+
String fileExtension = FilenameUtils.getExtension(filename);
66+
67+
if (!(YAML_FILE_EXTENSION.equals(fileExtension) || EXTENSION_DESCRIPTOR_FILE_EXTENSION.equals(fileExtension))) {
68+
throw new IllegalArgumentException(MessageFormat.format(Messages.THE_PROVIDED_0_FILE_IS_INVALID, filename));
69+
}
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.cloudfoundry.multiapps.controller.core.validators.parameters;
2+
3+
import java.io.ByteArrayInputStream;
4+
import java.io.InputStream;
5+
6+
import org.cloudfoundry.multiapps.controller.core.Messages;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.Test;
9+
import org.mockito.Mockito;
10+
import org.springframework.web.multipart.MultipartFile;
11+
12+
import static org.junit.jupiter.api.Assertions.assertThrows;
13+
import static org.mockito.Mockito.when;
14+
15+
class FileMimeTypeValidatorTest {
16+
17+
private MultipartFile multipartFile;
18+
private InputStream inputStream;
19+
private final FileMimeTypeValidator fileMimeTypeValidator = new FileMimeTypeValidator();
20+
21+
@BeforeEach
22+
void setUp() {
23+
multipartFile = Mockito.mock(MultipartFile.class);
24+
}
25+
26+
@Test
27+
void testValidateMultipartFileMimeTypeWithNullFile() {
28+
assertThrows(IllegalArgumentException.class, () -> {
29+
fileMimeTypeValidator.validateMultipartFileMimeType(null);
30+
}, Messages.THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY);
31+
}
32+
33+
@Test
34+
void testValidateMultipartFileMimeTypeWithEmptyFile() {
35+
when(multipartFile.isEmpty()).thenReturn(true);
36+
37+
assertThrows(IllegalArgumentException.class, () -> {
38+
fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile);
39+
}, Messages.THE_PROVIDED_MULTIPART_FILE_CANNOT_BE_EMPTY);
40+
}
41+
42+
@Test
43+
void testValidateMultipartFileMimeType_ValidYamlFile() throws Exception {
44+
inputStream = new ByteArrayInputStream("test: test".getBytes());
45+
46+
when(multipartFile.getInputStream()).thenReturn(inputStream);
47+
when(multipartFile.getOriginalFilename()).thenReturn("valid.yaml");
48+
49+
fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile);
50+
}
51+
52+
@Test
53+
void testValidateMultipartFileOctetStreamMimeType() throws Exception {
54+
when(multipartFile.getInputStream()).thenReturn(inputStream);
55+
when(multipartFile.getOriginalFilename()).thenReturn("valid.zip");
56+
57+
fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile);
58+
}
59+
60+
@Test
61+
void testValidateMultipartFileOApplicationZipMimeType() throws Exception {
62+
when(multipartFile.getInputStream()).thenReturn(inputStream);
63+
when(multipartFile.getOriginalFilename()).thenReturn("valid.zip");
64+
65+
fileMimeTypeValidator.validateMultipartFileMimeType(multipartFile);
66+
}
67+
}

multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/steps/ValidateDeployParametersStep.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package org.cloudfoundry.multiapps.controller.process.steps;
22

3+
import java.io.IOException;
4+
import java.io.InputStream;
35
import java.math.BigInteger;
46
import java.text.MessageFormat;
57
import java.util.ArrayList;
@@ -8,10 +10,13 @@
810
import java.util.concurrent.ExecutionException;
911
import java.util.concurrent.ExecutorService;
1012

13+
import jakarta.inject.Inject;
14+
import jakarta.inject.Named;
1115
import org.apache.commons.io.IOUtils;
1216
import org.cloudfoundry.multiapps.common.ContentException;
1317
import org.cloudfoundry.multiapps.common.SLException;
1418
import org.cloudfoundry.multiapps.controller.client.util.ResilientOperationExecutor;
19+
import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator;
1520
import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry;
1621
import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableFileEntry;
1722
import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException;
@@ -25,19 +30,18 @@
2530
import org.springframework.beans.factory.config.BeanDefinition;
2631
import org.springframework.context.annotation.Scope;
2732

28-
import jakarta.inject.Inject;
29-
import jakarta.inject.Named;
30-
3133
@Named("validateDeployParametersStep")
3234
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
3335
public class ValidateDeployParametersStep extends SyncFlowableStep {
3436

3537
private final ResilientOperationExecutor resilientOperationExecutor = new ResilientOperationExecutor();
3638
private final ExecutorService fileStorageThreadPool;
39+
private final FileMimeTypeValidator fileMimeTypeValidator;
3740

3841
@Inject
39-
public ValidateDeployParametersStep(ExecutorService fileStorageThreadPool) {
42+
public ValidateDeployParametersStep(ExecutorService fileStorageThreadPool, FileMimeTypeValidator fileMimeTypeValidator) {
4043
this.fileStorageThreadPool = fileStorageThreadPool;
44+
this.fileMimeTypeValidator = fileMimeTypeValidator;
4145
}
4246

4347
@Override
@@ -181,6 +185,7 @@ private void mergeArchive(ProcessContext context, List<FileEntry> archivePartEnt
181185
archivePartEntries.size(), archiveSize);
182186
FileEntry uploadedArchive = persistArchive(archiveStreamWithName, context, archiveSize);
183187
context.setVariable(Variables.APP_ARCHIVE_ID, uploadedArchive.getId());
188+
validateMergedArchive(uploadedArchive);
184189
getStepLogger().infoWithoutProgressMessage(MessageFormat.format(Messages.ARCHIVE_WITH_ID_0_AND_NAME_1_WAS_STORED,
185190
uploadedArchive.getId(),
186191
archiveStreamWithName.getArchiveName()));
@@ -189,13 +194,22 @@ private void mergeArchive(ProcessContext context, List<FileEntry> archivePartEnt
189194
}
190195
}
191196

197+
private void validateMergedArchive(FileEntry fileEntry) {
198+
try (InputStream fileInputStream = fileService.openInputStream(fileEntry.getSpace(), fileEntry.getId())) {
199+
fileMimeTypeValidator.validateInputStreamMimeType(fileInputStream,
200+
fileEntry.getName());
201+
} catch (FileStorageException | IOException e) {
202+
throw new SLException(e);
203+
}
204+
}
205+
192206
private String[] getArchivePartIds(ProcessContext context) {
193207
String archiveId = context.getRequiredVariable(Variables.APP_ARCHIVE_ID);
194208
return archiveId.split(",");
195209
}
196210

197211
private List<FileEntry> getArchivePartEntries(ProcessContext context, String[] appArchivePartsId) {
198-
return Arrays.stream(appArchivePartsId)
212+
return Arrays.stream(appArchivePartsId)
199213
.map(appArchivePartId -> findFile(context, appArchivePartId))
200214
.toList();
201215
}

0 commit comments

Comments
 (0)