Skip to content

Commit 5fc51ae

Browse files
committed
Add uploaded file validation
LMCROSSITXSADEPLOY-3175
1 parent fa61b07 commit 5fc51ae

14 files changed

Lines changed: 284 additions & 64 deletions

File tree

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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,11 @@
5959
requires org.apache.commons.collections4;
6060
requires org.apache.commons.io;
6161
requires org.apache.commons.lang3;
62+
requires org.apache.httpcomponents.httpclient;
63+
requires org.apache.httpcomponents.httpcore;
6264
requires org.apache.httpcomponents.client5.httpclient5;
6365
requires org.apache.httpcomponents.core5.httpcore5;
66+
requires org.apache.tika.core;
6467
requires org.cloudfoundry.multiapps.common;
6568
requires org.cloudfoundry.multiapps.controller.api;
6669
requires org.slf4j;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ public final class Messages {
9191
public static final String BUILDPACKS_REQUIRED_FOR_CNB = "Buildpacks must be provided when lifecycle is set to 'cnb'.";
9292
public static final String DOCKER_INFO_REQUIRED = "Docker information must be provided when lifecycle is set to 'docker'.";
9393
public static final String BUILDPACKS_NOT_ALLOWED_WITH_DOCKER = "Buildpacks must not be provided when lifecycle is set to 'docker'.";
94+
public static final String EXTENSION_DESCRIPTORS_COULD_NOT_BE_PARSED_TO_VALID_YAML = "Extension descriptor(s) could not be parsed as a valid YAML file. These descriptors may fail future deployments once stricter validation is enforced. Please review and correct them now to avoid future issues. Use at your own risk";
95+
public static final String UNSUPPORTED_FILE_FORMAT = "Unsupported file format! \"{0}\" detected";
9496

9597
// Warning messages
9698
public static final String ENVIRONMENT_VARIABLE_IS_NOT_SET_USING_DEFAULT = "Environment variable \"{0}\" is not set. Using default \"{1}\"...";

multiapps-controller-core/src/main/java/org/cloudfoundry/multiapps/controller/core/helpers/DescriptorParserFacadeFactory.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import jakarta.inject.Inject;
44
import jakarta.inject.Named;
5-
65
import org.cloudfoundry.multiapps.common.util.YamlParser;
76
import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration;
87
import org.cloudfoundry.multiapps.mta.handlers.DescriptorParserFacade;
@@ -19,9 +18,21 @@ public DescriptorParserFacadeFactory(ApplicationConfiguration applicationConfigu
1918
}
2019

2120
public DescriptorParserFacade getInstance() {
22-
LoaderOptions loaderOptions = new LoaderOptions();
23-
loaderOptions.setMaxAliasesForCollections(applicationConfiguration.getSnakeyamlMaxAliasesForCollections());
21+
LoaderOptions loaderOptions = createLoaderOptions(true);
2422
YamlParser yamlParser = new YamlParser(loaderOptions);
2523
return new DescriptorParserFacade(yamlParser);
2624
}
25+
26+
public DescriptorParserFacade getInstanceWithDisabledDuplicateKeys() {
27+
LoaderOptions loaderOptions = createLoaderOptions(false);
28+
YamlParser yamlParser = new YamlParser(loaderOptions);
29+
return new DescriptorParserFacade(yamlParser);
30+
}
31+
32+
private LoaderOptions createLoaderOptions(boolean shouldAllowDuplicateKeys) {
33+
LoaderOptions loaderOptions = new LoaderOptions();
34+
loaderOptions.setMaxAliasesForCollections(applicationConfiguration.getSnakeyamlMaxAliasesForCollections());
35+
loaderOptions.setAllowDuplicateKeys(shouldAllowDuplicateKeys);
36+
return loaderOptions;
37+
}
2738
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package org.cloudfoundry.multiapps.controller.core.validators.parameters;
2+
3+
import java.io.BufferedInputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.text.MessageFormat;
7+
import java.util.function.Consumer;
8+
9+
import jakarta.inject.Inject;
10+
import jakarta.inject.Named;
11+
import org.apache.tika.Tika;
12+
import org.cloudfoundry.multiapps.common.ParsingException;
13+
import org.cloudfoundry.multiapps.common.SLException;
14+
import org.cloudfoundry.multiapps.controller.core.Messages;
15+
import org.cloudfoundry.multiapps.controller.core.helpers.DescriptorParserFacadeFactory;
16+
import org.cloudfoundry.multiapps.controller.persistence.services.FileService;
17+
import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException;
18+
19+
@Named
20+
public class FileMimeTypeValidator {
21+
22+
private static final String APPLICATION_OCTET_STREAM_MIME_TYPE = "application/octet-stream";
23+
private static final String APPLICATION_ZIP_MIME_TYPE = "application/zip";
24+
private static final String TEXT_PLAIN_MIME_TYPE = "text/plain";
25+
private static final Tika mimeTypeDetector = new Tika();
26+
private final DescriptorParserFacadeFactory descriptorParserFactory;
27+
private final FileService fileService;
28+
29+
@Inject
30+
public FileMimeTypeValidator(FileService fileService, DescriptorParserFacadeFactory descriptorParserFactory) {
31+
this.fileService = fileService;
32+
this.descriptorParserFactory = descriptorParserFactory;
33+
}
34+
35+
public void validateFileType(String spaceGuid, String appArchiveId, Consumer<String> stepLogger) {
36+
try (InputStream fileInputStream = fileService.openInputStream(spaceGuid, appArchiveId);
37+
InputStream bufferedFileInputStream = new BufferedInputStream(fileInputStream)) {
38+
validateInputStreamMimeType(bufferedFileInputStream, stepLogger);
39+
} catch (FileStorageException | IOException e) {
40+
throw new SLException(e);
41+
}
42+
}
43+
44+
private void validateInputStreamMimeType(InputStream uploadedFileInputStream, Consumer<String> stepLogger) throws IOException {
45+
String detectedType = getFileMimeType(uploadedFileInputStream);
46+
47+
switch (detectedType) {
48+
case TEXT_PLAIN_MIME_TYPE -> validateYamlFile(uploadedFileInputStream, stepLogger);
49+
case APPLICATION_ZIP_MIME_TYPE, APPLICATION_OCTET_STREAM_MIME_TYPE -> {
50+
}
51+
default -> stepLogger.accept(MessageFormat.format(Messages.UNSUPPORTED_FILE_FORMAT, detectedType));
52+
}
53+
54+
}
55+
56+
private String getFileMimeType(InputStream uploadedFileInputStream) throws IOException {
57+
return mimeTypeDetector.detect(uploadedFileInputStream);
58+
}
59+
60+
private void validateYamlFile(InputStream uploadedFileInputStream, Consumer<String> stepLogger) {
61+
try {
62+
descriptorParserFactory.getInstanceWithDisabledDuplicateKeys()
63+
.parseExtensionDescriptor(uploadedFileInputStream);
64+
} catch (ParsingException e) {
65+
stepLogger.accept(Messages.EXTENSION_DESCRIPTORS_COULD_NOT_BE_PARSED_TO_VALID_YAML);
66+
stepLogger.accept(e.getMessage());
67+
}
68+
}
69+
}

multiapps-controller-core/src/test/java/org/cloudfoundry/multiapps/controller/core/helpers/DescriptorParserFacadeFactoryTest.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,16 @@ void testGetInstance() {
2323
Assertions.assertThrows(ParsingException.class, () -> instance.parseDeploymentDescriptor(mtadYaml));
2424
}
2525

26+
@Test
27+
void testParseWithDuplicateValues() {
28+
final int maxAliases = 5;
29+
ApplicationConfiguration applicationConfiguration = Mockito.mock(ApplicationConfiguration.class);
30+
Mockito.when(applicationConfiguration.getSnakeyamlMaxAliasesForCollections())
31+
.thenReturn(maxAliases);
32+
DescriptorParserFacadeFactory descriptorParserFacadeFactory = new DescriptorParserFacadeFactory(applicationConfiguration);
33+
DescriptorParserFacade instance = descriptorParserFacadeFactory.getInstanceWithDisabledDuplicateKeys();
34+
InputStream mtadYaml = getClass().getResourceAsStream("duplicate-keys.yaml");
35+
Assertions.assertThrows(ParsingException.class, () -> instance.parseDeploymentDescriptor(mtadYaml));
36+
}
37+
2638
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.cloudfoundry.multiapps.controller.core.validators.parameters;
2+
3+
import java.io.InputStream;
4+
import java.util.UUID;
5+
6+
import org.cloudfoundry.multiapps.controller.core.Messages;
7+
import org.cloudfoundry.multiapps.controller.core.helpers.DescriptorParserFacadeFactory;
8+
import org.cloudfoundry.multiapps.controller.core.util.ApplicationConfiguration;
9+
import org.cloudfoundry.multiapps.controller.persistence.services.FileService;
10+
import org.cloudfoundry.multiapps.controller.persistence.services.FileStorageException;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
import org.mockito.Mock;
14+
import org.mockito.Mockito;
15+
import org.mockito.MockitoAnnotations;
16+
17+
import static org.junit.jupiter.api.Assertions.assertTrue;
18+
import static org.mockito.Mockito.when;
19+
20+
class FileMimeTypeValidatorTest {
21+
22+
@Mock
23+
private FileService fileService;
24+
25+
private FileMimeTypeValidator fileMimeTypeValidator;
26+
27+
private final String TEST_SPACE_ID = UUID.randomUUID()
28+
.toString();
29+
private final String TEST_FILE_ID = UUID.randomUUID()
30+
.toString();
31+
32+
private DescriptorParserFacadeFactory descriptorParserFacadeFactory;
33+
34+
@BeforeEach
35+
void setUp() throws Exception {
36+
MockitoAnnotations.openMocks(this)
37+
.close();
38+
int maxAliases = 5;
39+
ApplicationConfiguration applicationConfiguration = Mockito.mock(ApplicationConfiguration.class);
40+
when(applicationConfiguration.getSnakeyamlMaxAliasesForCollections())
41+
.thenReturn(maxAliases);
42+
descriptorParserFacadeFactory = new DescriptorParserFacadeFactory(applicationConfiguration);
43+
fileMimeTypeValidator = new FileMimeTypeValidator(fileService, descriptorParserFacadeFactory);
44+
}
45+
46+
@Test
47+
void testValidationWithCorrectYaml() throws FileStorageException {
48+
InputStream mtadYaml = getClass().getResourceAsStream("valid-format.yaml");
49+
when(fileService.openInputStream(TEST_SPACE_ID, TEST_FILE_ID)).thenReturn(mtadYaml);
50+
fileMimeTypeValidator = new FileMimeTypeValidator(fileService, descriptorParserFacadeFactory);
51+
fileMimeTypeValidator.validateFileType(TEST_SPACE_ID, TEST_FILE_ID, message -> {
52+
});
53+
}
54+
55+
@Test
56+
void testValidationWithDuplicateKeys() throws FileStorageException {
57+
InputStream mtadYaml = getClass().getResourceAsStream("duplicate-key.yaml");
58+
when(fileService.openInputStream(TEST_SPACE_ID, TEST_FILE_ID)).thenReturn(mtadYaml);
59+
fileMimeTypeValidator = new FileMimeTypeValidator(fileService, descriptorParserFacadeFactory);
60+
fileMimeTypeValidator.validateFileType(TEST_SPACE_ID, TEST_FILE_ID, message -> {
61+
boolean doesMessageContainWarningMessage = message.contains(Messages.EXTENSION_DESCRIPTORS_COULD_NOT_BE_PARSED_TO_VALID_YAML);
62+
boolean doesMessageContainException = message.contains("Error while parsing YAML string");
63+
assertTrue(doesMessageContainException || doesMessageContainWarningMessage);
64+
});
65+
}
66+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
_schema-version: "3.1"
2+
ID: com.test.1
3+
version: 1.0.0
4+
5+
modules:
6+
- name: spark-service-admin
7+
type: abc
8+
properties:
9+
MAX_SIZE: а
10+
MAX_SIZE: а
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
_schema-version: "3.3"
2+
ID: com.test
3+
extends: com.test
4+
5+
modules:
6+
- name: spark-service-admin
7+
properties:
8+
MAX_SIZE: а
9+
MAX_SIZE: а
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
_schema-version: "3.3"
2+
ID: com.test
3+
extends: com.test
4+
5+
modules:
6+
- name: spark-service-admin
7+
properties:
8+
MAX_SIZE: а

0 commit comments

Comments
 (0)