WIP: Draft: RED-10730: added internal endpoints for dossier-initialization-service #927

Draft
ali.oezyetimoglu1 wants to merge 5 commits from RED-10730 into master
12 changed files with 407 additions and 21 deletions

View File

@ -2,17 +2,19 @@ package com.iqser.red.service.persistence.v1.internal.api.controller;
import java.util.List;
import jakarta.transaction.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.iqser.red.service.persistence.management.v1.processor.service.DossierManagementService;
import com.iqser.red.service.persistence.service.v1.api.internal.resources.DossierResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.CreateOrUpdateDossierRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity;
import feign.Param;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -42,4 +44,18 @@ public class DossierInternalController implements DossierResource {
return dossierManagementService.getDossierById(dossierId, includeArchived, includeDeleted);
}
@Override
@Transactional
public Dossier createDossier(@RequestBody CreateOrUpdateDossierRequest dossierRequest) {
if (dossierRequest.getOwnerId().equals("internal_user")) {
System.out.println("USERRRR: " + KeycloakSecurity.getUserId());
dossierRequest.setOwnerId(KeycloakSecurity.getUserId());
// dossierRequest.setOwnerId(KeycloakSecurity.getUserId());
// dossierRequest.setOwnerId(KeycloakSecurity.getUserId());
}
return dossierManagementService.createDossier(dossierRequest);
}
}

View File

@ -2,6 +2,8 @@ package com.iqser.red.service.persistence.v1.internal.api.controller;
import static com.knecon.fforesight.databasetenantcommons.providers.utils.MagicConverter.convert;
import java.util.List;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@ -37,4 +39,11 @@ public class DossierTemplateInternalController implements DossierTemplateResourc
return convert(dossierTemplatePersistenceService.getDossierTemplate(dossierTemplateId), DossierTemplate.class);
}
@Override
public List<DossierTemplate> getAllDossierTemplates() {
return convert(dossierTemplatePersistenceService.getAllDossierTemplates(), DossierTemplate.class);
}
}

View File

@ -0,0 +1,35 @@
package com.iqser.red.service.persistence.v1.internal.api.controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.iqser.red.service.persistence.management.v1.processor.service.UploadManagementService;
import com.iqser.red.service.persistence.service.v1.api.internal.resources.UploadResource;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileUploadResult;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequiredArgsConstructor
@Slf4j
@SuppressWarnings("PMD")
public class UploadInternalController implements UploadResource {
private final UploadManagementService uploadManagementService;
@Override
public FileUploadResult upload(@RequestPart(name = "file") MultipartFile file,
@PathVariable(DOSSIER_ID) String dossierId,
@RequestParam(value = "keepManualRedactions", required = false, defaultValue = "false") boolean keepManualRedactions,
@Parameter(name = DISABLE_AUTOMATIC_ANALYSIS_PARAM, description = "Disables automatic redaction for the uploaded file, imports only imported redactions") @RequestParam(value = DISABLE_AUTOMATIC_ANALYSIS_PARAM, required = false, defaultValue = "false") boolean disableAutomaticAnalysis) {
return uploadManagementService.upload(file, dossierId, keepManualRedactions, disableAutomaticAnalysis);
}
}

View File

@ -6,9 +6,13 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.CreateOrUpdateDossierRequest;
import com.iqser.red.service.persistence.service.v1.api.shared.model.dossiertemplate.dossier.Dossier;
@ResponseStatus(value = HttpStatus.OK)
@ -33,4 +37,9 @@ public interface DossierResource {
@RequestParam(name = INCLUDE_ARCHIVED_PARAM, defaultValue = "false", required = false) boolean includeArchived,
@RequestParam(name = INCLUDE_DELETED_PARAM, defaultValue = "false", required = false) boolean includeDeleted);
@ResponseBody
@PostMapping(value = InternalApi.BASE_PATH + REST_PATH, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
Dossier createDossier(@RequestBody CreateOrUpdateDossierRequest dossierRequest);
}

View File

