FINERACT-2461: Replace SQL string concatenation with parameterized queries
Fix ~30 SQL injection vulnerabilities across datatable services by replacing string concatenation with JDBC parameterized queries (?). Files changed: - DatatableWriteServiceImpl: parameterized DELETE, INSERT, SELECT queries - DatatableUtil: parameterized dataScopedSQL, office join conditions - GenericDataService/Impl: added parameterized fillResultsetRowData overload - DatatableReadServiceImpl: parameterized countDatatableEntries - WriteSurveyServiceImpl: removed duplicate vulnerable getPermissionSql - DepositAccountReadPlatformServiceImpl: added pagination validation Tests added: - DatatableUtilTest: 11 tests for parameterized dataScopedSQL and joins - DatatableWriteServiceImplTest: 7 tests for parameterized CRUD queries - DatatableReadServiceImplCountTest: 1 test for parameterized count query Do good with AI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
committed by
Adam Saghy
co-authored by
Claude Opus 4.6
parent
87e328e9e7
commit
054d18b020
+2
@@ -31,6 +31,8 @@ public interface GenericDataService {
|
||||
|
||||
List<ResultsetRowData> fillResultsetRowData(String sql, List<ResultsetColumnHeaderData> columnHeaders);
|
||||
|
||||
List<ResultsetRowData> fillResultsetRowData(String sql, List<ResultsetColumnHeaderData> columnHeaders, Object... args);
|
||||
|
||||
String generateJsonFromGenericResultsetData(GenericResultsetData grs);
|
||||
|
||||
String replace(String str, String pattern, String replace);
|
||||
|
||||
+2
-2
@@ -263,8 +263,8 @@ public class DatatableReadServiceImpl implements DatatableReadService {
|
||||
@Override
|
||||
public Long countDatatableEntries(final String datatableName, final Long appTableId, String foreignKeyColumn) {
|
||||
final String sqlString = "SELECT COUNT(" + sqlGenerator.escape(foreignKeyColumn) + ") FROM " + sqlGenerator.escape(datatableName)
|
||||
+ " WHERE " + sqlGenerator.escape(foreignKeyColumn) + " = " + appTableId;
|
||||
return this.jdbcTemplate.queryForObject(sqlString, Long.class); // NOSONAR
|
||||
+ " WHERE " + sqlGenerator.escape(foreignKeyColumn) + " = ?";
|
||||
return this.jdbcTemplate.queryForObject(sqlString, Long.class, appTableId); // NOSONAR
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+76
-45
@@ -23,6 +23,7 @@ import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiCon
|
||||
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_FIELD_ID;
|
||||
import static org.apache.fineract.infrastructure.dataqueries.api.DataTableApiConstant.TABLE_REGISTERED_TABLE;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -112,9 +113,10 @@ public class DatatableUtil {
|
||||
}
|
||||
|
||||
public CommandProcessingResult checkMainResourceExistsWithinScope(@NonNull EntityTables entityTable, final Long appTableId) {
|
||||
final String sql = dataScopedSQL(entityTable, appTableId);
|
||||
final List<Object> params = new ArrayList<>();
|
||||
final String sql = dataScopedSQL(entityTable, appTableId, params);
|
||||
log.debug("data scoped sql: {}", sql);
|
||||
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql);
|
||||
final SqlRowSet rs = this.jdbcTemplate.queryForRowSet(sql, params.toArray());
|
||||
|
||||
if (!rs.next()) {
|
||||
throw new DatatableNotFoundException(entityTable, appTableId);
|
||||
@@ -140,60 +142,88 @@ public class DatatableUtil {
|
||||
.build();
|
||||
}
|
||||
|
||||
public String dataScopedSQL(@NonNull EntityTables entityTable, final Long appTableId) {
|
||||
public String dataScopedSQL(@NonNull EntityTables entityTable, final Long appTableId, final List<Object> params) {
|
||||
// unfortunately have to, one way or another, be able to restrict data to the users office hierarchy. Here, a
|
||||
// few key tables are done. But if additional fields are needed on other tables the same pattern applies
|
||||
final AppUser currentUser = this.context.authenticatedUser();
|
||||
String officeHierarchy = currentUser.getOffice().getHierarchy();
|
||||
String hierarchyPattern = officeHierarchy + "%";
|
||||
// m_loan and m_savings_account are connected to an m_office through either an m_client or an m_group If both it
|
||||
// means it relates to an m_client that is in a group (still an m_client account)
|
||||
return switch (entityTable) {
|
||||
case LOAN -> "select distinct x.* from ( "
|
||||
+ "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l "
|
||||
+ getClientOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " union all "
|
||||
+ "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l "
|
||||
+ getGroupOfficeJoinCondition(officeHierarchy, "l") + " where l.id = " + appTableId + ")" + " ) as x";
|
||||
case SAVINGS -> "select distinct x.* from ( "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId "
|
||||
+ "from m_savings_account s " + getClientOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")"
|
||||
+ " union all "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId "
|
||||
+ "from m_savings_account s " + getGroupOfficeJoinCondition(officeHierarchy, "s") + " where s.id = " + appTableId + ")"
|
||||
+ " ) as x";
|
||||
case SAVINGS_TRANSACTION -> "select distinct x.* from ( "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId "
|
||||
+ "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id "
|
||||
+ getClientOfficeJoinCondition(officeHierarchy, "s") + " where t.id = " + appTableId + ")" + " union all "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId "
|
||||
+ "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id "
|
||||
+ getGroupOfficeJoinCondition(officeHierarchy, "s") + " where t.id = " + appTableId + ")" + " ) as x";
|
||||
case CLIENT ->
|
||||
"select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_client c "
|
||||
+ getOfficeJoinCondition(officeHierarchy, "c") + " where c.id = " + appTableId;
|
||||
case GROUP, CENTER ->
|
||||
"select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_group g "
|
||||
+ getOfficeJoinCondition(officeHierarchy, "g") + " where g.id = " + appTableId;
|
||||
case OFFICE ->
|
||||
"select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_office o "
|
||||
+ "where o.hierarchy like '" + officeHierarchy + "%'" + " and o.id = " + appTableId;
|
||||
case LOAN_PRODUCT, SAVINGS_PRODUCT, SHARE_PRODUCT ->
|
||||
"select null as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, p.id as entityId from "
|
||||
+ entityTable.getName() + " as p WHERE p.id = " + appTableId;
|
||||
case LOAN -> {
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
yield "select distinct x.* from ( "
|
||||
+ "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l "
|
||||
+ getClientOfficeJoinCondition("l") + " where l.id = ?)" + " union all "
|
||||
+ "(select o.id as officeId, l.group_id as groupId, l.client_id as clientId, null as savingsId, l.id as loanId, null as transactionId, null as entityId from m_loan l "
|
||||
+ getGroupOfficeJoinCondition("l") + " where l.id = ?)" + " ) as x";
|
||||
}
|
||||
case SAVINGS -> {
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
yield "select distinct x.* from ( "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId "
|
||||
+ "from m_savings_account s " + getClientOfficeJoinCondition("s") + " where s.id = ?)" + " union all "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, null as transactionId, null as entityId "
|
||||
+ "from m_savings_account s " + getGroupOfficeJoinCondition("s") + " where s.id = ?)" + " ) as x";
|
||||
}
|
||||
case SAVINGS_TRANSACTION -> {
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
yield "select distinct x.* from ( "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId "
|
||||
+ "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id "
|
||||
+ getClientOfficeJoinCondition("s") + " where t.id = ?)" + " union all "
|
||||
+ "(select o.id as officeId, s.group_id as groupId, s.client_id as clientId, s.id as savingsId, null as loanId, t.id as transactionId, null as entityId "
|
||||
+ "from m_savings_account_transaction t join m_savings_account s on t.savings_account_id = s.id "
|
||||
+ getGroupOfficeJoinCondition("s") + " where t.id = ?)" + " ) as x";
|
||||
}
|
||||
case CLIENT -> {
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
yield "select o.id as officeId, null as groupId, c.id as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_client c "
|
||||
+ getOfficeJoinCondition("c") + " where c.id = ?";
|
||||
}
|
||||
case GROUP, CENTER -> {
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
yield "select o.id as officeId, g.id as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_group g "
|
||||
+ getOfficeJoinCondition("g") + " where g.id = ?";
|
||||
}
|
||||
case OFFICE -> {
|
||||
params.add(hierarchyPattern);
|
||||
params.add(appTableId);
|
||||
yield "select o.id as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, null as entityId from m_office o "
|
||||
+ "where o.hierarchy like ? and o.id = ?";
|
||||
}
|
||||
case LOAN_PRODUCT, SAVINGS_PRODUCT, SHARE_PRODUCT -> {
|
||||
params.add(appTableId);
|
||||
yield "select null as officeId, null as groupId, null as clientId, null as savingsId, null as loanId, null as transactionId, p.id as entityId from "
|
||||
+ entityTable.getName() + " as p WHERE p.id = ?";
|
||||
}
|
||||
default -> throw new PlatformDataIntegrityException("error.msg.invalid.dataScopeCriteria",
|
||||
"Application Table: " + entityTable.getName() + " not catered for in data Scoping");
|
||||
};
|
||||
}
|
||||
|
||||
public String getOfficeJoinCondition(String officeHierarchy, String joinTableAlias) {
|
||||
return " join m_office o on o.id = " + joinTableAlias + ".office_id and o.hierarchy like '" + officeHierarchy + "%' ";
|
||||
public String getOfficeJoinCondition(String joinTableAlias) {
|
||||
return " join m_office o on o.id = " + joinTableAlias + ".office_id and o.hierarchy like ? ";
|
||||
}
|
||||
|
||||
public String getGroupOfficeJoinCondition(String officeHierarchy, String appTableAlias) {
|
||||
return " join m_group g on g.id = " + appTableAlias + ".group_id " + getOfficeJoinCondition(officeHierarchy, "g");
|
||||
public String getGroupOfficeJoinCondition(String appTableAlias) {
|
||||
return " join m_group g on g.id = " + appTableAlias + ".group_id " + getOfficeJoinCondition("g");
|
||||
}
|
||||
|
||||
public String getClientOfficeJoinCondition(String officeHierarchy, String appTableAlias) {
|
||||
return " join m_client c on c.id = " + appTableAlias + ".client_id " + getOfficeJoinCondition(officeHierarchy, "c");
|
||||
public String getClientOfficeJoinCondition(String appTableAlias) {
|
||||
return " join m_client c on c.id = " + appTableAlias + ".client_id " + getOfficeJoinCondition("c");
|
||||
}
|
||||
|
||||
public GenericResultsetData retrieveDataTableGenericResultSet(final EntityTables entityTable, final String dataTableName,
|
||||
@@ -201,20 +231,21 @@ public class DatatableUtil {
|
||||
final List<ResultsetColumnHeaderData> columnHeaders = genericDataService.fillResultsetColumnHeaders(dataTableName);
|
||||
final boolean multiRow = isMultirowDatatable(columnHeaders);
|
||||
|
||||
String whereClause = getFKField(entityTable) + " = " + appTableId;
|
||||
sqlValidator.validate(whereClause);
|
||||
String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + whereClause;
|
||||
final List<Object> params = new ArrayList<>();
|
||||
params.add(appTableId);
|
||||
String sql = "select * from " + sqlGenerator.escape(dataTableName) + " where " + getFKField(entityTable) + " = ?";
|
||||
|
||||
// id only used for reading a specific entry that belongs to appTableId (in a one to many datatable)
|
||||
if (multiRow && id != null) {
|
||||
sql = sql + " and " + TABLE_FIELD_ID + " = " + id;
|
||||
sql = sql + " and " + TABLE_FIELD_ID + " = ?";
|
||||
params.add(id);
|
||||
}
|
||||
if (StringUtils.isNotBlank(order)) {
|
||||
columnValidator.validateSqlInjection(sql, order);
|
||||
sql = sql + " order by " + order;
|
||||
}
|
||||
|
||||
final List<ResultsetRowData> result = genericDataService.fillResultsetRowData(sql, columnHeaders);
|
||||
final List<ResultsetRowData> result = genericDataService.fillResultsetRowData(sql, columnHeaders, params.toArray());
|
||||
return new GenericResultsetData(columnHeaders, result);
|
||||
}
|
||||
|
||||
|
||||
+41
-62
@@ -152,9 +152,7 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
@Override
|
||||
public void registerDatatable(final String dataTableName, final String entityName, final String entitySubType) {
|
||||
Integer category = DataTableApiConstant.CATEGORY_DEFAULT;
|
||||
|
||||
final String permissionSql = this.getPermissionSql(dataTableName);
|
||||
this.registerDataTable(entityName, dataTableName, entitySubType, category, permissionSql);
|
||||
this.registerDataTable(entityName, dataTableName, entitySubType, category);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -167,13 +165,13 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
Integer category = this.getCategory(command);
|
||||
|
||||
this.dataTableValidator.validateDataTableRegistration(command.json());
|
||||
final String permissionSql = this.getPermissionSql(dataTableName);
|
||||
this.registerDataTable(applicationTableName, dataTableName, entitySubType, category, permissionSql);
|
||||
this.registerDataTable(applicationTableName, dataTableName, entitySubType, category);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void registerDatatable(final JsonCommand command, final String permissionSql) {
|
||||
// permissionSql parameter is kept for interface compatibility but ignored in favor of parameterized queries
|
||||
final String applicationTableName = datatableReadService.getTableName(command.getUrl());
|
||||
final String dataTableName = datatableReadService.getDataTableName(command.getUrl());
|
||||
final String entitySubType = command.stringValueOfParameterNamed(ENTITY_SUB_TYPE);
|
||||
@@ -182,30 +180,23 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
|
||||
this.dataTableValidator.validateDataTableRegistration(command.json());
|
||||
|
||||
this.registerDataTable(applicationTableName, dataTableName, entitySubType, category, permissionSql);
|
||||
this.registerDataTable(applicationTableName, dataTableName, entitySubType, category);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public void deregisterDatatable(final String datatable) {
|
||||
datatableUtil.validateDatatableRegistered(datatable);
|
||||
final String permissionList = "('CREATE_" + datatable + "', 'CREATE_" + datatable + "_CHECKER', 'READ_" + datatable + "', 'UPDATE_"
|
||||
+ datatable + "', 'UPDATE_" + datatable + "_CHECKER', 'DELETE_" + datatable + "', 'DELETE_" + datatable + "_CHECKER')";
|
||||
final Object[] permissionCodes = { "CREATE_" + datatable, "CREATE_" + datatable + "_CHECKER", "READ_" + datatable,
|
||||
"UPDATE_" + datatable, "UPDATE_" + datatable + "_CHECKER", "DELETE_" + datatable, "DELETE_" + datatable + "_CHECKER" };
|
||||
final String placeholders = "(?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
final String deleteRolePermissionsSql = "delete from m_role_permission where m_role_permission.permission_id in (select id from m_permission where code in "
|
||||
+ permissionList + ")";
|
||||
|
||||
final String deletePermissionsSql = "delete from m_permission where code in " + permissionList;
|
||||
final String deleteRegisteredDatatableSql = "delete from x_registered_table where registered_table_name = '" + datatable + "'";
|
||||
final String deleteFromConfigurationSql = "delete from c_configuration where name ='" + datatable + "'";
|
||||
|
||||
String[] sqlArray = new String[4];
|
||||
sqlArray[0] = deleteRolePermissionsSql;
|
||||
sqlArray[1] = deletePermissionsSql;
|
||||
sqlArray[2] = deleteRegisteredDatatableSql;
|
||||
sqlArray[3] = deleteFromConfigurationSql;
|
||||
|
||||
this.jdbcTemplate.batchUpdate(sqlArray); // NOSONAR
|
||||
this.jdbcTemplate
|
||||
.update("DELETE FROM m_role_permission WHERE m_role_permission.permission_id IN (SELECT id FROM m_permission WHERE code IN "
|
||||
+ placeholders + ")", permissionCodes);
|
||||
this.jdbcTemplate.update("DELETE FROM m_permission WHERE code IN " + placeholders, permissionCodes);
|
||||
this.jdbcTemplate.update("DELETE FROM x_registered_table WHERE registered_table_name = ?", datatable);
|
||||
this.jdbcTemplate.update("DELETE FROM c_configuration WHERE name = ?", datatable);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -605,8 +596,8 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
return deleteDatatableEntries(dataTableName, appTableId, datatableId, command);
|
||||
}
|
||||
|
||||
private void registerDataTable(final String entityName, final String dataTableName, final String entitySubType, final Integer category,
|
||||
final String permissionsSql) {
|
||||
private void registerDataTable(final String entityName, final String dataTableName, final String entitySubType,
|
||||
final Integer category) {
|
||||
datatableUtil.resolveEntity(entityName);
|
||||
datatableUtil.validateDatatableName(dataTableName);
|
||||
validateDataTableExists(dataTableName);
|
||||
@@ -622,7 +613,7 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
|
||||
try {
|
||||
this.namedParameterJdbcTemplate.update(registerDatatableSql, paramMap);
|
||||
this.jdbcTemplate.update(permissionsSql);
|
||||
this.registerPermissions(dataTableName, true);
|
||||
|
||||
// add the registered table to the config if it is a ppi
|
||||
if (category.equals(DataTableApiConstant.CATEGORY_PPI)) {
|
||||
@@ -638,24 +629,21 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
}
|
||||
}
|
||||
|
||||
private String getPermissionSql(final String dataTableName) {
|
||||
final String createPermission = "'CREATE_" + dataTableName + "'";
|
||||
final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'";
|
||||
final String readPermission = "'READ_" + dataTableName + "'";
|
||||
final String updatePermission = "'UPDATE_" + dataTableName + "'";
|
||||
final String updatePermissionChecker = "'UPDATE_" + dataTableName + "_CHECKER'";
|
||||
final String deletePermission = "'DELETE_" + dataTableName + "'";
|
||||
final String deletePermissionChecker = "'DELETE_" + dataTableName + "_CHECKER'";
|
||||
private void registerPermissions(final String dataTableName, final boolean canMakerChecker) {
|
||||
final List<String> escapedColumns = Stream.of("grouping", "code", "action_name", "entity_name", "can_maker_checker")
|
||||
.map(sqlGenerator::escape).toList();
|
||||
final String columns = String.join(", ", escapedColumns);
|
||||
|
||||
return "insert into m_permission (" + columns + ") values " + "('datatable', " + createPermission + ", 'CREATE', '" + dataTableName
|
||||
+ "', true)," + "('datatable', " + createPermissionChecker + ", 'CREATE', '" + dataTableName + "', false),"
|
||||
+ "('datatable', " + readPermission + ", 'READ', '" + dataTableName + "', false)," + "('datatable', " + updatePermission
|
||||
+ ", 'UPDATE', '" + dataTableName + "', true)," + "('datatable', " + updatePermissionChecker + ", 'UPDATE', '"
|
||||
+ dataTableName + "', false)," + "('datatable', " + deletePermission + ", 'DELETE', '" + dataTableName + "', true),"
|
||||
+ "('datatable', " + deletePermissionChecker + ", 'DELETE', '" + dataTableName + "', false)";
|
||||
final String sql = "INSERT INTO m_permission (" + columns + ") VALUES ('datatable', ?, ?, ?, ?)";
|
||||
final Object[][] permissions = { { "CREATE_" + dataTableName, "CREATE", dataTableName, canMakerChecker },
|
||||
{ "CREATE_" + dataTableName + "_CHECKER", "CREATE", dataTableName, false },
|
||||
{ "READ_" + dataTableName, "READ", dataTableName, false },
|
||||
{ "UPDATE_" + dataTableName, "UPDATE", dataTableName, canMakerChecker },
|
||||
{ "UPDATE_" + dataTableName + "_CHECKER", "UPDATE", dataTableName, false },
|
||||
{ "DELETE_" + dataTableName, "DELETE", dataTableName, canMakerChecker },
|
||||
{ "DELETE_" + dataTableName + "_CHECKER", "DELETE", dataTableName, false }, };
|
||||
for (Object[] params : permissions) {
|
||||
this.jdbcTemplate.update(sql, params);
|
||||
}
|
||||
}
|
||||
|
||||
private Integer getCategory(final JsonCommand command) {
|
||||
@@ -897,9 +885,9 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
String schemaSql = databaseTypeResolver.isMySQL() ? "i.TABLE_SCHEMA = SCHEMA()"
|
||||
: "i.table_catalog = current_catalog AND i.table_schema = current_schema";
|
||||
String findFKSql = "SELECT count(*) FROM information_schema.TABLE_CONSTRAINTS i" + " WHERE i.CONSTRAINT_TYPE = 'FOREIGN KEY' AND "
|
||||
+ schemaSql + " AND i.TABLE_NAME = '" + datatableName + "' AND i.CONSTRAINT_NAME = '" + fkName + "' ";
|
||||
+ schemaSql + " AND i.TABLE_NAME = ? AND i.CONSTRAINT_NAME = ?";
|
||||
|
||||
final Integer count = this.jdbcTemplate.queryForObject(findFKSql, Integer.class); // NOSONAR
|
||||
final Integer count = this.jdbcTemplate.queryForObject(findFKSql, Integer.class, datatableName, fkName); // NOSONAR
|
||||
if (count != null && count > 0) {
|
||||
codeMappings.add(datatableAlias + "_" + name);
|
||||
constrainBuilder.append(", DROP FOREIGN KEY ").append(sqlGenerator.escape(fkName)).append(" ");
|
||||
@@ -908,36 +896,28 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
|
||||
private void registerColumnCodeMapping(final Map<String, Long> codeMappings) {
|
||||
if (codeMappings != null && !codeMappings.isEmpty()) {
|
||||
final String[] addSqlList = new String[codeMappings.size()];
|
||||
int i = 0;
|
||||
final String sql = "INSERT INTO x_table_column_code_mappings (column_alias_name, code_id) VALUES (?, ?)";
|
||||
for (final Map.Entry<String, Long> mapEntry : codeMappings.entrySet()) {
|
||||
addSqlList[i++] = "insert into x_table_column_code_mappings (column_alias_name, code_id) values ('" + mapEntry.getKey()
|
||||
+ "'," + mapEntry.getValue() + ");";
|
||||
this.jdbcTemplate.update(sql, mapEntry.getKey(), mapEntry.getValue());
|
||||
}
|
||||
|
||||
this.jdbcTemplate.batchUpdate(addSqlList);
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteColumnCodeMapping(final List<String> columnNames) {
|
||||
if (columnNames != null && !columnNames.isEmpty()) {
|
||||
final String[] deleteSqlList = new String[columnNames.size()];
|
||||
int i = 0;
|
||||
final String sql = "DELETE FROM x_table_column_code_mappings WHERE column_alias_name = ?";
|
||||
for (final String columnName : columnNames) {
|
||||
deleteSqlList[i++] = "DELETE FROM x_table_column_code_mappings WHERE column_alias_name='" + columnName + "';";
|
||||
this.jdbcTemplate.update(sql, columnName);
|
||||
}
|
||||
|
||||
this.jdbcTemplate.batchUpdate(deleteSqlList);
|
||||
}
|
||||
}
|
||||
|
||||
private int getCodeIdForColumn(final String dataTableNameAlias, final String name) {
|
||||
final StringBuilder checkColumnCodeMapping = new StringBuilder();
|
||||
checkColumnCodeMapping.append("select ccm.code_id from x_table_column_code_mappings ccm where ccm.column_alias_name='")
|
||||
.append(dataTableNameAlias).append("_").append(name).append("'");
|
||||
final String sql = "SELECT ccm.code_id FROM x_table_column_code_mappings ccm WHERE ccm.column_alias_name = ?";
|
||||
final String columnAliasName = dataTableNameAlias + "_" + name;
|
||||
Integer codeId = 0;
|
||||
try {
|
||||
codeId = this.jdbcTemplate.queryForObject(checkColumnCodeMapping.toString(), Integer.class);
|
||||
codeId = this.jdbcTemplate.queryForObject(sql, Integer.class, columnAliasName);
|
||||
} catch (final EmptyResultDataAccessException e) {
|
||||
log.warn("Error occurred.", e);
|
||||
}
|
||||
@@ -1327,10 +1307,9 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
whereColumn = TABLE_FIELD_ID;
|
||||
whereValue = datatableId;
|
||||
}
|
||||
String sql = "DELETE FROM " + sqlGenerator.escape(dataTableName) + " WHERE " + sqlGenerator.escape(whereColumn) + " = "
|
||||
+ whereValue;
|
||||
String sql = "DELETE FROM " + sqlGenerator.escape(dataTableName) + " WHERE " + sqlGenerator.escape(whereColumn) + " = ?";
|
||||
|
||||
this.jdbcTemplate.update(sql); // NOSONAR
|
||||
this.jdbcTemplate.update(sql, whereValue); // NOSONAR
|
||||
final Map<String, Object> dataParams = null;
|
||||
final DatatableEntryDetails datatableEntryDetails = new DatatableEntryDetails(dataTableName, entityTable, datatableId, appTableId,
|
||||
dataParams);
|
||||
@@ -1351,8 +1330,8 @@ public class DatatableWriteServiceImpl implements DatatableWriteService {
|
||||
private boolean isDatatableAttachedToEntityDatatableCheck(final String datatableName) {
|
||||
String sql = "SELECT COUNT(edc.x_registered_table_name) FROM x_registered_table xrt"
|
||||
+ " JOIN m_entity_datatable_check edc ON edc.x_registered_table_name = xrt.registered_table_name"
|
||||
+ " WHERE edc.x_registered_table_name = '" + datatableName + "'";
|
||||
final Long count = this.jdbcTemplate.queryForObject(sql, Long.class); // NOSONAR
|
||||
+ " WHERE edc.x_registered_table_name = ?";
|
||||
final Long count = this.jdbcTemplate.queryForObject(sql, Long.class, datatableName); // NOSONAR
|
||||
return count != null && count > 0;
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -141,6 +141,13 @@ public class GenericDataServiceImpl implements GenericDataService {
|
||||
return fillResultsetRowData(rs, columnHeaders);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public List<ResultsetRowData> fillResultsetRowData(final String sql, List<ResultsetColumnHeaderData> columnHeaders, Object... args) {
|
||||
final SqlRowSet rs = jdbcTemplate.queryForRowSet(sql, args);
|
||||
return fillResultsetRowData(rs, columnHeaders);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static List<ResultsetRowData> fillResultsetRowData(SqlRowSet rs, List<ResultsetColumnHeaderData> columnHeaders) {
|
||||
final SqlRowSetMetaData rsmd = rs.getMetaData();
|
||||
|
||||
+1
-30
@@ -18,13 +18,9 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.survey.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.dataqueries.service.DatatableReadService;
|
||||
import org.apache.fineract.infrastructure.dataqueries.service.DatatableWriteService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -36,39 +32,14 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
@RequiredArgsConstructor
|
||||
public class WriteSurveyServiceImpl implements WriteSurveyService {
|
||||
|
||||
private final DatatableReadService datatableReadService;
|
||||
private final DatatableWriteService datatableWriteService;
|
||||
private final DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public CommandProcessingResult registerSurvey(JsonCommand command) {
|
||||
final String dataTableName = datatableReadService.getDataTableName(command.getUrl());
|
||||
final String permissionSql = this.getPermissionSql(dataTableName);
|
||||
datatableWriteService.registerDatatable(command, permissionSql);
|
||||
datatableWriteService.registerDatatable(command);
|
||||
|
||||
return CommandProcessingResult.commandOnlyResult(command.commandId());
|
||||
|
||||
}
|
||||
|
||||
private String getPermissionSql(final String dataTableName) {
|
||||
final String createPermission = "'CREATE_" + dataTableName + "'";
|
||||
final String createPermissionChecker = "'CREATE_" + dataTableName + "_CHECKER'";
|
||||
final String readPermission = "'READ_" + dataTableName + "'";
|
||||
final String updatePermission = "'UPDATE_" + dataTableName + "'";
|
||||
final String updatePermissionChecker = "'UPDATE_" + dataTableName + "_CHECKER'";
|
||||
final String deletePermission = "'DELETE_" + dataTableName + "'";
|
||||
final String deletePermissionChecker = "'DELETE_" + dataTableName + "_CHECKER'";
|
||||
final List<String> escapedColumns = Stream.of("grouping", "code", "action_name", "entity_name", "can_maker_checker")
|
||||
.map(sqlGenerator::escape).toList();
|
||||
final String columns = String.join(", ", escapedColumns);
|
||||
|
||||
return "insert into m_permission (" + columns + ") values " + "('datatable', " + createPermission + ", 'CREATE', '" + dataTableName
|
||||
+ "', false)," + "('datatable', " + createPermissionChecker + ", 'CREATE', '" + dataTableName + "', false),"
|
||||
+ "('datatable', " + readPermission + ", 'READ', '" + dataTableName + "', false)," + "('datatable', " + updatePermission
|
||||
+ ", 'UPDATE', '" + dataTableName + "', false)," + "('datatable', " + updatePermissionChecker + ", 'UPDATE', '"
|
||||
+ dataTableName + "', false)," + "('datatable', " + deletePermission + ", 'DELETE', '" + dataTableName + "', false),"
|
||||
+ "('datatable', " + deletePermissionChecker + ", 'DELETE', '" + dataTableName + "', false)";
|
||||
}
|
||||
|
||||
@Transactional
|
||||
|
||||
+3
@@ -134,6 +134,9 @@ public class DepositAccountReadPlatformServiceImpl implements DepositAccountRead
|
||||
final PaginationParameters paginationParameters) {
|
||||
|
||||
this.context.authenticatedUser();
|
||||
this.paginationParametersDataValidator.validateParameterValues(paginationParameters, supportedOrderByValues,
|
||||
depositAccountType.resourceName());
|
||||
|
||||
final DepositAccountMapper depositAccountMapper = this.getDepositAccountMapper(depositAccountType);
|
||||
if (depositAccountMapper == null) {
|
||||
return null;
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 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.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
import org.apache.fineract.portfolio.search.service.SearchUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class DatatableReadServiceImplCountTest {
|
||||
|
||||
private static final Long APP_TABLE_ID = 42L;
|
||||
private static final String DATATABLE_NAME = "dt_loan_test";
|
||||
private static final String FK_COLUMN = "loan_id";
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Mock
|
||||
private DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
@Mock
|
||||
private PlatformSecurityContext context;
|
||||
@Mock
|
||||
private GenericDataService genericDataService;
|
||||
@Mock
|
||||
private DataTableValidator dataTableValidator;
|
||||
@Mock
|
||||
private SqlValidator sqlValidator;
|
||||
@Mock
|
||||
private SearchUtil searchUtil;
|
||||
@Mock
|
||||
private DatatableUtil datatableUtil;
|
||||
|
||||
private DatatableReadServiceImpl underTest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
underTest = new DatatableReadServiceImpl(jdbcTemplate, sqlGenerator, context, genericDataService, dataTableValidator, sqlValidator,
|
||||
searchUtil, datatableUtil);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCountDatatableEntriesUsesParameterizedQuery() {
|
||||
when(sqlGenerator.escape(anyString())).thenAnswer(invocation -> "\"" + invocation.getArgument(0) + "\"");
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Long.class), eq(APP_TABLE_ID))).thenReturn(3L);
|
||||
|
||||
Long count = underTest.countDatatableEntries(DATATABLE_NAME, APP_TABLE_ID, FK_COLUMN);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbcTemplate).queryForObject(sqlCaptor.capture(), eq(Long.class), eq(APP_TABLE_ID));
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
assertTrue(sql.contains("= ?"), "SQL should use ? placeholder for appTableId");
|
||||
assertFalse(sql.contains("= " + APP_TABLE_ID), "SQL should not contain concatenated appTableId");
|
||||
assertTrue(sql.contains("\"" + FK_COLUMN + "\""), "SQL should contain escaped FK column name");
|
||||
assertTrue(sql.contains("\"" + DATATABLE_NAME + "\""), "SQL should contain escaped datatable name");
|
||||
assertEquals(3L, count, "Should return the count from the query");
|
||||
}
|
||||
}
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* 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.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseType;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
import org.apache.fineract.infrastructure.security.utils.ColumnValidator;
|
||||
import org.apache.fineract.organisation.office.domain.Office;
|
||||
import org.apache.fineract.portfolio.search.service.SearchUtil;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.support.rowset.SqlRowSet;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class DatatableUtilTest {
|
||||
|
||||
private static final Long APP_TABLE_ID = 42L;
|
||||
private static final String OFFICE_HIERARCHY = ".1.2.";
|
||||
|
||||
@Mock
|
||||
private SearchUtil searchUtil;
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Mock
|
||||
private SqlValidator sqlValidator;
|
||||
@Mock
|
||||
private PlatformSecurityContext context;
|
||||
@Mock
|
||||
private GenericDataService genericDataService;
|
||||
@Mock
|
||||
private DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
@Mock
|
||||
private ColumnValidator columnValidator;
|
||||
|
||||
private DatatableUtil underTest;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
underTest = new DatatableUtil(searchUtil, jdbcTemplate, sqlValidator, context, genericDataService, sqlGenerator, columnValidator);
|
||||
setupSecurityContext();
|
||||
}
|
||||
|
||||
private void setupSecurityContext() {
|
||||
AppUser currentUser = Mockito.mock(AppUser.class);
|
||||
Office office = Mockito.mock(Office.class);
|
||||
when(context.authenticatedUser()).thenReturn(currentUser);
|
||||
when(currentUser.getOffice()).thenReturn(office);
|
||||
when(office.getHierarchy()).thenReturn(OFFICE_HIERARCHY);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlLoanUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.LOAN, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(4, params.size(), "LOAN case should have 4 parameters");
|
||||
assertTrue(sql.contains("where l.id = ?"), "SQL should use ? placeholder for loan id");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
assertFalse(sql.contains("'" + OFFICE_HIERARCHY), "SQL should not contain concatenated officeHierarchy");
|
||||
assertEquals(OFFICE_HIERARCHY + "%", params.get(0), "First param should be hierarchy pattern");
|
||||
assertEquals(APP_TABLE_ID, params.get(1), "Second param should be appTableId");
|
||||
assertEquals(OFFICE_HIERARCHY + "%", params.get(2), "Third param should be hierarchy pattern");
|
||||
assertEquals(APP_TABLE_ID, params.get(3), "Fourth param should be appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlSavingsUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.SAVINGS, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(4, params.size(), "SAVINGS case should have 4 parameters");
|
||||
assertTrue(sql.contains("where s.id = ?"), "SQL should use ? placeholder for savings id");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlSavingsTransactionUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.SAVINGS_TRANSACTION, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(4, params.size(), "SAVINGS_TRANSACTION case should have 4 parameters");
|
||||
assertTrue(sql.contains("where t.id = ?"), "SQL should use ? placeholder for transaction id");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlClientUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.CLIENT, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(2, params.size(), "CLIENT case should have 2 parameters");
|
||||
assertTrue(sql.contains("where c.id = ?"), "SQL should use ? placeholder for client id");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
assertEquals(OFFICE_HIERARCHY + "%", params.get(0), "First param should be hierarchy pattern");
|
||||
assertEquals(APP_TABLE_ID, params.get(1), "Second param should be appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlGroupUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.GROUP, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(2, params.size(), "GROUP case should have 2 parameters");
|
||||
assertTrue(sql.contains("where g.id = ?"), "SQL should use ? placeholder for group id");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlOfficeUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.OFFICE, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(2, params.size(), "OFFICE case should have 2 parameters");
|
||||
assertTrue(sql.contains("o.hierarchy like ?"), "SQL should use ? placeholder for hierarchy LIKE");
|
||||
assertTrue(sql.contains("o.id = ?"), "SQL should use ? placeholder for office id");
|
||||
assertFalse(sql.contains("'" + OFFICE_HIERARCHY), "SQL should not contain concatenated officeHierarchy");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDataScopedSqlProductUsesParameterizedQuery() {
|
||||
List<Object> params = new ArrayList<>();
|
||||
String sql = underTest.dataScopedSQL(EntityTables.LOAN_PRODUCT, APP_TABLE_ID, params);
|
||||
|
||||
assertEquals(1, params.size(), "PRODUCT case should have 1 parameter");
|
||||
assertTrue(sql.contains("p.id = ?"), "SQL should use ? placeholder for product id");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
assertEquals(APP_TABLE_ID, params.get(0), "First param should be appTableId");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCheckMainResourceExistsWithinScopePassesParamsToQuery() {
|
||||
SqlRowSet rs = Mockito.mock(SqlRowSet.class);
|
||||
when(rs.next()).thenReturn(true).thenReturn(false);
|
||||
when(rs.getObject(anyString())).thenReturn(1L);
|
||||
when(jdbcTemplate.queryForRowSet(anyString(), any(Object[].class))).thenReturn(rs);
|
||||
|
||||
underTest.checkMainResourceExistsWithinScope(EntityTables.CLIENT, APP_TABLE_ID);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Object[]> paramsCaptor = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbcTemplate).queryForRowSet(sqlCaptor.capture(), paramsCaptor.capture());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
Object[] capturedParams = paramsCaptor.getValue();
|
||||
|
||||
assertTrue(sql.contains("?"), "SQL should contain ? placeholders");
|
||||
assertFalse(sql.contains(APP_TABLE_ID.toString()), "SQL should not contain concatenated appTableId");
|
||||
assertTrue(capturedParams.length > 0, "Parameters array should not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRetrieveDataTableGenericResultSetUsesParameterizedQuery() {
|
||||
EntityTables entityTable = EntityTables.LOAN;
|
||||
String dataTableName = "test_datatable";
|
||||
Long id = 5L;
|
||||
|
||||
when(sqlGenerator.escape(anyString())).thenReturn("\"" + dataTableName + "\"");
|
||||
when(genericDataService.fillResultsetColumnHeaders(anyString())).thenReturn(createMultiRowHeaders());
|
||||
when(searchUtil.findFiltered(any(), any())).thenReturn(ResultsetColumnHeaderData.basic("id", "bigint", DatabaseType.MYSQL));
|
||||
when(genericDataService.fillResultsetRowData(anyString(), any(), any(Object[].class))).thenReturn(new ArrayList<>());
|
||||
|
||||
underTest.retrieveDataTableGenericResultSet(entityTable, dataTableName, APP_TABLE_ID, null, id);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Object[]> paramsCaptor = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(genericDataService).fillResultsetRowData(sqlCaptor.capture(), any(), paramsCaptor.capture());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
Object[] capturedParams = paramsCaptor.getValue();
|
||||
|
||||
assertTrue(sql.contains("= ?"), "SQL should use ? placeholder for foreign key");
|
||||
assertTrue(sql.contains("id = ?"), "SQL should use ? placeholder for id");
|
||||
assertFalse(sql.contains("= " + APP_TABLE_ID), "SQL should not contain concatenated appTableId");
|
||||
assertFalse(sql.contains("= " + id), "SQL should not contain concatenated id");
|
||||
assertEquals(2, capturedParams.length, "Should have 2 parameters (appTableId + id)");
|
||||
assertEquals(APP_TABLE_ID, capturedParams[0], "First param should be appTableId");
|
||||
assertEquals(id, capturedParams[1], "Second param should be id");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOfficeJoinConditionUsesPlaceholder() {
|
||||
String joinCondition = underTest.getOfficeJoinCondition("t");
|
||||
|
||||
assertTrue(joinCondition.contains("o.hierarchy like ?"), "Join condition should use ? for hierarchy");
|
||||
assertFalse(joinCondition.contains("like '"), "Join condition should not contain literal quote for hierarchy");
|
||||
}
|
||||
|
||||
private List<ResultsetColumnHeaderData> createMultiRowHeaders() {
|
||||
List<ResultsetColumnHeaderData> headers = new ArrayList<>();
|
||||
headers.add(ResultsetColumnHeaderData.basic("id", "bigint", DatabaseType.MYSQL));
|
||||
headers.add(ResultsetColumnHeaderData.basic("loan_id", "bigint", DatabaseType.MYSQL));
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
/**
|
||||
* 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.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.fineract.infrastructure.codes.service.CodeReadPlatformService;
|
||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||
import org.apache.fineract.infrastructure.core.serialization.DatatableCommandFromApiJsonDeserializer;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
|
||||
import org.apache.fineract.infrastructure.core.service.database.DatabaseTypeResolver;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.DataTableValidator;
|
||||
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
|
||||
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||
import org.apache.fineract.portfolio.search.service.SearchUtil;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class DatatableWriteServiceImplTest {
|
||||
|
||||
private static final String DATATABLE_NAME = "dt_test_loan";
|
||||
|
||||
@Mock
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Mock
|
||||
private DatabaseTypeResolver databaseTypeResolver;
|
||||
@Mock
|
||||
private DatabaseSpecificSQLGenerator sqlGenerator;
|
||||
@Mock
|
||||
private PlatformSecurityContext context;
|
||||
@Mock
|
||||
private FromJsonHelper fromJsonHelper;
|
||||
@Mock
|
||||
private GenericDataService genericDataService;
|
||||
@Mock
|
||||
private DatatableCommandFromApiJsonDeserializer fromApiJsonDeserializer;
|
||||
@Mock
|
||||
private ConfigurationDomainService configurationDomainService;
|
||||
@Mock
|
||||
private CodeReadPlatformService codeReadPlatformService;
|
||||
@Mock
|
||||
private DataTableValidator dataTableValidator;
|
||||
@Mock
|
||||
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
|
||||
@Mock
|
||||
private DatatableKeywordGenerator datatableKeywordGenerator;
|
||||
@Mock
|
||||
private SearchUtil searchUtil;
|
||||
@Mock
|
||||
private BusinessEventNotifierService businessEventNotifierService;
|
||||
@Mock
|
||||
private DatatableReadService datatableReadService;
|
||||
@Mock
|
||||
private DatatableUtil datatableUtil;
|
||||
|
||||
@InjectMocks
|
||||
private DatatableWriteServiceImpl underTest;
|
||||
|
||||
@Test
|
||||
void testDeregisterDatatableUsesParameterizedQueries() {
|
||||
underTest.deregisterDatatable(DATATABLE_NAME);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Object[]> paramsCaptor = ArgumentCaptor.forClass(Object[].class);
|
||||
|
||||
verify(jdbcTemplate, atLeastOnce()).update(sqlCaptor.capture(), paramsCaptor.capture());
|
||||
|
||||
List<String> allSql = sqlCaptor.getAllValues();
|
||||
List<Object[]> allParams = paramsCaptor.getAllValues();
|
||||
|
||||
// First call: DELETE FROM m_role_permission WHERE ... IN (?, ?, ?, ?, ?, ?, ?)
|
||||
assertTrue(allSql.get(0).contains("m_role_permission"), "First DELETE should target m_role_permission");
|
||||
assertTrue(allSql.get(0).contains("?"), "SQL should use ? placeholders");
|
||||
assertFalse(allSql.get(0).contains("'" + DATATABLE_NAME), "SQL should not contain concatenated datatable name");
|
||||
assertEquals(7, allParams.get(0).length, "Should have 7 permission code parameters");
|
||||
|
||||
// Second call: DELETE FROM m_permission WHERE code IN (?, ?, ?, ?, ?, ?, ?)
|
||||
assertTrue(allSql.get(1).contains("m_permission"), "Second DELETE should target m_permission");
|
||||
assertTrue(allSql.get(1).contains("?"), "SQL should use ? placeholders");
|
||||
assertEquals(7, allParams.get(1).length, "Should have 7 permission code parameters");
|
||||
|
||||
// Third call: DELETE FROM x_registered_table WHERE registered_table_name = ?
|
||||
assertTrue(allSql.get(2).contains("x_registered_table"), "Third DELETE should target x_registered_table");
|
||||
assertTrue(allSql.get(2).contains("= ?"), "SQL should use ? placeholder");
|
||||
|
||||
// Fourth call: DELETE FROM c_configuration WHERE name = ?
|
||||
assertTrue(allSql.get(3).contains("c_configuration"), "Fourth DELETE should target c_configuration");
|
||||
assertTrue(allSql.get(3).contains("= ?"), "SQL should use ? placeholder");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeleteColumnCodeMappingUsesParameterizedQuery() {
|
||||
List<String> columnNames = new ArrayList<>();
|
||||
columnNames.add("test_column_1");
|
||||
columnNames.add("test_column_2");
|
||||
|
||||
ReflectionTestUtils.invokeMethod(underTest, "deleteColumnCodeMapping", columnNames);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbcTemplate, atLeastOnce()).update(sqlCaptor.capture(), anyString());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
assertTrue(sql.contains("DELETE FROM x_table_column_code_mappings"), "Should delete from x_table_column_code_mappings");
|
||||
assertTrue(sql.contains("column_alias_name = ?"), "Should use ? placeholder for column_alias_name");
|
||||
assertFalse(sql.contains("test_column"), "SQL should not contain concatenated column name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetCodeIdForColumnUsesParameterizedQuery() {
|
||||
String dataTableAlias = "dt_test";
|
||||
String columnName = "status";
|
||||
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), anyString())).thenReturn(42);
|
||||
|
||||
ReflectionTestUtils.invokeMethod(underTest, "getCodeIdForColumn", dataTableAlias, columnName);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> paramCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbcTemplate).queryForObject(sqlCaptor.capture(), eq(Integer.class), paramCaptor.capture());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
String param = paramCaptor.getValue();
|
||||
|
||||
assertTrue(sql.contains("column_alias_name = ?"), "SQL should use ? placeholder");
|
||||
assertFalse(sql.contains(dataTableAlias + "_" + columnName), "SQL should not contain concatenated alias");
|
||||
assertEquals(dataTableAlias + "_" + columnName, param, "Parameter should be the combined alias_name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsDatatableAttachedUsesParameterizedQuery() {
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Long.class), anyString())).thenReturn(0L);
|
||||
|
||||
ReflectionTestUtils.invokeMethod(underTest, "isDatatableAttachedToEntityDatatableCheck", DATATABLE_NAME);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> paramCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbcTemplate).queryForObject(sqlCaptor.capture(), eq(Long.class), paramCaptor.capture());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
assertTrue(sql.contains("x_registered_table_name = ?"), "SQL should use ? placeholder for datatable name");
|
||||
assertFalse(sql.contains("'" + DATATABLE_NAME + "'"), "SQL should not contain concatenated datatable name");
|
||||
assertEquals(DATATABLE_NAME, paramCaptor.getValue(), "Parameter should be the datatable name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testParseDatatableColumnForDropUsesParameterizedFKLookup() {
|
||||
when(databaseTypeResolver.isMySQL()).thenReturn(true);
|
||||
when(sqlGenerator.escape(anyString())).thenAnswer(invocation -> "\"" + invocation.getArgument(0) + "\"");
|
||||
when(jdbcTemplate.queryForObject(anyString(), eq(Integer.class), anyString(), anyString())).thenReturn(0);
|
||||
|
||||
JsonObject column = new JsonObject();
|
||||
column.addProperty("name", "test_col");
|
||||
|
||||
StringBuilder sqlBuilder = new StringBuilder();
|
||||
StringBuilder constrainBuilder = new StringBuilder();
|
||||
List<String> codeMappings = new ArrayList<>();
|
||||
|
||||
ReflectionTestUtils.invokeMethod(underTest, "parseDatatableColumnForDrop", column, sqlBuilder, DATATABLE_NAME, constrainBuilder,
|
||||
codeMappings);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> param1Captor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<String> param2Captor = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbcTemplate).queryForObject(sqlCaptor.capture(), eq(Integer.class), param1Captor.capture(), param2Captor.capture());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
assertTrue(sql.contains("i.TABLE_NAME = ?"), "SQL should use ? placeholder for TABLE_NAME");
|
||||
assertTrue(sql.contains("i.CONSTRAINT_NAME = ?"), "SQL should use ? placeholder for CONSTRAINT_NAME");
|
||||
assertFalse(sql.contains("'" + DATATABLE_NAME + "'"), "SQL should not contain concatenated table name");
|
||||
assertEquals(DATATABLE_NAME, param1Captor.getValue(), "First param should be the table name");
|
||||
assertTrue(param2Captor.getValue().startsWith("fk_"), "Second param should be the FK constraint name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegisterPermissionsUsesParameterizedQuery() {
|
||||
when(sqlGenerator.escape(anyString())).thenAnswer(invocation -> "\"" + invocation.getArgument(0) + "\"");
|
||||
|
||||
ReflectionTestUtils.invokeMethod(underTest, "registerPermissions", DATATABLE_NAME, true);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
ArgumentCaptor<Object[]> paramsCaptor = ArgumentCaptor.forClass(Object[].class);
|
||||
verify(jdbcTemplate, atLeastOnce()).update(sqlCaptor.capture(), paramsCaptor.capture());
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
assertTrue(sql.contains("INSERT INTO m_permission"), "SQL should be an INSERT into m_permission");
|
||||
assertTrue(sql.contains("VALUES ('datatable', ?, ?, ?, ?)"), "SQL should use ? placeholders for values");
|
||||
assertFalse(sql.contains("'" + DATATABLE_NAME + "'"), "SQL should not contain concatenated datatable name");
|
||||
|
||||
List<Object[]> allParams = paramsCaptor.getAllValues();
|
||||
assertEquals(7, allParams.size(), "Should create 7 permission entries");
|
||||
assertEquals("CREATE_" + DATATABLE_NAME, allParams.get(0)[0], "First permission code should be CREATE_<datatable>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRegisterColumnCodeMappingUsesParameterizedQuery() {
|
||||
Map<String, Long> codeMappings = new HashMap<>();
|
||||
codeMappings.put("dt_test_status", 10L);
|
||||
|
||||
ReflectionTestUtils.invokeMethod(underTest, "registerColumnCodeMapping", codeMappings);
|
||||
|
||||
ArgumentCaptor<String> sqlCaptor = ArgumentCaptor.forClass(String.class);
|
||||
verify(jdbcTemplate).update(sqlCaptor.capture(), eq("dt_test_status"), eq(10L));
|
||||
|
||||
String sql = sqlCaptor.getValue();
|
||||
assertTrue(sql.contains("INSERT INTO x_table_column_code_mappings"), "SQL should insert into code mappings table");
|
||||
assertTrue(sql.contains("VALUES (?, ?)"), "SQL should use ? placeholders");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user