Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync with folio #107

Merged
merged 8 commits into from
Mar 28, 2023
2 changes: 1 addition & 1 deletion components/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<dependency>
<groupId>org.folio</groupId>
<artifactId>spring-domain</artifactId>
<version>1.1.1</version>
<version>${spring-module-core.version}</version>
</dependency>

<dependency>
Expand Down
12 changes: 11 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<parent>
<groupId>org.folio</groupId>
<artifactId>spring-module-core</artifactId>
<version>1.1.2</version>
<version>1.1.4</version>
</parent>

<modules>
Expand All @@ -33,8 +33,18 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-module-core.version>1.1.4</spring-module-core.version>
</properties>

<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>folio-nexus</id>
Expand Down
1 change: 1 addition & 0 deletions service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
events/
10 changes: 8 additions & 2 deletions service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,13 @@
<dependency>
<groupId>org.folio</groupId>
<artifactId>spring-tenant</artifactId>
<version>1.1.1</version>
<version>${spring-module-core.version}</version>
</dependency>

<dependency>
<groupId>org.folio</groupId>
<artifactId>spring-web</artifactId>
<version>1.1.1</version>
<version>${spring-module-core.version}</version>
</dependency>

<dependency>
Expand Down Expand Up @@ -158,6 +158,12 @@
<artifactId>spring-data-rest-hal-explorer</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,29 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.FileSystemException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.jms.JMSException;
import javax.servlet.http.HttpServletRequest;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

import org.apache.commons.lang3.StringUtils;
import org.folio.rest.workflow.exception.EventPublishException;
import org.folio.rest.workflow.jms.EventProducer;
import org.folio.rest.workflow.jms.model.Event;
import org.folio.rest.workflow.model.Trigger;
import org.folio.rest.workflow.model.repo.TriggerRepo;
import org.folio.spring.tenant.annotation.TenantHeader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpMethod;
import org.springframework.util.PathMatcher;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -36,11 +36,22 @@
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.HandlerMapping;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;

