Skip to content

Commit c37cda8

Browse files
committed
Add uploaded file validation
LMCROSSITXSADEPLOY-3175
1 parent 1ecc9d9 commit c37cda8

File tree

9 files changed

+180
-8
lines changed

9 files changed

+180
-8
lines changed

multiapps-controller-core/pom.xml

+5-1
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

+1
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
requires transitive org.cloudfoundry.multiapps.controller.persistence;
4646
requires transitive org.cloudfoundry.multiapps.mta;
4747

48+
requires org.apache.tika.core;
4849
requires org.cloudfoundry.client;
4950
requires com.fasterxml.jackson.annotation;
5051
requires com.fasterxml.jackson.core;

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

+4
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ 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 INVALID_MULTIPART_FILE = "The provided multipart file cannot be empty";
90+
public static final String INVALID_YAML_FILE = "The provided yaml file is invalid";
91+
public static final String INVALID_MTAR_FILE = "The provided mtar file is invalid";
92+
public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected";
8993

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

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

+11-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.cloudfoundry.multiapps.controller.process.steps;
22

3+
import java.io.IOException;
34
import java.math.BigInteger;
45
import java.text.MessageFormat;
56
import java.util.ArrayList;
@@ -8,15 +9,19 @@
89
import java.util.concurrent.ExecutionException;
910
import java.util.concurrent.ExecutorService;
1011