@ -1,5 +1,7 @@
package com.iqser.red.service.persistence.service.v1.api.internal.resources;
import java.util.List;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -29,4 +31,8 @@ public interface DossierTemplateResource {
@GetMapping(value = InternalApi.BASE_PATH + DOSSIER_TEMPLATE_PATH + DOSSIER_ID_PATH_PARAM, produces = MediaType.APPLICATION_JSON_VALUE)
DossierTemplate getDossierTemplateById(@PathVariable(DOSSIER_TEMPLATE_ID_PARAM) String dossierTemplateId);
@GetMapping(value = InternalApi.BASE_PATH + DOSSIER_TEMPLATE_PATH, produces = MediaType.APPLICATION_JSON_VALUE)
List<DossierTemplate> getAllDossierTemplates();
}

View File

@ -0,0 +1,33 @@
package com.iqser.red.service.persistence.service.v1.api.internal.resources;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileUploadResult;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
public interface UploadResource {
String DOSSIER_ID = "dossierId";
String FILE_ID = "fileId";
String UPLOAD_PATH = InternalApi.BASE_PATH + "/upload";
String DOSSIER_ID_PATH_VARIABLE = "/{" + DOSSIER_ID + "}";
String FILE_ID_PATH_VARIABLE = "/{" + FILE_ID + "}";
String DISABLE_AUTOMATIC_ANALYSIS_PARAM = "disableAutomaticAnalysis";
@ResponseBody
@PostMapping(value = UPLOAD_PATH + DOSSIER_ID_PATH_VARIABLE, consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
FileUploadResult upload(@Schema(type = "string", format = "binary", name = "file") @RequestPart(name = "file") MultipartFile file,
@PathVariable(DOSSIER_ID) String dossierId,
@RequestParam(value = "keepManualRedactions", required = false, defaultValue = "false") boolean keepManualRedactions,
@Parameter(name = DISABLE_AUTOMATIC_ANALYSIS_PARAM, description = "Disables automatic analysis for the uploaded file, imports only imported redactions") @RequestParam(value = DISABLE_AUTOMATIC_ANALYSIS_PARAM, required = false, defaultValue = "false") boolean disableAutomaticAnalysis);
}

View File

@ -1,5 +1,16 @@
package com.iqser.red.service.persistence.management.v1.processor.service;
import static com.iqser.red.service.persistence.management.v1.processor.exception.DossierNotFoundException.DOSSIER_NOT_FOUND_MESSAGE;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.stereotype.Service;
import com.iqser.red.service.persistence.management.v1.processor.entity.dossier.DossierEntity;
import com.iqser.red.service.persistence.management.v1.processor.exception.DossierNotFoundException;
import com.iqser.red.service.persistence.management.v1.processor.utils.DossierMapper;
@ -17,17 +28,6 @@ import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.OffsetDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static com.iqser.red.service.persistence.management.v1.processor.exception.DossierNotFoundException.DOSSIER_NOT_FOUND_MESSAGE;
@Slf4j
@Service
@RequiredArgsConstructor
@ -47,6 +47,13 @@ public class DossierManagementService {
}
@Transactional
public Dossier createDossier(CreateOrUpdateDossierRequest dossierRequest) {
return MagicConverter.convert(dossierService.addDossier(dossierRequest), Dossier.class, new DossierMapper());
}
@Transactional
public Dossier updateDossier(CreateOrUpdateDossierRequest dossierRequest, String dossierId) {
@ -65,8 +72,9 @@ public class DossierManagementService {
List<FileModel> fileStatuses = fileStatusService.getDossierStatus(dossierId);
var relevantFileIds = fileStatuses.stream()
.filter(fileStatus -> fileStatus.getDeleted() == null).map(FileModel::getId).toList();
.filter(fileStatus -> fileStatus.getDeleted() == null)
.map(FileModel::getId)
.toList();
dossierDeletionService.softDeleteDossier(dossierId, relevantFileIds, now);
fileDeletionService.reindexDeletedFiles(dossierId, relevantFileIds);
@ -120,12 +128,14 @@ public class DossierManagementService {
}
@Transactional
public List<String> getAllDossierIdsForDossierTemplateId(String dossierTemplateId, boolean includeArchived, boolean includeDeleted) {
return dossierService.getAllDossierIdsForDossierTemplateId(dossierTemplateId, includeArchived, includeDeleted);
}
@Transactional
public List<String> findAllDossierIdsInDossierTemplateId(String dossierTemplateId, Set<String> dossierIds) {
@ -215,7 +225,10 @@ public class DossierManagementService {
for (String dossierId : dossierIds) {
List<String> fileIds = fileStatusService.getDossierStatus(dossierId).stream().map(FileModel::getId).collect(Collectors.toList());
List<String> fileIds = fileStatusService.getDossierStatus(dossierId)
.stream()
.map(FileModel::getId)
.collect(Collectors.toList());
dossierDeletionService.hardDeleteDossier(dossierId, fileIds);
dossierDeletionService.hardDeleteFileDataAndIndexUpdates(dossierId, fileIds);
}
@ -228,10 +241,13 @@ public class DossierManagementService {
for (String dossierId : dossierIds) {
var dossier = dossierService.getDossierById(dossierId);
List<FileModel> fileStatuses = fileStatusService.getDossierStatus(dossierId);
var relevantFileIds = fileStatuses.stream().filter(fileStatus -> fileStatus.getDeleted() != null && (fileStatus.getDeleted().equals(dossier.getSoftDeletedTime()) || fileStatus.getDeleted()
.isAfter(dossier.getSoftDeletedTime()))).map(FileModel::getId).collect(Collectors.toList());
var relevantFileIds = fileStatuses.stream()
.filter(fileStatus -> fileStatus.getDeleted() != null && (fileStatus.getDeleted().equals(dossier.getSoftDeletedTime()) || fileStatus.getDeleted()
.isAfter(dossier.getSoftDeletedTime())))
.map(FileModel::getId)
.collect(Collectors.toList());
dossierDeletionService.undeleteDossier(dossierId,relevantFileIds,dossier.getSoftDeletedTime());
dossierDeletionService.undeleteDossier(dossierId, relevantFileIds, dossier.getSoftDeletedTime());
dossierDeletionService.reindexUndeletedFiles(dossier.getDossierTemplateId(), dossierId, relevantFileIds);
}
@ -278,6 +294,7 @@ public class DossierManagementService {
public DossierChangeResponseV2 changesSinceV2(JSONPrimitive<OffsetDateTime> since) {
return dossierService.changesSinceV2(since.getValue());
}
@ -285,7 +302,7 @@ public class DossierManagementService {
@Transactional
public List<Dossier> getDossiersByIds(Set<String> viewableDossierIds) {
return getConvertedAllDossiers(dossierService.getAllDossiers(viewableDossierIds), true,true);
return getConvertedAllDossiers(dossierService.getAllDossiers(viewableDossierIds), true, true);
}
}

View File

@ -59,6 +59,8 @@ public class DossierService {
validateDossierName(createOrUpdateDossierRequest);
try {
System.out.println("PSPS: " + createOrUpdateDossierRequest);
log.info("PSPS: " + createOrUpdateDossierRequest);
return dossierPersistenceService.insert(createOrUpdateDossierRequest);
} catch (Exception e) {
if (e.getCause() instanceof ConstraintViolationException) {

View File

@ -48,6 +48,7 @@ public class FileService {
public JSONPrimitive<String> upload(AddFileRequest request, boolean keepManualRedactions, long size, boolean disableAutomaticAnalysis) {
log.info("MMMM: in file upload");
dossierPersistenceService.getAndValidateDossier(request.getDossierId());
var existingStatus = retrieveStatus(request.getFileId());

View File

@ -260,6 +260,7 @@ public class FileStatusService {
protected void addToAnalysisQueue(String dossierId, String fileId, boolean priority, Set<Integer> sectionsToReanalyse, AnalysisType analysisType) {
log.info("OOOO: in analysis queue");
var dossier = dossierPersistenceService.getAndValidateDossier(dossierId);
var fileEntity = fileStatusPersistenceService.getStatus(fileId);
@ -888,8 +889,11 @@ public class FileStatusService {
@Transactional
public void createStatus(String dossierId, String fileId, String uploader, String filename, long size, boolean disableAutomaticAnalysis) {
log.info("NNNN: Creating status for {}", fileId);
fileStatusPersistenceService.createStatus(dossierId, fileId, filename, uploader, size, disableAutomaticAnalysis);
log.info("NNNN: Websocket");
websocketService.sendFileEvent(dossierId, fileId, FileEventType.CREATE);
log.info("NNNN: AnalysisQueue");
addToAnalysisQueue(dossierId, fileId, false, Set.of(), AnalysisType.DEFAULT);
}

View File

@ -0,0 +1,253 @@
package com.iqser.red.service.persistence.management.v1.processor.service;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.UUID;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException;
import com.iqser.red.service.persistence.management.v1.processor.exception.NotAllowedException;
import com.iqser.red.service.persistence.management.v1.processor.utils.FileUtils;
import com.iqser.red.service.persistence.service.v1.api.shared.model.FileUploadResult;
import com.knecon.fforesight.tenantcommons.TenantContext;
import io.micrometer.core.annotation.Timed;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class UploadManagementService {
private static final int THRESHOLD_ENTRIES = 10000; // Maximum number of files allowed
private static final int THRESHOLD_SIZE = 1000000000; // 1 GB total unzipped data
private static final double THRESHOLD_RATIO = 10; // Max allowed compression ratio
private final UploadService uploadService;
private final FileFormatValidationService fileFormatValidationService;
@Timed
public FileUploadResult upload(MultipartFile file, String dossierId, boolean keepManualRedactions, boolean disableAutomaticAnalysis) {
String originalFilename = file.getOriginalFilename();
if (originalFilename == null) {
throw new BadRequestException("Could not upload file, no filename provided.");
}
String extension = getExtension(originalFilename);
try {
return switch (extension) {
case "zip" -> handleZip(dossierId, file.getBytes(), keepManualRedactions, disableAutomaticAnalysis);
case "csv" -> uploadService.importCsv(dossierId, file.getBytes());
default -> {
validateExtensionOrThrow(extension);
yield uploadService.processSingleFile(dossierId, originalFilename, file.getBytes(), keepManualRedactions, disableAutomaticAnalysis);
}
};
} catch (IOException e) {
throw new BadRequestException("Failed to process file: " + e.getMessage(), e);
}
}
private void validateExtensionOrThrow(String extension) {
if (!fileFormatValidationService.getAllFileFormats().contains(extension)) {
throw new BadRequestException("Invalid file uploaded (unrecognized extension).");
}
if (!fileFormatValidationService.getValidFileFormatsForTenant(TenantContext.getTenantId()).contains(extension)) {
throw new NotAllowedException("Insufficient permissions for this file type.");
}
}
/**
* 1. Write the uploaded content to a temp ZIP file
* 2. Check the number of entries and reject if too big or if symlinks found
* 3. Unzip and process each file, while checking size and ratio.
*/
private FileUploadResult handleZip(String dossierId, byte[] fileContent, boolean keepManualRedactions, boolean disableAutomaticAnalysis) throws IOException {
File tempZip = FileUtils.createTempFile(UUID.randomUUID().toString(), ".zip");
try (FileOutputStream fos = new FileOutputStream(tempZip)) {
IOUtils.write(fileContent, fos);
}
validateZipEntries(tempZip);
try {
ZipData zipData = processZipContents(tempZip, dossierId, keepManualRedactions, disableAutomaticAnalysis);
if (zipData.csvBytes != null) {
try {
FileUploadResult csvResult = uploadService.importCsv(dossierId, zipData.csvBytes);
zipData.fileUploadResult.getProcessedAttributes().addAll(csvResult.getProcessedAttributes());
zipData.fileUploadResult.getProcessedFileIds().addAll(csvResult.getProcessedFileIds());
} catch (Exception e) {
log.debug("CSV file inside ZIP failed to import", e);
}
} else if (zipData.fileUploadResult.getFileIds().isEmpty()) {
if (zipData.containedUnpermittedFiles) {
throw new NotAllowedException("Zip file contains unpermitted files.");
} else {
throw new BadRequestException("Only unsupported files in the ZIP.");
}
}
return zipData.fileUploadResult;
} finally {
if (!tempZip.delete()) {
log.warn("Could not delete temporary ZIP file: {}", tempZip);
}
}
}
private void validateZipEntries(File tempZip) throws IOException {
try (FileInputStream fis = new FileInputStream(tempZip); ZipFile zipFile = new ZipFile(fis.getChannel())) {
int count = 0;
var entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry ze = entries.nextElement();
if (ze.isUnixSymlink()) {
throw new BadRequestException("ZIP-files with symlinks are not allowed.");
}
if (!ze.isDirectory() && !ze.getName().startsWith(".")) {
count++;
if (count > THRESHOLD_ENTRIES) {
throw new BadRequestException("ZIP-Bomb detected: too many entries.");
}
}
}
}
}
private ZipData processZipContents(File tempZip, String dossierId, boolean keepManualRedactions, boolean disableAutomaticAnalysis) throws IOException {
ZipData zipData = new ZipData();
try (FileInputStream fis = new FileInputStream(tempZip); ZipFile zipFile = new ZipFile(fis.getChannel())) {
var entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
ZipArchiveEntry entry = entries.nextElement();
if (entry.isDirectory() || entry.getName().startsWith(".")) {
continue;
}
byte[] entryBytes = readEntryWithRatioCheck(entry, zipFile);
zipData.totalSizeArchive += entryBytes.length;
if (zipData.totalSizeArchive > THRESHOLD_SIZE) {
throw new BadRequestException("ZIP-Bomb detected (exceeds total size limit).");
}
String extension = getExtension(entry.getName());
if ("csv".equalsIgnoreCase(extension)) {
zipData.csvBytes = entryBytes;
} else {
handleRegularFile(dossierId, entryBytes, extension, extractFileName(entry.getName()), zipData, keepManualRedactions, disableAutomaticAnalysis);
}
}
}
return zipData;
}
private byte[] readEntryWithRatioCheck(ZipArchiveEntry entry, ZipFile zipFile) throws IOException {
long compressedSize = entry.getCompressedSize() > 0 ? entry.getCompressedSize() : 1;
try (var is = zipFile.getInputStream(entry); var bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int bytesRead;
int totalUncompressed = 0;
while ((bytesRead = is.read(buffer)) != -1) {
bos.write(buffer, 0, bytesRead);
totalUncompressed += bytesRead;
double ratio = (double) totalUncompressed / compressedSize;
if (ratio > THRESHOLD_RATIO) {
throw new BadRequestException("ZIP-Bomb detected (compression ratio too high).");
}
}
return bos.toByteArray();
}
}
private void handleRegularFile(String dossierId,
byte[] fileBytes,
String extension,
String fileName,
ZipData zipData,
boolean keepManualRedactions,
boolean disableAutomaticAnalysis) {
if (!fileFormatValidationService.getAllFileFormats().contains(extension)) {
zipData.containedUnpermittedFiles = false;
return;
}
if (!fileFormatValidationService.getValidFileFormatsForTenant(TenantContext.getTenantId()).contains(extension)) {
zipData.containedUnpermittedFiles = true;
return;
}
try {
FileUploadResult result = uploadService.processSingleFile(dossierId, fileName, fileBytes, keepManualRedactions, disableAutomaticAnalysis);
zipData.fileUploadResult.getFileIds().addAll(result.getFileIds());
} catch (Exception e) {
log.debug("Failed to process file '{}' in ZIP: {}", fileName, e.getMessage(), e);
}
}
private String extractFileName(String path) {
int idx = path.lastIndexOf('/');
return (idx >= 0) ? path.substring(idx + 1) : path;
}
private String getExtension(String fileName) {
int idx = fileName.lastIndexOf('.');
if (idx < 0) {
return "";
}
return fileName.substring(idx + 1).toLowerCase(Locale.ROOT);
}
@FieldDefaults(level = AccessLevel.PUBLIC)
private static final class ZipData {
byte[] csvBytes;
int totalSizeArchive;
FileUploadResult fileUploadResult = new FileUploadResult();
boolean containedUnpermittedFiles;
}
}

View File

@ -74,8 +74,9 @@ public class UploadService {
var storageId = StorageIdUtils.getStorageId(dossierId, fileId, FileType.UNTOUCHED);
try {
log.info("MMMM: Will store object with id {} into storage {}", fileId, storageId);
storageService.storeObject(TenantContext.getTenantId(), storageId, new ByteArrayInputStream(fileContent));
log.info("MMMM: Uploading file");
fileService.upload(new AddFileRequest(fileName, fileId, dossierId, KeycloakSecurity.getUserId()), keepManualRedactions, fileContent.length, disableAutomaticAnalysis);
} catch (Exception e) {