Skip to content

Commit

Permalink
Merge pull request #107 from TAMULib/sync-with-folio
Browse files Browse the repository at this point in the history
Sync with folio
  • Loading branch information
jeremythuff authored Mar 28, 2023
2 parents b74b312 + 9b45c18 commit d58c403
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 27 deletions.
4 changes: 2 additions & 2 deletions components/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
<parent>
<groupId>org.folio</groupId>
<artifactId>workflow-parent</artifactId>
<version>1.1.3-SNAPSHOT</version>
<version>1.1.4</version>
</parent>

<dependencies>

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

<dependency>
Expand Down
14 changes: 12 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<artifactId>workflow-parent</artifactId>

<version>1.1.3-SNAPSHOT</version>
<version>1.1.4</version>

<name>Workflow Parent</name>

Expand All @@ -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/
12 changes: 9 additions & 3 deletions service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.folio</groupId>
<artifactId>workflow-parent</artifactId>
<version>1.1.3-SNAPSHOT</version>
<version>1.1.4</version>
</parent>

<packaging>jar</packaging>
Expand Down 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);
}
}
Loading

0 comments on commit d58c403

Please sign in to comment.