12+
import jakarta.inject.Inject;
13+
import jakarta.inject.Named;
1114
import org.apache.commons.io.IOUtils;
1215
import org.cloudfoundry.multiapps.common.ContentException;
1316
import org.cloudfoundry.multiapps.common.SLException;
1417
import org.cloudfoundry.multiapps.controller.client.util.ResilientOperationExecutor;
18+
import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator;
1519
import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry;
1620
import org.cloudfoundry.multiapps.controller.persistence.model.ImmutableFileEntry;
1721
import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException;
1822
import org.cloudfoundry.multiapps.controller.process.Messages;
1923
import org.cloudfoundry.multiapps.controller.process.stream.ArchiveStreamWithName;
24+
import org.cloudfoundry.multiapps.controller.process.stream.LazyArchiveInputStream;
2025
import org.cloudfoundry.multiapps.controller.process.util.FileSweeper;
2126
import org.cloudfoundry.multiapps.controller.process.util.MergedArchiveStreamCreator;
2227
import org.cloudfoundry.multiapps.controller.process.util.PriorityCallable;
@@ -25,9 +30,6 @@
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 {
@@ -177,13 +179,18 @@ private void mergeArchive(ProcessContext context, List<FileEntry> archivePartEnt
177179
private void mergeArchive(ProcessContext context, List<FileEntry> archivePartEntries, BigInteger archiveSize) {
178180
ArchiveStreamWithName archiveStreamWithName = getMergedArchiveStreamCreator(archivePartEntries, archiveSize).createArchiveStream();
179181
try {
182+
LazyArchiveInputStream lazyArchiveInputStream = (LazyArchiveInputStream) archiveStreamWithName.getArchiveStream();
183+
FileMimeTypeValidator.validateInputStreamMimeType(lazyArchiveInputStream.getCurrentInputStream(),
184+
archiveStreamWithName.getArchiveName());
180185
getStepLogger().infoWithoutProgressMessage(Messages.ARCHIVE_IS_SPLIT_TO_0_PARTS_TOTAL_SIZE_IN_BYTES_1_UPLOADING,
181186
archivePartEntries.size(), archiveSize);
182187
FileEntry uploadedArchive = persistArchive(archiveStreamWithName, context, archiveSize);
183188
context.setVariable(Variables.APP_ARCHIVE_ID, uploadedArchive.getId());
184189
getStepLogger().infoWithoutProgressMessage(MessageFormat.format(Messages.ARCHIVE_WITH_ID_0_AND_NAME_1_WAS_STORED,
185190
uploadedArchive.getId(),
186191
archiveStreamWithName.getArchiveName()));
192+
} catch (IOException e) {
193+
throw new SLException(e);
187194
} finally {
188195
IOUtils.closeQuietly(archiveStreamWithName.getArchiveStream());
189196
}
@@ -195,7 +202,7 @@ private String[] getArchivePartIds(ProcessContext context) {
195202
}
196203

197204
private List<FileEntry> getArchivePartEntries(ProcessContext context, String[] appArchivePartsId) {
198-
return Arrays.stream(appArchivePartsId)
205+
return Arrays.stream(appArchivePartsId)
199206
.map(appArchivePartId -> findFile(context, appArchivePartId))
200207
.toList();
201208
}

multiapps-controller-process/src/main/java/org/cloudfoundry/multiapps/controller/process/stream/LazyArchiveInputStream.java

+4
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@ public synchronized int read(byte[] b, int off, int len) throws IOException {
8282
return bytesRead;
8383
}
8484

85+
public InputStream getCurrentInputStream() {
86+
return currentInputStream;
87+
}
88+
8589
@Override
8690
public synchronized int available() throws IOException {
8791
// The return value of this method must be anything except 0

multiapps-controller-web/src/main/java/org/cloudfoundry/multiapps/controller/web/api/impl/FilesApiServiceImpl.java

+7-3
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,15 @@
2020
import java.util.Base64;
2121
import java.util.List;
2222
import java.util.UUID;
23+
import java.util.concurrent.ConcurrentHashMap;
2324
import java.util.concurrent.ExecutorService;
2425
import java.util.concurrent.Future;
2526
import java.util.concurrent.RejectedExecutionException;
2627
import java.util.concurrent.atomic.AtomicLong;
2728
import java.util.stream.Collectors;
2829

30+
import jakarta.inject.Inject;
31+
import jakarta.inject.Named;
2932
import org.apache.commons.io.IOUtils;
3033
import org.apache.commons.io.input.ProxyInputStream;
3134
import org.cloudfoundry.multiapps.common.SLException;
@@ -44,6 +47,7 @@
4447
import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration;
4548
import org.cloudfoundry.multiapps.controller.core.util.FileUtils;
4649
import org.cloudfoundry.multiapps.controller.core.util.UriUtil;
50+
import org.cloudfoundry.multiapps.controller.core.validators.parameters.FileMimeTypeValidator;
4751
import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry;
4852
import org.cloudfoundry.multiapps.controller.persistence.model.AsyncUploadJobEntry.State;
4953
import org.cloudfoundry.multiapps.controller.persistence.model.FileEntry;
@@ -69,9 +73,6 @@
6973
import org.springframework.web.multipart.MultipartFile;
7074
import org.springframework.web.multipart.MultipartHttpServletRequest;
7175

72-
import jakarta.inject.Inject;
73-
import jakarta.inject.Named;
74-
7576
@Named
7677
public class FilesApiServiceImpl implements FilesApiService {
7778

@@ -81,13 +82,15 @@ public class FilesApiServiceImpl implements FilesApiService {
8182
private static final Duration HTTP_CONNECT_TIMEOUT = Duration.ofMinutes(10);
8283
private static final String RETRY_AFTER_SECONDS = "30";
8384
private static final String USERNAME_PASSWORD_URL_FORMAT = "{0}:{1}";
85+
8486
static {
8587
System.setProperty(Constants.RETRY_LIMIT_PROPERTY, "0");
8688
}
8789

8890
private final CachedMap<String, AtomicLong> jobCounters = new CachedMap<>(Duration.ofHours(1));
8991
private final CachedMap<String, Future<?>> runningTasks = new CachedMap<>(Duration.ofHours(1));
9092
private final ResilientOperationExecutor resilientOperationExecutor = getResilientOperationExecutor();
93+
private final ConcurrentHashMap<UUID, Boolean> terminatedProcesses = new ConcurrentHashMap<>();
9194
@Inject
9295
@Named("fileService")
9396
private FileService fileService;
@@ -124,6 +127,7 @@ public ResponseEntity<List<FileMetadata>> getFiles(String spaceGuid, String name
124127
public ResponseEntity<FileMetadata> uploadFile(MultipartHttpServletRequest request, String spaceGuid, String namespace) {
125128
LOGGER.trace(Messages.RECEIVED_UPLOAD_REQUEST, ServletUtil.decodeUri(request));
126129
var multipartFile = getFileFromRequest(request);
130+
FileMimeTypeValidator.validateMultipartFileMimeType(multipartFile);
127131
try (InputStream in = new BufferedInputStream(multipartFile.getInputStream(), INPUT_STREAM_BUFFER_SIZE)) {
128132
var startTime = LocalDateTime.now();
129133
FileEntry fileEntry = fileStorageThreadPool.submit(createUploadFileTask(spaceGuid, namespace, multipartFile, in))

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,12 @@
468468
<artifactId>slf4j-jdk14</artifactId>
469469
<version>${slf4j.version}</version>
470470
</dependency>
471+
<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-core -->
472+
<dependency>
473+
<groupId>org.apache.tika</groupId>
474+
<artifactId>tika-core</artifactId>
475+
<version>3.1.0</version>
476+
</dependency>
471477
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
472478
<dependency>
473479
<groupId>commons-io</groupId>

0 commit comments

Comments
 (0)