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

feat: P4ADEV-1830-save-file-to-shared-folder #9

Merged
merged 12 commits into from
Jan 15, 2025
3 changes: 3 additions & 0 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,12 @@ microservice-chart:

SHARED_FOLDER_ROOT: "/shared"

ORGANIZATION_BASE_URL: "http://p4pa-organization-microservice-chart:8080"
AUTH_SERVER_BASE_URL: "http://p4pa-auth-microservice-chart:8080/payhub"

envSecret:
APPLICATIONINSIGHTS_CONNECTION_STRING: appinsights-connection-string
FILE_ENCRYPT_PASSWORD: file-encrypt-password

# nodeSelector: {}

Expand Down
1 change: 1 addition & 0 deletions openapi/p4pa-fileshare.openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,6 @@ components:
type: string
enum:
- INVALID_FILE
- FILE_UPLOAD_ERROR
message:
type: string
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package it.gov.pagopa.pu.fileshare.config;

import it.gov.pagopa.pu.fileshare.dto.generated.IngestionFlowFileType;
import java.util.Map;
import java.util.Optional;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "folders")
public class FoldersPathsConfig {
private String shared;
private Map<IngestionFlowFileType,String> ingestionFlowFileTypePaths;

public String getIngestionFlowFilePath(IngestionFlowFileType ingestionFlowFileType) {
return Optional.ofNullable(
ingestionFlowFileTypePaths.get(ingestionFlowFileType))
.orElseThrow(()-> {
log.debug("No path configured for ingestionFlowFileType {}",ingestionFlowFileType);
return new UnsupportedOperationException();
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import it.gov.pagopa.pu.fileshare.dto.generated.FileshareErrorDTO;
import it.gov.pagopa.pu.fileshare.dto.generated.FileshareErrorDTO.CodeEnum;
import it.gov.pagopa.pu.fileshare.exception.custom.FileUploadException;
import it.gov.pagopa.pu.fileshare.exception.custom.InvalidFileException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -26,6 +27,11 @@ public ResponseEntity<FileshareErrorDTO> handleInvalidFileError(RuntimeException
return handleFileshareErrorException(ex, request, HttpStatus.BAD_REQUEST, CodeEnum.INVALID_FILE);
}

@ExceptionHandler({FileUploadException.class})
public ResponseEntity<FileshareErrorDTO> handleFileStorageError(RuntimeException ex, HttpServletRequest request){
return handleFileshareErrorException(ex, request, HttpStatus.INTERNAL_SERVER_ERROR, CodeEnum.FILE_UPLOAD_ERROR);
}

static ResponseEntity<FileshareErrorDTO> handleFileshareErrorException(RuntimeException ex, HttpServletRequest request, HttpStatus httpStatus, FileshareErrorDTO.CodeEnum errorEnum) {
String message = logException(ex, request, httpStatus);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package it.gov.pagopa.pu.fileshare.exception.custom;

public class FileUploadException extends RuntimeException {
public FileUploadException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package it.gov.pagopa.pu.fileshare.service;

import it.gov.pagopa.pu.fileshare.exception.custom.InvalidFileException;
import java.util.stream.Stream;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
Expand All @@ -9,8 +10,26 @@
@Slf4j
@Service
public class FileService {

public void validateFile(MultipartFile ingestionFlowFile, String validFileExt) {
if( ingestionFlowFile == null || !StringUtils.defaultString(ingestionFlowFile.getOriginalFilename()).endsWith(validFileExt)){
if( ingestionFlowFile == null){
log.debug("Invalid ingestion flow file");
throw new InvalidFileException("Invalid file");
}
String filename = StringUtils.defaultString(ingestionFlowFile.getOriginalFilename());
validateFileExtension(validFileExt, filename);
validateFilename(filename);
}

public static void validateFilename(String filename) {
if(Stream.of("..", "\\", "/").anyMatch(filename::contains)){
log.debug("Invalid ingestion flow filename");
throw new InvalidFileException("Invalid filename");
}
}

private static void validateFileExtension(String validFileExt, String filename) {
if(!filename.endsWith(validFileExt)){
log.debug("Invalid ingestion flow file extension");
throw new InvalidFileException("Invalid file extension");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package it.gov.pagopa.pu.fileshare.service;

import it.gov.pagopa.pu.fileshare.config.FoldersPathsConfig;
import it.gov.pagopa.pu.fileshare.exception.custom.FileUploadException;
import it.gov.pagopa.pu.fileshare.exception.custom.InvalidFileException;
import it.gov.pagopa.pu.fileshare.util.AESUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
@Service
public class FileStorerService {
private final FoldersPathsConfig foldersPathsConfig;
private final String fileEncryptPassword;

public FileStorerService(FoldersPathsConfig foldersPathsConfig,
@Value(("${app.fileEncryptPassword}")) String fileEncryptPassword) {
this.foldersPathsConfig = foldersPathsConfig;
this.fileEncryptPassword = fileEncryptPassword;
}

private Path getFilePath(String relativePath, String filename) {
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
String basePath = foldersPathsConfig.getShared()+relativePath;
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
Path fileLocation = Paths.get(basePath,filename).normalize();
if(!fileLocation.startsWith(basePath)){
log.debug("Invalid file path");
throw new InvalidFileException("Invalid file path");
}
return fileLocation;
}

public String saveToSharedFolder(MultipartFile file, String relativePath){
if(file==null){
log.debug("File is mandatory");
throw new FileUploadException("File is mandatory");
}

String sharedFolderRootPath = foldersPathsConfig.getShared();
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
String filename = org.springframework.util.StringUtils.cleanPath(
StringUtils.defaultString(file.getOriginalFilename()));
FileService.validateFilename(filename);
Path fileLocation = getFilePath(relativePath, filename);
//create missing parent folder, if any
try {
if (!Files.exists(fileLocation.getParent()))
Files.createDirectories(fileLocation.getParent());
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
encryptAndSaveFile(file, fileLocation);
}catch (Exception e) {
String errorMessage = "Error uploading file to folder %s%s".formatted(
sharedFolderRootPath,
relativePath);
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
log.debug(
errorMessage, e);
throw new FileUploadException(
errorMessage);
}
log.debug("File upload to folder %s%s completed".formatted(sharedFolderRootPath,
relativePath));
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
return Paths.get(relativePath,filename).toString();
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
}

private void encryptAndSaveFile(MultipartFile file, Path fileLocation)
throws IOException {
try(InputStream is = file.getInputStream();
InputStream cipherIs = AESUtils.encrypt(fileEncryptPassword, is)){
Files.copy(cipherIs, fileLocation, StandardCopyOption.REPLACE_EXISTING);
Fixed Show fixed Hide fixed
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package it.gov.pagopa.pu.fileshare.service.ingestion;

import it.gov.pagopa.pu.fileshare.config.FoldersPathsConfig;
import it.gov.pagopa.pu.fileshare.dto.generated.IngestionFlowFileType;
import it.gov.pagopa.pu.fileshare.service.FileService;
import it.gov.pagopa.pu.fileshare.service.FileStorerService;
import it.gov.pagopa.pu.fileshare.service.UserAuthorizationService;
import it.gov.pagopa.pu.p4paauth.dto.generated.UserInfo;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -14,19 +16,28 @@
public class IngestionFlowFileServiceImpl implements IngestionFlowFileService {
private final UserAuthorizationService userAuthorizationService;
private final FileService fileService;
private final FileStorerService fileStorerService;
private final FoldersPathsConfig foldersPathsConfig;
private final String validIngestionFlowFileExt;

public IngestionFlowFileServiceImpl(
UserAuthorizationService userAuthorizationService, FileService fileService,
@Value("${uploads.ingestion-flow-file.valid-extension}") String validIngestionFlowFileExt) {
FileStorerService fileStorerService,
FoldersPathsConfig foldersPathsConfig,
@Value("${uploads.ingestion-flow-file.valid-extension}") String validIngestionFlowFileExt
) {
this.userAuthorizationService = userAuthorizationService;
this.fileService = fileService;
this.fileStorerService = fileStorerService;
this.foldersPathsConfig = foldersPathsConfig;
this.validIngestionFlowFileExt = validIngestionFlowFileExt;
}

@Override
public void uploadIngestionFlowFile(Long organizationId, IngestionFlowFileType ingestionFlowFileType, MultipartFile ingestionFlowFile, UserInfo user, String accessToken) {
userAuthorizationService.checkUserAuthorization(organizationId, user, accessToken);
fileService.validateFile(ingestionFlowFile, validIngestionFlowFileExt);
fileStorerService.saveToSharedFolder(ingestionFlowFile,
foldersPathsConfig.getIngestionFlowFilePath(ingestionFlowFileType));
}
}
10 changes: 10 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ folders:
process-target-sub-folders:
archive: "\${PROCESS_TARGET_SUB_FOLDER_ARCHIVE:archive}"
errors: "\${PROCESS_TARGET_SUB_FOLDER_ERRORS:errors}"
ingestion-flow-file-type-paths:
RECEIPT: "\${INGESTION_FLOW_FILE_RECEIPT_PATH:/receipt}"
PAYMENTS_REPORTING: "\${INGESTION_FLOW_FILE_PAYMENTS_REPORTING_PATH:/payments_reporting}"
OPI: "\${INGESTION_FLOW_FILE_OPI_PATH:/opi}"
TREASURY_CSV: "\${INGESTION_FLOW_FILE_TREASURY_CSV_PATH:/treasury_csv}"
TREASURY_XLS: "\${INGESTION_FLOW_FILE_TREASURY_XLS_PATH:/treasury_xls}"
TREASURY_POSTE: "\${INGESTION_FLOW_FILE_TREASURY_POSTE_PATH:/treasury_poste}"
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved

rest:
default-timeout:
Expand All @@ -64,3 +71,6 @@ rest:
uploads:
ingestion-flow-file:
valid-extension: "\${INGESTION_FLOW_FILE_VALID_EXTENSION:.zip}"

app:
fileEncryptPassword: "\${FILE_ENCRYPT_PASSWORD:ENCR_PSW}"
antonioT90 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package it.gov.pagopa.pu.fileshare.config;

import it.gov.pagopa.pu.fileshare.dto.generated.IngestionFlowFileType;
import java.util.EnumMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class FoldersPathsConfigTest {
private FoldersPathsConfig foldersPathsConfig;

@BeforeEach
void setUp() {
foldersPathsConfig = new FoldersPathsConfig();

}

@Test
void givenPopulatedPathWhenGetIngestionFlowFilePathThenOK(){
String expected = "/receipt";
Map<IngestionFlowFileType,String> paths = new EnumMap<>(
IngestionFlowFileType.class);
paths.put(IngestionFlowFileType.RECEIPT, "/receipt");
foldersPathsConfig.setIngestionFlowFileTypePaths(paths);

String result = foldersPathsConfig.getIngestionFlowFilePath(
IngestionFlowFileType.RECEIPT);

Assertions.assertEquals(expected,result);
}

@Test
void givenNoPathWhenGetIngestionFlowFilePathThenUnsupportedOperation(){
foldersPathsConfig.setIngestionFlowFileTypePaths(new EnumMap<>(IngestionFlowFileType.class));
try {
foldersPathsConfig.getIngestionFlowFilePath(
IngestionFlowFileType.RECEIPT);
Assertions.fail("Expected UnsupportedOperationException");
}catch (UnsupportedOperationException e){
//do nothing
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static org.mockito.Mockito.doThrow;

import it.gov.pagopa.pu.fileshare.dto.generated.FileshareErrorDTO.CodeEnum;
import it.gov.pagopa.pu.fileshare.exception.custom.FileUploadException;
import it.gov.pagopa.pu.fileshare.exception.custom.InvalidFileException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -58,4 +59,17 @@ void handleInvalidFileException() throws Exception {
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(CodeEnum.INVALID_FILE.toString()))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Error"));
}

@Test
void handleFileUploadException() throws Exception {
doThrow(new FileUploadException("Error")).when(testControllerSpy).testEndpoint(DATA);

mockMvc.perform(MockMvcRequestBuilders.get("/test")
.param(DATA, DATA)
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isInternalServerError())
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(CodeEnum.FILE_UPLOAD_ERROR.toString()))
.andExpect(MockMvcResultMatchers.jsonPath("$.message").value("Error"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ void givenValidFileExtensionWhenValidateFileThenOk(){
fileService.validateFile(file, VALID_FILE_EXTENSION);
}

@Test
void givenNoFileWhenValidateFileThenInvalidFileException(){
try{
fileService.validateFile(null, VALID_FILE_EXTENSION);
Assertions.fail("Expected InvalidFileException");
}catch(InvalidFileException e){
//do nothing
}
}

@Test
void givenInvalidFileExtensionWhenValidateFileThenInvalidFileException(){
MockMultipartFile file = new MockMultipartFile(
Expand All @@ -47,4 +57,21 @@ void givenInvalidFileExtensionWhenValidateFileThenInvalidFileException(){
//do nothing
}
}

@Test
void givenInvalidFilenameWhenValidateFileThenInvalidFileException(){
MockMultipartFile file = new MockMultipartFile(
"ingestionFlowFile",
"../test.zip",
MediaType.TEXT_PLAIN_VALUE,
"this is a test file".getBytes()
);

try{
fileService.validateFile(file, VALID_FILE_EXTENSION);
Assertions.fail("Expected InvalidFileException");
}catch(InvalidFileException e){
//do nothing
}
}
}
Loading
Loading