Fix possible NPE when uploading Documents & Images (FINERACT-1036)
This commit is contained in:
committed by
Petri Tuomola
parent
cb3260b61f
commit
6d22549373
@@ -0,0 +1,32 @@
|
|||||||
|
# Validation
|
||||||
|
|
||||||
|
## Programmatic
|
||||||
|
|
||||||
|
Use the [DataValidatorBuilder](https://github.com/apache/fineract/search?q=%22class+DataValidatorBuilder%22), e.g. like so:
|
||||||
|
|
||||||
|
```java
|
||||||
|
new DataValidatorBuilder().resource("fileUpload")
|
||||||
|
.reset().parameter("Content-Length").value(contentLength).notBlank().integerGreaterThanNumber(0)
|
||||||
|
.reset().parameter("FormDataContentDisposition").value(fileDetails).notNull()
|
||||||
|
.throwValidationErrors();
|
||||||
|
```
|
||||||
|
|
||||||
|
Such code is often encapsulated in [`*Validator`](https://github.com/apache/fineract/search?q=Validator)
|
||||||
|
classes (if more than a few lines, and/or reused from several places; avoid copy/paste), like so:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class YourThingValidator {
|
||||||
|
|
||||||
|
public void validate(YourThing thing) {
|
||||||
|
new DataValidatorBuilder().resource("yourThing")
|
||||||
|
...
|
||||||
|
.throwValidationErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Declarative
|
||||||
|
|
||||||
|
[FINERACT-1229](https://issues.apache.org/jira/browse/FINERACT-1229) is an open issue about adopting
|
||||||
|
Bean Validation for _declarative_ instead of _programmatic_ (as above) validation. Contributions welcome!
|
||||||
+27
@@ -22,6 +22,7 @@ import com.google.common.base.Splitter;
|
|||||||
import com.google.gson.JsonArray;
|
import com.google.gson.JsonArray;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
@@ -30,6 +31,7 @@ import java.util.stream.Collectors;
|
|||||||
import net.fortuna.ical4j.model.property.RRule;
|
import net.fortuna.ical4j.model.property.RRule;
|
||||||
import net.fortuna.ical4j.validate.ValidationException;
|
import net.fortuna.ical4j.validate.ValidationException;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
import org.joda.time.LocalDate;
|
import org.joda.time.LocalDate;
|
||||||
import org.quartz.CronExpression;
|
import org.quartz.CronExpression;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
@@ -45,6 +47,20 @@ public class DataValidatorBuilder {
|
|||||||
private Object value;
|
private Object value;
|
||||||
private boolean ignoreNullValue = false;
|
private boolean ignoreNullValue = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default constructor used to start a new "validation chain".
|
||||||
|
*/
|
||||||
|
public DataValidatorBuilder() {
|
||||||
|
this(new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor used to "continue" an existing "validation chain".
|
||||||
|
*
|
||||||
|
* @param dataValidationErrors
|
||||||
|
* an existing list of {@link ApiParameterError} to add new validation errors to
|
||||||
|
*/
|
||||||
|
|
||||||
public DataValidatorBuilder(final List<ApiParameterError> dataValidationErrors) {
|
public DataValidatorBuilder(final List<ApiParameterError> dataValidationErrors) {
|
||||||
this.dataValidationErrors = dataValidationErrors;
|
this.dataValidationErrors = dataValidationErrors;
|
||||||
}
|
}
|
||||||
@@ -1103,4 +1119,15 @@ public class DataValidatorBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throws Exception if validation errors.
|
||||||
|
*
|
||||||
|
* @throws PlatformApiDataValidationException
|
||||||
|
* unchecked exception (RuntimeException) thrown if there are any validation error
|
||||||
|
*/
|
||||||
|
public void throwValidationErrors() throws PlatformApiDataValidationException {
|
||||||
|
if (!dataValidationErrors.isEmpty()) {
|
||||||
|
throw new PlatformApiDataValidationException(dataValidationErrors);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -20,6 +20,7 @@ package org.apache.fineract.infrastructure.core.exception;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception thrown when problem with an API request to the platform.
|
* Exception thrown when problem with an API request to the platform.
|
||||||
@@ -28,6 +29,12 @@ public class PlatformApiDataValidationException extends AbstractPlatformExceptio
|
|||||||
|
|
||||||
private final List<ApiParameterError> errors;
|
private final List<ApiParameterError> errors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor. Consider simply using {@link DataValidatorBuilder#throwValidationErrors()} directly.
|
||||||
|
*
|
||||||
|
* @param errors
|
||||||
|
* list of {@link ApiParameterError} to throw
|
||||||
|
*/
|
||||||
public PlatformApiDataValidationException(List<ApiParameterError> errors) {
|
public PlatformApiDataValidationException(List<ApiParameterError> errors) {
|
||||||
super("validation.msg.validation.errors.exist", "Validation errors exist.");
|
super("validation.msg.validation.errors.exist", "Validation errors exist.");
|
||||||
this.errors = errors;
|
this.errors = errors;
|
||||||
|
|||||||
+11
-10
@@ -62,9 +62,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("{entityType}/{entityId}/documents")
|
|
||||||
@Component
|
@Component
|
||||||
@Scope("singleton")
|
@Scope("singleton")
|
||||||
|
@Path("{entityType}/{entityId}/documents")
|
||||||
@Tag(name = "Documents", description = "Multiple Documents (a combination of a name, description and a file) may be attached to different Entities like Clients, Groups, Staff, Loans, Savings and Client Identifiers in the system\n"
|
@Tag(name = "Documents", description = "Multiple Documents (a combination of a name, description and a file) may be attached to different Entities like Clients, Groups, Staff, Loans, Savings and Client Identifiers in the system\n"
|
||||||
+ "\n" + "Note: The currently allowed Entities are\n" + "\n" + "Clients: URL Pattern as clients\n" + "Staff: URL Pattern as staff\n"
|
+ "\n" + "Note: The currently allowed Entities are\n" + "\n" + "Clients: URL Pattern as clients\n" + "Staff: URL Pattern as staff\n"
|
||||||
+ "Loans: URL Pattern as loans\n" + "Savings: URL Pattern as savings\n" + "Client Identifiers: URL Pattern as client_identifiers\n"
|
+ "Loans: URL Pattern as loans\n" + "Savings: URL Pattern as savings\n" + "Client Identifiers: URL Pattern as client_identifiers\n"
|
||||||
@@ -81,16 +81,19 @@ public class DocumentManagementApiResource {
|
|||||||
private final DocumentWritePlatformService documentWritePlatformService;
|
private final DocumentWritePlatformService documentWritePlatformService;
|
||||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
private final ToApiJsonSerializer<DocumentData> toApiJsonSerializer;
|
private final ToApiJsonSerializer<DocumentData> toApiJsonSerializer;
|
||||||
|
private final FileUploadValidator fileUploadValidator;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public DocumentManagementApiResource(final PlatformSecurityContext context,
|
public DocumentManagementApiResource(final PlatformSecurityContext context,
|
||||||
final DocumentReadPlatformService documentReadPlatformService, final DocumentWritePlatformService documentWritePlatformService,
|
final DocumentReadPlatformService documentReadPlatformService, final DocumentWritePlatformService documentWritePlatformService,
|
||||||
final ApiRequestParameterHelper apiRequestParameterHelper, final ToApiJsonSerializer<DocumentData> toApiJsonSerializer) {
|
final ApiRequestParameterHelper apiRequestParameterHelper, final ToApiJsonSerializer<DocumentData> toApiJsonSerializer,
|
||||||
|
final FileUploadValidator fileUploadValidator) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.documentReadPlatformService = documentReadPlatformService;
|
this.documentReadPlatformService = documentReadPlatformService;
|
||||||
this.documentWritePlatformService = documentWritePlatformService;
|
this.documentWritePlatformService = documentWritePlatformService;
|
||||||
this.apiRequestParameterHelper = apiRequestParameterHelper;
|
this.apiRequestParameterHelper = apiRequestParameterHelper;
|
||||||
this.toApiJsonSerializer = toApiJsonSerializer;
|
this.toApiJsonSerializer = toApiJsonSerializer;
|
||||||
|
this.fileUploadValidator = fileUploadValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@@ -125,19 +128,16 @@ public class DocumentManagementApiResource {
|
|||||||
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
|
@PathParam("entityId") @Parameter(description = "entityId") final Long entityId,
|
||||||
@HeaderParam("Content-Length") @Parameter(description = "Content-Length") final Long fileSize,
|
@HeaderParam("Content-Length") @Parameter(description = "Content-Length") final Long fileSize,
|
||||||
@FormDataParam("file") @Parameter(description = "file") final InputStream inputStream,
|
@FormDataParam("file") @Parameter(description = "file") final InputStream inputStream,
|
||||||
@FormDataParam("file") final @Parameter(description = "file") FormDataContentDisposition fileDetails,
|
@FormDataParam("file") @Parameter(description = "file") final FormDataContentDisposition fileDetails,
|
||||||
@FormDataParam("file") @Parameter(description = "file") final FormDataBodyPart bodyPart,
|
@FormDataParam("file") @Parameter(description = "file") final FormDataBodyPart bodyPart,
|
||||||
@FormDataParam("name") @Parameter(description = "name") final String name,
|
@FormDataParam("name") @Parameter(description = "name") final String name,
|
||||||
@FormDataParam("description") @Parameter(description = "description") final String description) {
|
@FormDataParam("description") @Parameter(description = "description") final String description) {
|
||||||
|
|
||||||
/**
|
// TODO: stop reading from stream after max size is reached to protect against malicious clients
|
||||||
* TODO: also need to have a backup and stop reading from stream after max size is reached to protect against
|
// TODO: need to extract the actual file type and determine if they are permissible
|
||||||
* malicious clients
|
|
||||||
**/
|
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: need to extract the actual file type and determine if they are permissable
|
|
||||||
**/
|
|
||||||
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileDetails.getFileName(),
|
final DocumentCommand documentCommand = new DocumentCommand(null, null, entityType, entityId, name, fileDetails.getFileName(),
|
||||||
fileSize, bodyPart.getMediaType().toString(), description, null);
|
fileSize, bodyPart.getMediaType().toString(), description, null);
|
||||||
|
|
||||||
@@ -174,6 +174,7 @@ public class DocumentManagementApiResource {
|
|||||||
***/
|
***/
|
||||||
DocumentCommand documentCommand = null;
|
DocumentCommand documentCommand = null;
|
||||||
if (inputStream != null && fileDetails.getFileName() != null) {
|
if (inputStream != null && fileDetails.getFileName() != null) {
|
||||||
|
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
|
||||||
modifiedParams.add("fileName");
|
modifiedParams.add("fileName");
|
||||||
modifiedParams.add("size");
|
modifiedParams.add("size");
|
||||||
modifiedParams.add("type");
|
modifiedParams.add("type");
|
||||||
|
|||||||
+41
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
package org.apache.fineract.infrastructure.documentmanagement.api;
|
||||||
|
|
||||||
|
import com.sun.jersey.core.header.FormDataContentDisposition;
|
||||||
|
import com.sun.jersey.multipart.FormDataBodyPart;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validator for uploaded files.
|
||||||
|
*
|
||||||
|
* @author Michael Vorburger.ch
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class FileUploadValidator {
|
||||||
|
|
||||||
|
public void validate(Long contentLength, InputStream inputStream, FormDataContentDisposition fileDetails, FormDataBodyPart bodyPart) {
|
||||||
|
new DataValidatorBuilder().resource("fileUpload").reset().parameter("Content-Length").value(contentLength).notBlank()
|
||||||
|
.integerGreaterThanNumber(0).reset().parameter("InputStream").value(inputStream).notNull().reset()
|
||||||
|
.parameter("FormDataContentDisposition").value(fileDetails).notNull().reset().parameter("FormDataBodyPart").value(bodyPart)
|
||||||
|
.notNull().throwValidationErrors();
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-5
@@ -52,24 +52,26 @@ import org.springframework.beans.factory.annotation.Autowired;
|
|||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Path("{entity}/{entityId}/images")
|
|
||||||
@Component
|
@Component
|
||||||
@Scope("singleton")
|
@Scope("singleton")
|
||||||
|
@Path("{entity}/{entityId}/images")
|
||||||
public class ImagesApiResource {
|
public class ImagesApiResource {
|
||||||
|
|
||||||
private final PlatformSecurityContext context;
|
private final PlatformSecurityContext context;
|
||||||
private final ImageReadPlatformService imageReadPlatformService;
|
private final ImageReadPlatformService imageReadPlatformService;
|
||||||
private final ImageWritePlatformService imageWritePlatformService;
|
private final ImageWritePlatformService imageWritePlatformService;
|
||||||
private final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer;
|
private final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer;
|
||||||
|
private final FileUploadValidator fileUploadValidator;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public ImagesApiResource(final PlatformSecurityContext context, final ImageReadPlatformService readPlatformService,
|
public ImagesApiResource(final PlatformSecurityContext context, final ImageReadPlatformService readPlatformService,
|
||||||
final ImageWritePlatformService imageWritePlatformService, final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer) {
|
final ImageWritePlatformService imageWritePlatformService, final DefaultToApiJsonSerializer<ClientData> toApiJsonSerializer,
|
||||||
|
final FileUploadValidator fileUploadValidator) {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.imageReadPlatformService = readPlatformService;
|
this.imageReadPlatformService = readPlatformService;
|
||||||
this.imageWritePlatformService = imageWritePlatformService;
|
this.imageWritePlatformService = imageWritePlatformService;
|
||||||
this.toApiJsonSerializer = toApiJsonSerializer;
|
this.toApiJsonSerializer = toApiJsonSerializer;
|
||||||
|
this.fileUploadValidator = fileUploadValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -82,6 +84,7 @@ public class ImagesApiResource {
|
|||||||
@HeaderParam("Content-Length") final Long fileSize, @FormDataParam("file") final InputStream inputStream,
|
@HeaderParam("Content-Length") final Long fileSize, @FormDataParam("file") final InputStream inputStream,
|
||||||
@FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart) {
|
@FormDataParam("file") final FormDataContentDisposition fileDetails, @FormDataParam("file") final FormDataBodyPart bodyPart) {
|
||||||
validateEntityTypeforImage(entityName);
|
validateEntityTypeforImage(entityName);
|
||||||
|
fileUploadValidator.validate(fileSize, inputStream, fileDetails, bodyPart);
|
||||||
// TODO: vishwas might need more advances validation (like reading magic
|
// TODO: vishwas might need more advances validation (like reading magic
|
||||||
// number) for handling malicious clients
|
// number) for handling malicious clients
|
||||||
// and clients not setting mime type
|
// and clients not setting mime type
|
||||||
@@ -103,6 +106,7 @@ public class ImagesApiResource {
|
|||||||
public String addNewClientImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId,
|
public String addNewClientImage(@PathParam("entity") final String entityName, @PathParam("entityId") final Long entityId,
|
||||||
final String jsonRequestBody) {
|
final String jsonRequestBody) {
|
||||||
validateEntityTypeforImage(entityName);
|
validateEntityTypeforImage(entityName);
|
||||||
|
|
||||||
final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
|
final Base64EncodedImage base64EncodedImage = ContentRepositoryUtils.extractImageFromDataURL(jsonRequestBody);
|
||||||
|
|
||||||
final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateImage(entityName, entityId, base64EncodedImage);
|
final CommandProcessingResult result = this.imageWritePlatformService.saveOrUpdateImage(entityName, entityId, base64EncodedImage);
|
||||||
@@ -140,7 +144,7 @@ public class ImagesApiResource {
|
|||||||
imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.PNG.getValue();
|
imageDataURISuffix = ContentRepositoryUtils.ImageDataURIsuffix.PNG.getValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
byte[] resizedImage = imageData.getContentOfSize(maxWidth, maxHeight);
|
final byte[] resizedImage = imageData.getContentOfSize(maxWidth, maxHeight);
|
||||||
final String clientImageAsBase64Text = imageDataURISuffix + Base64.getMimeEncoder().encodeToString(resizedImage);
|
final String clientImageAsBase64Text = imageDataURISuffix + Base64.getMimeEncoder().encodeToString(resizedImage);
|
||||||
return Response.ok(clientImageAsBase64Text).build();
|
return Response.ok(clientImageAsBase64Text).build();
|
||||||
}
|
}
|
||||||
@@ -161,7 +165,7 @@ public class ImagesApiResource {
|
|||||||
final ImageData imageData = this.imageReadPlatformService.retrieveImage(entityName, entityId);
|
final ImageData imageData = this.imageReadPlatformService.retrieveImage(entityName, entityId);
|
||||||
|
|
||||||
final ResponseBuilder response = Response.ok(imageData.getContentOfSize(maxWidth, maxHeight));
|
final ResponseBuilder response = Response.ok(imageData.getContentOfSize(maxWidth, maxHeight));
|
||||||
String dispositionType = "inline_octet".equals(output) ? "inline" : "attachment";
|
final String dispositionType = "inline_octet".equals(output) ? "inline" : "attachment";
|
||||||
response.header("Content-Disposition",
|
response.header("Content-Disposition",
|
||||||
dispositionType + "; filename=\"" + imageData.getEntityDisplayName() + ImageFileExtension.JPEG + "\"");
|
dispositionType + "; filename=\"" + imageData.getEntityDisplayName() + ImageFileExtension.JPEG + "\"");
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user