FINERACT-1747 add datatable query api

This commit is contained in:
Zoltan Nebli
2022-10-15 09:02:17 +02:00
committed by Arnold Gálovics
parent f26b7bc576
commit ad2f2862cb
9 changed files with 640 additions and 158 deletions
@@ -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 })
@@ -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;
}
@@ -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);
@@ -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);
}
@@ -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();
}
}
}
@@ -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"));
}
}
@@ -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));
}
}