@RestController
@RequestMapping("/events")
public class EventController {

private static final Logger logger = LoggerFactory.getLogger(EventController.class);
private static final Pattern TENANT_PATTERN = Pattern.compile("^[a-z0-9_-]+$");

@Value("${tenant.headerName:X-Okapi-Tenant}")
private String tenantHeaderName;

@Value("${event.uploads.path}")
private String eventUploadsDirectory;

@Autowired
private EventProducer eventProducer;
Expand Down Expand Up @@ -69,13 +80,34 @@ public JsonNode postHandleEvents(
public JsonNode postHandleEventsWithFile(
@RequestParam("file") MultipartFile multipartFile,
@RequestParam("path") String directoryPath,
@TenantHeader String tenant,
HttpServletRequest request
) throws EventPublishException, IOException {
// @formatter:on

if (! TENANT_PATTERN.matcher(tenant).matches()) {
throw new FileSystemException("Invalid tenant directory name");
}

ObjectNode body = objectMapper.createObjectNode();
String filePath = StringUtils.appendIfMissing(directoryPath, File.separator) + multipartFile.getOriginalFilename();
body.put("inputFilePath", filePath);

Path tenantPath = Path.of(eventUploadsDirectory)
.resolve(tenant)
.normalize();

Path filePath = tenantPath.resolve(directoryPath)
.resolve(multipartFile.getOriginalFilename())
.normalize();

if (!filePath.startsWith(tenantPath)) {
throw new FileSystemException("Path/directory traversal attack");
}

File file = filePath.toFile();

file.mkdirs();

body.put("inputFilePath", tenantPath.relativize(filePath).toString());

Collections.list(request.getParameterNames())
.stream()
Expand All @@ -85,12 +117,8 @@ public JsonNode postHandleEventsWithFile(
body.put(name, request.getParameter(name));
});

File file = new File(filePath);

file.mkdirs();

try (InputStream is = multipartFile.getInputStream()) {
Files.copy(is, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
Files.copy(is, filePath, StandardCopyOption.REPLACE_EXISTING);
}

return processRequest(request, body);
Expand All @@ -100,7 +128,7 @@ private JsonNode processRequest(
HttpServletRequest request,
JsonNode body
) throws EventPublishException {
String tenant = request.getHeader("X-Okapi-Tenant");
String tenant = request.getHeader(tenantHeaderName);

String requestPath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
HttpMethod method = HttpMethod.valueOf(request.getMethod());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.folio.rest.workflow.controller.advice;

import java.nio.file.FileSystemException;

import org.folio.rest.workflow.exception.EventPublishException;
import org.folio.spring.model.response.ResponseErrors;
import org.folio.spring.utility.ErrorUtility;
Expand All @@ -22,4 +24,11 @@ public ResponseErrors handleEventPublishException(EventPublishException exceptio
return ErrorUtility.buildError(exception, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(FileSystemException.class)
public ResponseErrors handleFileSystemException(FileSystemException exception) {
logger.debug(exception.getMessage(), exception);
return ErrorUtility.buildError(exception, HttpStatus.BAD_REQUEST);
}

}
16 changes: 13 additions & 3 deletions service/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
logging:
file: logs/mod-workflow.log
file:
path: logs
name: mod-workflow.log
level:
com:
zaxxer:
Expand All @@ -20,14 +22,18 @@ spring:
data.rest:
returnBodyOnCreate: true
returnBodyOnUpdate: true

sql:
init:
platform: postgres

datasource:
hikari:
leakDetectionThreshold: 180000
connectionTimeout: 30000
idleTimeout: 600000
maxLifetime: 1800000
maximumPoolSize: 16
platform: postgres
driverClassName: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/mod_workflow

Expand All @@ -48,7 +54,11 @@ spring:
mode: TEXT
suffix: .sql

event.queue.name: event.queue
event:
uploads:
path: events
queue:
name: event.queue

tenant:
header-name: X-Okapi-Tenant
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package org.folio.rest.workflow.controller;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystemException;
import java.util.stream.Stream;
import org.apache.commons.io.FileUtils;
import org.folio.rest.workflow.jms.EventProducer;
import org.folio.rest.workflow.model.repo.TriggerRepo;
import org.folio.spring.tenant.properties.TenantProperties;
import org.folio.spring.tenant.resolver.TenantHeaderResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

@WebMvcTest(EventController.class)
class EventControllerTest {

private MockMvc mockMvc;

@Autowired
private EventController eventController;

@MockBean
private EventProducer eventProducer;

@MockBean
private TriggerRepo triggerRepo;

@MockBean
private TenantProperties tenantProperties;

@BeforeEach
void beforeEach() {
mockMvc = MockMvcBuilders.standaloneSetup(eventController)
.setCustomArgumentResolvers(new TenantHeaderResolver("X-Okapi-Tenant"))
.build();
}

@ParameterizedTest
@MethodSource
void upload(String tenant, String dir, String file, String expectedPath) throws Exception {
String expectedInputFilePath = expectedPath.replaceFirst("[^/]+/[^/]+/", "");
mockMvc.perform(upload(tenant, dir, file))
.andExpectAll(status().isOk(), jsonPath("inputFilePath").value(expectedInputFilePath));
assertThat(readFile(expectedPath)).isEqualTo("This is the file content");
}

static Stream<Arguments> upload() {
return Stream.of(
arguments("diku", "d1/d2/d3", "filename.txt", "events/diku/d1/d2/d3/filename.txt"),
arguments("diku", "", "baz.txt", "events/diku/baz.txt"),
arguments("foo", "a/../b", "c/d/../e/f.txt", "events/foo/b/c/e/f.txt"));
}

@ParameterizedTest
@MethodSource
void uploadRejected(String tenant, String dir, String file) throws Exception {
assertThrows(FileSystemException.class, () ->
mockMvc.perform(upload(tenant, dir, file)));
}

static Stream<Arguments> uploadRejected() {
return Stream.of(
arguments("diku/../x", "a", "a.txt"),
arguments("diku/../../x", "a", "a.txt"),
arguments("diku", "a/../../x", "a.txt"),
arguments("diku", "../x", "a.txt"),
arguments("diku", "a", "../../x.txt"),
arguments("diku", "a", "../../../x.txt"),
arguments("diku", "a", "../../../../x.txt"));
}

MockHttpServletRequestBuilder upload(String tenant, String dir, String file) throws Exception {
var sampleFile = new MockMultipartFile(
"file",
file,
MediaType.TEXT_PLAIN_VALUE,
"This is the file content".getBytes());

return MockMvcRequestBuilders.multipart("/events").file(sampleFile)
.header("X-Okapi-Tenant", tenant).param("path", dir);
}

private static String readFile(String path) throws IOException {
return FileUtils.readFileToString(new File(path), StandardCharsets.UTF_8);
}
}
15 changes: 11 additions & 4 deletions service/src/test/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
logging:
file: logs/mod-workflow-tests.log
file:
path: logs
name: mod-workflow-test.log
level:
org:
folio: INFO
hibernate: INFO
springframework: INFO
path:

server:
port: 9101
Expand All @@ -16,8 +17,10 @@ spring:
data.rest:
returnBodyOnCreate: true
returnBodyOnUpdate: true
sql:
init:
platform: h2
datasource:
platform: h2
url: jdbc:h2:mem:AZ;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
driverClassName: org.h2.Driver
username: folio
Expand All @@ -39,7 +42,11 @@ spring:
mode: TEXT
suffix: .sql

event.queue.name: event.queue
event:
uploads:
path: events
queue:
name: event.queue

tenant:
header-name: X-Okapi-Tenant
Expand Down