FINERACT-1747 add datatable query api
This commit is contained in:
committed by
Arnold Gálovics
parent
f26b7bc576
commit
ad2f2862cb
+20
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.dataqueries.api;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
@@ -218,6 +219,25 @@ public class DatatablesApiResource {
|
||||
return this.toApiJsonSerializer.serializePretty(prettyPrint, result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{datatable}/query")
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
@Operation(summary = "Query Data Table values", description = "Query values from a registered data table.")
|
||||
@ApiResponses({
|
||||
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = List.class))) })
|
||||
public String queryValues(@PathParam("datatable") @Parameter(description = "datatable") final String datatable,
|
||||
@QueryParam("columnFilter") @Parameter(description = "columnFilter") final String columnFilter,
|
||||
@QueryParam("valueFilter") @Parameter(description = "valueFilter") final String valueFilter,
|
||||
@QueryParam("resultColumns") @Parameter(description = "resultColumns") final String resultColumns,
|
||||
@Context final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasDatatableReadPermission(datatable);
|
||||
|
||||
final List<JsonObject> result = this.readWriteNonCoreDataService.queryDataTable(datatable, columnFilter, valueFilter,
|
||||
resultColumns);
|
||||
|
||||
return this.toApiJsonSerializer.serializePretty(ApiParameterHelper.prettyPrint(uriInfo.getQueryParameters()), result);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("{datatable}/{apptableId}")
|
||||
@Consumes({ MediaType.APPLICATION_JSON })
|
||||
|
||||
+5
-1
@@ -87,7 +87,7 @@ public final class ResultsetColumnHeaderData implements Serializable {
|
||||
displayType = "DECIMAL";
|
||||
} else if (isAnyText()) {
|
||||
displayType = "TEXT";
|
||||
} else if (isBit()) {
|
||||
} else if (isBit() || isBoolean()) {
|
||||
displayType = "BOOLEAN";
|
||||
} else {
|
||||
throw new PlatformDataIntegrityException("error.msg.invalid.lookup.type",
|
||||
@@ -231,6 +231,10 @@ public final class ResultsetColumnHeaderData implements Serializable {
|
||||
return "bit".equalsIgnoreCase(this.columnType);
|
||||
}
|
||||
|
||||
private boolean isBoolean() {
|
||||
return "boolean".equalsIgnoreCase(this.columnType) || "bool".equalsIgnoreCase(this.columnType);
|
||||
}
|
||||
|
||||
public String getColumnName() {
|
||||
return this.columnName;
|
||||
}
|
||||
|
||||
+3
@@ -18,6 +18,7 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.dataqueries.service;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
@@ -31,6 +32,8 @@ public interface ReadWriteNonCoreDataService {
|
||||
|
||||
DatatableData retrieveDatatable(String datatable);
|
||||
|
||||
List<JsonObject> queryDataTable(String datatable, String columnFilter, String valueFilter, String resultColumns);
|
||||
|
||||
@PreAuthorize(value = "hasAnyAuthority('ALL_FUNCTIONS', 'REGISTER_DATATABLE')")
|
||||
void registerDatatable(JsonCommand command);
|
||||
|
||||
|
||||
+185
-4
@@ -18,6 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.dataqueries.service;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.apache.fineract.infrastructure.core.data.ApiParameterError.parameterErrorWithValue;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.JsonArray;
|
||||
@@ -30,12 +33,18 @@ import java.sql.Date;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Timestamp;
|
||||
import java.sql.Types;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import javax.persistence.PersistenceException;
|
||||
@@ -74,6 +83,7 @@ import org.apache.fineract.infrastructure.security.service.SqlInjectionPreventer
|
||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
@@ -104,11 +114,24 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
||||
.put("number", "INT").put("boolean", "BIT").put("decimal", "DECIMAL").put("date", "DATE").put("datetime", "DATETIME")
|
||||
.put("text", "TEXT").put("dropdown", "INT").build();
|
||||
private static final ImmutableMap<String, String> apiTypeToPostgreSQL = ImmutableMap.<String, String>builder().put("string", "VARCHAR")
|
||||
.put("number", "INT").put("boolean", "BIT").put("decimal", "DECIMAL").put("date", "DATE").put("datetime", "TIMESTAMP")
|
||||
.put("number", "INT").put("boolean", "boolean").put("decimal", "DECIMAL").put("date", "DATE").put("datetime", "TIMESTAMP")
|
||||
.put("text", "TEXT").put("dropdown", "INT").build();
|
||||
|
||||
private static final List<String> stringDataTypes = Arrays.asList("char", "varchar", "blob", "text", "tinyblob", "tinytext",
|
||||
"mediumblob", "mediumtext", "longblob", "longtext");
|
||||
private static final DateTimeFormatter DATA_TABLE_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
private static final DateTimeFormatter DATA_TABLE_DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
public static final String PG_BOOLEAN_TYPE = "boolean";
|
||||
public static final String PG_BOOL_TYPE = "bool";
|
||||
public static final String PG_INT_TYPE = "integer";
|
||||
public static final String PG_BIGINT_TYPE = "bigint";
|
||||
public static final String PG_DATE_TYPE = "date";
|
||||
public static final String PG_NUMERIC_TYPE = "numeric";
|
||||
public static final String PG_TEXT_TYPE = "text";
|
||||
public static final String PG_VARCHAR_TYPE = "character varying";
|
||||
public static final String BIT_TYPE = "bit";
|
||||
public static final String MYSQL_DATETIME_TYPE = "datetime";
|
||||
public static final String MYSQL_DATE_TYPE = "date";
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
private final DatabaseTypeResolver databaseTypeResolver;
|
||||
@@ -160,9 +183,9 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
||||
|
||||
// PERMITTED datatables
|
||||
SQLInjectionValidator.validateSQLInput(datatable);
|
||||
final String sql = "select application_table_name, registered_table_name, entity_subtype" + " from x_registered_table "
|
||||
+ " where exists" + " (select 'f'" + " from m_appuser_role ur " + " join m_role r on r.id = ur.role_id"
|
||||
+ " left join m_role_permission rp on rp.role_id = r.id" + " left join m_permission p on p.id = rp.permission_id"
|
||||
final String sql = "select application_table_name, registered_table_name, entity_subtype from x_registered_table "
|
||||
+ " where exists (select 'f' from m_appuser_role ur join m_role r on r.id = ur.role_id"
|
||||
+ " left join m_role_permission rp on rp.role_id = r.id left join m_permission p on p.id = rp.permission_id"
|
||||
+ " where ur.appuser_id = ? and registered_table_name=? and (p.code in ('ALL_FUNCTIONS', "
|
||||
+ "'ALL_FUNCTIONS_READ') or p.code = concat('READ_', registered_table_name))) "
|
||||
+ " order by application_table_name, registered_table_name";
|
||||
@@ -183,6 +206,164 @@ public class ReadWriteNonCoreDataServiceImpl implements ReadWriteNonCoreDataServ
|
||||
return datatableData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<JsonObject> queryDataTable(String datatable, String columnFilter, String valueFilter, String resultColumns) {
|
||||
Arrays.asList(datatable, columnFilter, valueFilter, resultColumns).forEach(SQLInjectionValidator::validateDynamicQuery);
|
||||
|
||||
List<ResultsetColumnHeaderData> resultsetColumnHeaderData = genericDataService.fillResultsetColumnHeaders(datatable);
|
||||
validateRequestParams(columnFilter, valueFilter, resultColumns, resultsetColumnHeaderData);
|
||||
|
||||
String sql = "select " + resultColumns + " from " + datatable + " where " + columnFilter + " = ?";
|
||||
SqlRowSet rowSet = null;
|
||||
String filterColumnType = resultsetColumnHeaderData.stream().filter(column -> Objects.equals(columnFilter, column.getColumnName()))
|
||||
.findFirst().map(ResultsetColumnHeaderData::getColumnType).orElse(columnFilter + " does not exist in datatable");
|
||||
if (databaseTypeResolver.isPostgreSQL()) {
|
||||
rowSet = callFilteredPgSql(sql, valueFilter, filterColumnType);
|
||||
} else if (databaseTypeResolver.isMySQL()) {
|
||||
rowSet = callFilteredMysql(sql, valueFilter, filterColumnType);
|
||||
}
|
||||
|
||||
String[] resultColumnNames = resultColumns.split(",");
|
||||
List<JsonObject> results = new ArrayList<>();
|
||||
while (rowSet.next()) {
|
||||
extractResults(rowSet, resultColumnNames, results);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void extractResults(SqlRowSet rowSet, String[] resultColumnNames, List<JsonObject> results) {
|
||||
JsonObject json = new JsonObject();
|
||||
for (String rcn : resultColumnNames) {
|
||||
Object rowValue = rowSet.getObject(rcn);
|
||||
if (rowValue != null) {
|
||||
if (rowValue instanceof Character) {
|
||||
json.addProperty(rcn, (Character) rowValue);
|
||||
} else if (rowValue instanceof Number) {
|
||||
json.addProperty(rcn, new BigDecimal(rowValue.toString()));
|
||||
} else if (rowValue instanceof Boolean) {
|
||||
json.addProperty(rcn, (Boolean) rowValue);
|
||||
} else if (rowValue instanceof LocalDateTime) {
|
||||
json.addProperty(rcn, DATA_TABLE_DATETIME_FORMAT.format((LocalDateTime) rowValue));
|
||||
} else if (rowValue instanceof Timestamp) {
|
||||
json.addProperty(rcn, DATA_TABLE_DATETIME_FORMAT.format(((Timestamp) rowValue).toLocalDateTime()));
|
||||
} else if (rowValue instanceof LocalDate) {
|
||||
json.addProperty(rcn, DATA_TABLE_DATE_FORMAT.format((LocalDate) rowValue));
|
||||
} else if (rowValue instanceof Date) {
|
||||
json.addProperty(rcn, DATA_TABLE_DATE_FORMAT.format(((Date) rowValue).toLocalDate()));
|
||||
} else {
|
||||
json.addProperty(rcn, rowValue.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (json.size() > 0) {
|
||||
results.add(json);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private SqlRowSet callFilteredMysql(String sql, String valueFilter, String filterColumnType) {
|
||||
Object finalValueFilter = valueFilter;
|
||||
SqlRowSet rowSet;
|
||||
if (BIT_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
int[] argType = new int[1];
|
||||
argType[0] = Types.BIT;
|
||||
finalValueFilter = BooleanUtils.toString(BooleanUtils.toBooleanObject(valueFilter), "1", "0", "null");
|
||||
rowSet = jdbcTemplate.queryForRowSet(sql, new Object[] { finalValueFilter }, argType);
|
||||
} else if (MYSQL_DATE_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
int[] argType = new int[1];
|
||||
argType[0] = Types.DATE;
|
||||
try {
|
||||
rowSet = jdbcTemplate.queryForRowSet(sql, new Object[] { LocalDate.parse(valueFilter, DATA_TABLE_DATE_FORMAT) }, argType);
|
||||
} catch (DateTimeParseException e) {
|
||||
List<ApiParameterError> paramErrors = new ArrayList<>();
|
||||
paramErrors.add(parameterErrorWithValue("400",
|
||||
"Unsupported input type for datatable query! Use format: 'yyyy-MM-dd'. Column filter: " + filterColumnType,
|
||||
"valueFilter", valueFilter));
|
||||
throw new PlatformApiDataValidationException(paramErrors, e);
|
||||
}
|
||||
} else if (MYSQL_DATETIME_TYPE.equals(filterColumnType)) {
|
||||
int[] argType = new int[1];
|
||||
argType[0] = Types.TIMESTAMP;
|
||||
try {
|
||||
rowSet = jdbcTemplate.queryForRowSet(sql, new Object[] { LocalDateTime.parse(valueFilter, DATA_TABLE_DATETIME_FORMAT) },
|
||||
argType);
|
||||
} catch (DateTimeParseException e) {
|
||||
List<ApiParameterError> paramErrors = new ArrayList<>();
|
||||
paramErrors.add(parameterErrorWithValue("400",
|
||||
"Unsupported input type for datatable query! Use format: 'yyyy-MM-dd HH:mm:ss'. Column filter: " + filterColumnType,
|
||||
"valueFilter", valueFilter));
|
||||
throw new PlatformApiDataValidationException(paramErrors, e);
|
||||
}
|
||||
} else {
|
||||
rowSet = jdbcTemplate.queryForRowSet(sql, finalValueFilter);
|
||||
}
|
||||
return rowSet;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private SqlRowSet callFilteredPgSql(String sql, String valueFilter, String filterColumnType) {
|
||||
Object finalValueFilter = valueFilter;
|
||||
int[] argType = new int[1];
|
||||
if (BIT_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
finalValueFilter = BooleanUtils.toString(BooleanUtils.toBooleanObject(valueFilter), "1", "0", "null");
|
||||
argType[0] = Types.BIT;
|
||||
} else if (PG_BOOLEAN_TYPE.equalsIgnoreCase(filterColumnType) || PG_BOOL_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
finalValueFilter = BooleanUtils.toString(BooleanUtils.toBooleanObject(valueFilter), "true", "false", "null");
|
||||
argType[0] = Types.BOOLEAN;
|
||||
} else if (PG_INT_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
argType[0] = Types.INTEGER;
|
||||
} else if (PG_BIGINT_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
argType[0] = Types.BIGINT;
|
||||
} else if (PG_DATE_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
argType[0] = Types.DATE;
|
||||
} else if (filterColumnType.toLowerCase().contains("timestamp")) {
|
||||
argType[0] = Types.TIMESTAMP;
|
||||
} else if (PG_NUMERIC_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
argType[0] = Types.DECIMAL;
|
||||
} else if (PG_TEXT_TYPE.equalsIgnoreCase(filterColumnType) || PG_VARCHAR_TYPE.equalsIgnoreCase(filterColumnType)) {
|
||||
argType[0] = Types.VARCHAR;
|
||||
} else {
|
||||
List<ApiParameterError> paramErrors = new ArrayList<>();
|
||||
paramErrors.add(parameterErrorWithValue("400", "Unsupported input type for datatable query! Column filter: " + filterColumnType,
|
||||
"valueFilter", valueFilter));
|
||||
throw new PlatformApiDataValidationException(paramErrors);
|
||||
}
|
||||
return jdbcTemplate.queryForRowSet(sql, new Object[] { finalValueFilter }, argType);
|
||||
}
|
||||
|
||||
private static void validateRequestParams(String columnFilter, String valueFilter, String resultColumns,
|
||||
List<ResultsetColumnHeaderData> resultsetColumnHeaderData) {
|
||||
List<ApiParameterError> paramErrors = new ArrayList<>();
|
||||
List<String> dataTableColumnNames = resultsetColumnHeaderData.stream().map(ResultsetColumnHeaderData::getColumnName).toList();
|
||||
if (columnFilter == null || columnFilter.isEmpty()) {
|
||||
paramErrors.add(parameterErrorWithValue("400", "Column filter is empty!", "columnFilter", columnFilter));
|
||||
} else {
|
||||
if (!dataTableColumnNames.contains(columnFilter)) {
|
||||
paramErrors.add(parameterErrorWithValue("400", "Column filter not exist in datatable!", "columnFilter", columnFilter));
|
||||
}
|
||||
}
|
||||
|
||||
if (valueFilter == null || valueFilter.isEmpty()) {
|
||||
paramErrors.add(parameterErrorWithValue("400", "Value filter is empty!", "valueFilter", valueFilter));
|
||||
}
|
||||
|
||||
if (resultColumns == null || resultColumns.isEmpty()) {
|
||||
paramErrors.add(parameterErrorWithValue("400", "Result columns filter is empty!", "resultColumns", resultColumns));
|
||||
} else {
|
||||
asList(resultColumns.split(",")).forEach(rcn -> {
|
||||
if (!dataTableColumnNames.contains(rcn)) {
|
||||
paramErrors.add(parameterErrorWithValue("400", "Result column not exist in datatable!", "resultColumns", rcn));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!paramErrors.isEmpty()) {
|
||||
throw new PlatformApiDataValidationException(paramErrors);
|
||||
}
|
||||
}
|
||||
|
||||
private void logAsErrorUnexpectedDataIntegrityException(final Exception dve) {
|
||||
LOG.error("Error occurred.", dve);
|
||||
}
|
||||
|
||||
+40
-66
@@ -35,7 +35,7 @@ public final class SQLInjectionValidator {
|
||||
|
||||
private static final String[] COMMENTS = { "--", "({", "/*", "#" };
|
||||
|
||||
private static final String SQL_PATTERN = "[a-zA-Z_=,\\-'!><.?\"`% ()0-9*\n\r]*";
|
||||
private static final String SQL_PATTERN = "[a-zA-Z_=,\\-:'!><.?\"`% ()0-9*\n\r]*";
|
||||
|
||||
public static void validateSQLInput(final String sqlSearch) {
|
||||
if (StringUtils.isBlank(sqlSearch)) {
|
||||
@@ -60,69 +60,7 @@ public final class SQLInjectionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Removing the space before and after '=' operator
|
||||
// String s = " \" OR 1 = 1"; For the cases like this
|
||||
boolean injectionFound = false;
|
||||
String inputSqlString = lowerCaseSQL;
|
||||
while (inputSqlString.indexOf(" =") > 0) { // Don't remove space before
|
||||
// = operator
|
||||
inputSqlString = inputSqlString.replaceAll(" =", "=");
|
||||
}
|
||||
|
||||
while (inputSqlString.indexOf("= ") > 0) { // Don't remove space after =
|
||||
// operator
|
||||
inputSqlString = inputSqlString.replaceAll("= ", "=");
|
||||
}
|
||||
|
||||
StringTokenizer tokenizer = new StringTokenizer(inputSqlString, " ");
|
||||
while (tokenizer.hasMoreTokens()) {
|
||||
String token = tokenizer.nextToken().trim();
|
||||
if (token.equals("'")) {
|
||||
if (tokenizer.hasMoreElements()) {
|
||||
String nextToken = tokenizer.nextToken().trim();
|
||||
if (!nextToken.equals("'")) {
|
||||
injectionFound = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
injectionFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (token.equals("\"")) {
|
||||
if (tokenizer.hasMoreElements()) {
|
||||
String nextToken = tokenizer.nextToken().trim();
|
||||
if (!nextToken.equals("\"")) {
|
||||
injectionFound = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
injectionFound = true;
|
||||
break;
|
||||
}
|
||||
} else if (token.indexOf('=') > 0) {
|
||||
StringTokenizer operatorToken = new StringTokenizer(token, "=");
|
||||
String operand = operatorToken.nextToken().trim();
|
||||
if (!operatorToken.hasMoreTokens()) {
|
||||
injectionFound = true;
|
||||
break;
|
||||
}
|
||||
String value = operatorToken.nextToken().trim();
|
||||
if (operand.equals(value)) {
|
||||
injectionFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (injectionFound) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile(SQL_PATTERN);
|
||||
Matcher matcher = pattern.matcher(sqlSearch);
|
||||
if (!matcher.matches()) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
|
||||
}
|
||||
|
||||
public static void validateAdhocQuery(final String sqlSearch) {
|
||||
@@ -142,17 +80,52 @@ public final class SQLInjectionValidator {
|
||||
}
|
||||
}
|
||||
|
||||
// Removing the space before and after '=' operator
|
||||
// String s = " \" OR 1 = 1"; For the cases like this
|
||||
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
|
||||
}
|
||||
|
||||
public static void validateDynamicQuery(final String sqlSearch) {
|
||||
if (StringUtils.isBlank(sqlSearch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lowerCaseSQL = sqlSearch.toLowerCase();
|
||||
for (String ddl : DDL_COMMANDS) {
|
||||
if (ddl.equals(lowerCaseSQL)) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
|
||||
for (String dml : DML_COMMANDS) {
|
||||
if (dml.equals(lowerCaseSQL)) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
|
||||
for (String comment : COMMENTS) {
|
||||
if (comment.equals(lowerCaseSQL)) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
|
||||
// Removing the space before and after '=' operator
|
||||
// String s = " \" OR 1 = 1"; For the cases like this
|
||||
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
|
||||
}
|
||||
|
||||
private static void patternMatchSqlInjection(String sqlSearch, String lowerCaseSQL) {
|
||||
// Removing the space before and after '=' operator
|
||||
// String s = " \" OR 1 = 1"; For the cases like this
|
||||
boolean injectionFound = false;
|
||||
String inputSqlString = lowerCaseSQL;
|
||||
while (inputSqlString.indexOf(" =") > 0) { // Don't remove space before
|
||||
// = operator
|
||||
// = operator
|
||||
inputSqlString = inputSqlString.replaceAll(" =", "=");
|
||||
}
|
||||
|
||||
while (inputSqlString.indexOf("= ") > 0) { // Don't remove space after =
|
||||
// operator
|
||||
// operator
|
||||
inputSqlString = inputSqlString.replaceAll("= ", "=");
|
||||
}
|
||||
|
||||
@@ -206,4 +179,5 @@ public final class SQLInjectionValidator {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* 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.dataqueries.service;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionException;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.rowset.SqlRowSet;
|
||||
|
||||
public class ReadWriteNonCoreDataServiceImplTest {
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Mock
|
||||
private GenericDataService genericDataService;
|
||||
|
||||
@Mock
|
||||
private DatabaseTypeResolver databaseTypeResolver;
|
||||
|
||||
@InjectMocks
|
||||
private ReadWriteNonCoreDataServiceImpl underTest;
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
MockitoAnnotations.openMocks(this);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSqlInjectionCaughtQueryDataTable() {
|
||||
assertThrows(SQLInjectionException.class, () -> {
|
||||
underTest.queryDataTable("table", "cf1", "vf1", "' or 1=1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSqlInjectionCaughtQueryDataTable2() {
|
||||
assertThrows(SQLInjectionException.class, () -> {
|
||||
underTest.queryDataTable("table", "cf1", "vf1", "1; DROP TABLE m_loan; SELECT");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryDataTableSuccess() {
|
||||
SqlRowSet sqlRS = Mockito.mock(SqlRowSet.class);
|
||||
when(jdbcTemplate.queryForRowSet(eq("select rc1,rc2 from table where cf1 = ?"), any(Object[].class), any(int[].class)))
|
||||
.thenReturn(sqlRS);
|
||||
when(sqlRS.next()).thenReturn(true).thenReturn(false);
|
||||
when(sqlRS.getObject(ArgumentMatchers.anyString())).thenReturn("value1").thenReturn("value2");
|
||||
when(databaseTypeResolver.isPostgreSQL()).thenReturn(true);
|
||||
|
||||
ResultsetColumnHeaderData cf1 = ResultsetColumnHeaderData.detailed("cf1", "text", 10L, false, false, null, null);
|
||||
ResultsetColumnHeaderData rc1 = ResultsetColumnHeaderData.detailed("rc1", "text", 10L, false, false, null, null);
|
||||
ResultsetColumnHeaderData rc2 = ResultsetColumnHeaderData.detailed("rc2", "text", 10L, false, false, null, null);
|
||||
when(genericDataService.fillResultsetColumnHeaders("table")).thenReturn(List.of(cf1, rc1, rc2));
|
||||
|
||||
List<JsonObject> results = underTest.queryDataTable("table", "cf1", "vf1", "rc1,rc2");
|
||||
|
||||
Assertions.assertEquals("value1", results.get(0).get("rc1").getAsString());
|
||||
Assertions.assertEquals("value2", results.get(0).get("rc2").getAsString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryDataTableValidationError() {
|
||||
when(genericDataService.fillResultsetColumnHeaders("table")).thenReturn(Collections.emptyList());
|
||||
assertThrows(PlatformApiDataValidationException.class, () -> underTest.queryDataTable("table", "cf1", "vf1", "rc1,rc2"));
|
||||
}
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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.security.utils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class SQLInjectionValidatorTest {
|
||||
|
||||
private static final String[] DDL_COMMANDS = { "create", "drop", "alter", "truncate", "comment", "sleep" };
|
||||
private static final String[] DML_COMMANDS = { "select", "insert", "update", "delete", "merge", "upsert", "call" };
|
||||
private static final String[] COMMENTS = { "--", "({", "/*", "#" };
|
||||
|
||||
@Test
|
||||
public void testValidateSqlInputQuote() {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateSQLInput("' or 1=1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateSqlInputSemicolon() {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateSQLInput("; drop table foo;");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateAdhocQueryQuote() {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateAdhocQuery("' or 1=1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateAdhocQuerySemicolon() {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateAdhocQuery("; drop table foo;");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDynamicQueryQuote() {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateDynamicQuery("' or 1=1");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDynamicQuerySemicolon() {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateDynamicQuery("; drop table foo;");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateSqlLInputReservedWords() {
|
||||
Arrays.asList(DDL_COMMANDS).forEach(ddl -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateSQLInput(ddl);
|
||||
});
|
||||
});
|
||||
|
||||
Arrays.asList(DML_COMMANDS).forEach(dml -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateSQLInput(dml);
|
||||
});
|
||||
});
|
||||
|
||||
Arrays.asList(COMMENTS).forEach(comment -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateSQLInput(comment);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateAdhocQueryReservedWords() {
|
||||
Arrays.asList(DDL_COMMANDS).forEach(ddl -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateAdhocQuery(ddl);
|
||||
});
|
||||
});
|
||||
|
||||
// left out intentionally from adhocquery validation?
|
||||
// Arrays.asList(DML_COMMANDS).forEach(dml -> {
|
||||
// Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
// SQLInjectionValidator.validateAdhocQuery(dml);
|
||||
// });
|
||||
// });
|
||||
|
||||
Arrays.asList(COMMENTS).forEach(comment -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateAdhocQuery(comment);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDynamicQueryReservedWords() {
|
||||
Arrays.asList(DDL_COMMANDS).forEach(ddl -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateDynamicQuery(ddl);
|
||||
});
|
||||
});
|
||||
|
||||
Arrays.asList(DML_COMMANDS).forEach(dml -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateDynamicQuery(dml);
|
||||
});
|
||||
});
|
||||
|
||||
Arrays.asList(COMMENTS).forEach(comment -> {
|
||||
Assertions.assertThrows(SQLInjectionException.class, () -> {
|
||||
SQLInjectionValidator.validateDynamicQuery(comment);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDynamicQueryColon() {
|
||||
SQLInjectionValidator.validateDynamicQuery("2022-10-13 18:40:21");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateDynamicQueryReservedWordsInsideValue() {
|
||||
Arrays.asList(DDL_COMMANDS).forEach(ddl -> SQLInjectionValidator.validateDynamicQuery("foo" + ddl));
|
||||
Arrays.asList(DML_COMMANDS).forEach(dml -> SQLInjectionValidator.validateDynamicQuery("foo" + dml));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user