FINERACT-2076: SQL query optimization
This commit is contained in:
committed by
Aleksandar Vidakovic
parent
039bca440b
commit
a022a51189
+36
@@ -76,6 +76,8 @@ public class FineractProperties {
|
||||
|
||||
private FineractModulesProperties module;
|
||||
|
||||
private FineractSqlValidationProperties sqlValidation;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractTenantProperties {
|
||||
@@ -533,4 +535,38 @@ public class FineractProperties {
|
||||
public static class FineractInvestorModuleProperties extends AbstractFineractModuleProperties {
|
||||
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSqlValidationProperties {
|
||||
|
||||
private List<FineractSqlValidationPatternProperties> patterns;
|
||||
private List<FineractSqlValidationProfileProperties> profiles;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSqlValidationProfileProperties {
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private List<FineractSqlValidationPatternReferenceProperties> patternRefs;
|
||||
private Boolean enabled = true;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSqlValidationPatternReferenceProperties {
|
||||
|
||||
private String name;
|
||||
private Integer order;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class FineractSqlValidationPatternProperties {
|
||||
|
||||
private String name;
|
||||
private String pattern;
|
||||
}
|
||||
}
|
||||
|
||||
+36
-75
@@ -18,101 +18,62 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.data;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Immutable data object representing pagination parameter values.
|
||||
* </p>
|
||||
*/
|
||||
public final class PaginationParameters {
|
||||
@Builder
|
||||
@Getter
|
||||
public class PaginationParameters {
|
||||
|
||||
private final boolean paged;
|
||||
private final Integer offset;
|
||||
private final Integer limit;
|
||||
private final String orderBy;
|
||||
private final String sortOrder;
|
||||
// TODO: why do we really need this class? SearchParameters seems to provide similar functionality
|
||||
|
||||
public static PaginationParameters instance(Boolean paged, Integer offset, Integer limit, String orderBy, String sortOrder) {
|
||||
if (null == paged) {
|
||||
paged = false;
|
||||
}
|
||||
public static final int DEFAULT_MAX_LIMIT = 200;
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
|
||||
return new PaginationParameters(paged, offset, maxLimitAllowed, orderBy, sortOrder);
|
||||
}
|
||||
|
||||
private PaginationParameters(boolean paged, Integer offset, Integer limit, String orderBy, String sortOrder) {
|
||||
SQLInjectionValidator.validateSQLInput(orderBy);
|
||||
SQLInjectionValidator.validateSQLInput(sortOrder);
|
||||
|
||||
this.paged = paged;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
this.orderBy = orderBy;
|
||||
this.sortOrder = sortOrder;
|
||||
}
|
||||
|
||||
public static Integer getCheckedLimit(final Integer limit) {
|
||||
|
||||
final Integer maxLimitAllowed = 200;
|
||||
// default to max limit first off
|
||||
Integer checkedLimit = maxLimitAllowed;
|
||||
|
||||
if (limit != null && limit > 0) {
|
||||
checkedLimit = limit;
|
||||
} else if (limit != null) {
|
||||
// unlimited case: limit provided and 0 or less
|
||||
checkedLimit = null;
|
||||
}
|
||||
|
||||
return checkedLimit;
|
||||
}
|
||||
|
||||
public boolean isPaged() {
|
||||
return this.paged;
|
||||
}
|
||||
|
||||
public Integer getOffset() {
|
||||
return this.offset;
|
||||
}
|
||||
private boolean paged;
|
||||
private Integer offset;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Integer limit;
|
||||
private String orderBy;
|
||||
private String sortOrder;
|
||||
|
||||
public Integer getLimit() {
|
||||
return this.limit;
|
||||
if (limit == null) {
|
||||
return DEFAULT_MAX_LIMIT;
|
||||
}
|
||||
|
||||
if (limit > 0) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
return null; // unlimited (0 or less)
|
||||
}
|
||||
|
||||
public String getOrderBy() {
|
||||
return this.orderBy;
|
||||
}
|
||||
|
||||
public String getSortOrder() {
|
||||
return this.sortOrder;
|
||||
}
|
||||
|
||||
public boolean isOrderByRequested() {
|
||||
public boolean hasOrderBy() {
|
||||
return StringUtils.isNotBlank(this.orderBy);
|
||||
}
|
||||
|
||||
public boolean isSortOrderProvided() {
|
||||
public boolean hasSortOrder() {
|
||||
return StringUtils.isNotBlank(this.sortOrder);
|
||||
}
|
||||
|
||||
public boolean isLimited() {
|
||||
return this.limit != null && this.limit.intValue() > 0;
|
||||
public boolean hasLimit() {
|
||||
return this.limit != null && this.limit > 0;
|
||||
}
|
||||
|
||||
public boolean isOffset() {
|
||||
public boolean hasOffset() {
|
||||
return this.offset != null;
|
||||
}
|
||||
|
||||
// TODO: following functions are just doing too much in one place; will disappear with type safe queries
|
||||
|
||||
public String orderBySql() {
|
||||
final StringBuilder sql = new StringBuilder();
|
||||
|
||||
if (this.isOrderByRequested()) {
|
||||
if (this.hasOrderBy()) {
|
||||
sql.append(" order by ").append(this.getOrderBy());
|
||||
if (this.isSortOrderProvided()) {
|
||||
if (this.hasSortOrder()) {
|
||||
sql.append(' ').append(this.getSortOrder());
|
||||
}
|
||||
}
|
||||
@@ -121,9 +82,9 @@ public final class PaginationParameters {
|
||||
|
||||
public String limitSql() {
|
||||
final StringBuilder sql = new StringBuilder();
|
||||
if (this.isLimited()) {
|
||||
if (this.hasLimit()) {
|
||||
sql.append(" limit ").append(this.getLimit());
|
||||
if (this.isOffset()) {
|
||||
if (this.hasOffset()) {
|
||||
sql.append(" offset ").append(this.getOffset());
|
||||
}
|
||||
}
|
||||
@@ -132,10 +93,10 @@ public final class PaginationParameters {
|
||||
|
||||
public String paginationSql() {
|
||||
final StringBuilder sqlBuilder = new StringBuilder(50);
|
||||
if (this.isOrderByRequested()) {
|
||||
if (this.hasOrderBy()) {
|
||||
sqlBuilder.append(' ').append(this.orderBySql());
|
||||
}
|
||||
if (this.isLimited()) {
|
||||
if (this.hasLimit()) {
|
||||
sqlBuilder.append(' ').append(this.limitSql());
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -34,7 +34,7 @@ public class PaginationParametersDataValidator {
|
||||
public void validateParameterValues(PaginationParameters parameters, final Set<String> supportedOrdeByValues,
|
||||
final String resourceName) {
|
||||
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
|
||||
if (parameters.isOrderByRequested() && !supportedOrdeByValues.contains(parameters.getOrderBy())) {
|
||||
if (parameters.hasOrderBy() && !supportedOrdeByValues.contains(parameters.getOrderBy())) {
|
||||
final String defaultUserMessage = "The orderBy value '" + parameters.getOrderBy()
|
||||
+ "' is not supported. The supported orderBy values are " + supportedOrdeByValues;
|
||||
final ApiParameterError error = ApiParameterError.parameterError(
|
||||
@@ -43,7 +43,7 @@ public class PaginationParametersDataValidator {
|
||||
dataValidationErrors.add(error);
|
||||
}
|
||||
|
||||
if (parameters.isSortOrderProvided() && !sortOrderValues.contains(parameters.getSortOrder().toUpperCase())) {
|
||||
if (parameters.hasSortOrder() && !sortOrderValues.contains(parameters.getSortOrder().toUpperCase())) {
|
||||
final String defaultUserMessage = "The sortOrder value '" + parameters.getSortOrder()
|
||||
+ "' is not supported. The supported sortOrder values are " + sortOrderValues;
|
||||
final ApiParameterError error = ApiParameterError.parameterError(
|
||||
|
||||
+56
-461
@@ -18,518 +18,113 @@
|
||||
*/
|
||||
package org.apache.fineract.infrastructure.core.service;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public final class SearchParameters {
|
||||
public class SearchParameters {
|
||||
|
||||
private final Long officeId;
|
||||
private final String externalId;
|
||||
private final String name;
|
||||
private final String hierarchy;
|
||||
private final String firstname;
|
||||
private final String lastname;
|
||||
private final String status;
|
||||
private final Integer offset;
|
||||
private final Integer limit;
|
||||
private final String orderBy;
|
||||
private final String sortOrder;
|
||||
private final String accountNo;
|
||||
private final String currencyCode;
|
||||
public static final int DEFAULT_MAX_LIMIT = 200;
|
||||
|
||||
private final Long staffId;
|
||||
private Long officeId;
|
||||
private String externalId;
|
||||
private String name;
|
||||
private String hierarchy;
|
||||
private String firstname;
|
||||
private String lastname;
|
||||
private String status;
|
||||
private Integer offset;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Integer limit;
|
||||
private String orderBy;
|
||||
private String sortOrder;
|
||||
private String accountNo;
|
||||
private String currencyCode;
|
||||
private Long staffId;
|
||||
private Long loanId;
|
||||
private Long savingsId;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Boolean orphansOnly;
|
||||
private Long provisioningEntryId;
|
||||
private Long productId;
|
||||
private Long categoryId;
|
||||
@Getter(AccessLevel.NONE)
|
||||
private Boolean isSelfUser;
|
||||
|
||||
private final Long loanId;
|
||||
public Integer getLimit() {
|
||||
if (limit == null) {
|
||||
return DEFAULT_MAX_LIMIT;
|
||||
}
|
||||
|
||||
private final Long savingsId;
|
||||
private final Boolean orphansOnly;
|
||||
if (limit > 0) {
|
||||
return limit;
|
||||
}
|
||||
|
||||
// Provisioning Entries Search Params
|
||||
private final Long provisioningEntryId;
|
||||
private final Long productId;
|
||||
private final Long categoryId;
|
||||
private final boolean isSelfUser;
|
||||
|
||||
public static SearchParameters from(final Long officeId, final String externalId, final String name, final String hierarchy) {
|
||||
final Long staffId = null;
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
return new SearchParameters(officeId, externalId, name, hierarchy, null, null, null, null, null, null, staffId, accountNo, loanId,
|
||||
savingsId, orphansOnly, isSelfUser);
|
||||
return null; // unlimited (0 or less)
|
||||
}
|
||||
|
||||
public static SearchParameters forClients(final Long officeId, final String externalId, final String displayName,
|
||||
final String firstname, final String lastname, final String status, final String hierarchy, final Integer offset,
|
||||
final Integer limit, final String orderBy, final String sortOrder, final Boolean orphansOnly, final boolean isSelfUser) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
|
||||
return new SearchParameters(officeId, externalId, displayName, hierarchy, firstname, lastname, status, offset, maxLimitAllowed,
|
||||
orderBy, sortOrder, staffId, accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
public Boolean getOrphansOnly() {
|
||||
return Boolean.TRUE.equals(orphansOnly);
|
||||
}
|
||||
|
||||
public static SearchParameters forGroups(final Long officeId, final Long staffId, final String externalId, final String name,
|
||||
final String hierarchy, final Integer offset, final Integer limit, final String orderBy, final String sortOrder,
|
||||
final Boolean orphansOnly) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(officeId, externalId, name, hierarchy, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
|
||||
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
public Boolean getIsSelfUser() {
|
||||
return Boolean.TRUE.equals(isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forOffices(final String orderBy, final String sortOrder) {
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
return new SearchParameters(null, null, null, null, null, null, null, null, orderBy, sortOrder, null, null, null, null, orphansOnly,
|
||||
isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forLoans(final String externalId, final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder, final String accountNo) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
|
||||
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forJournalEntries(final Long officeId, final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder, final Long loanId, final Long savingsId) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(officeId, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null,
|
||||
loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forJournalEntries(final Long officeId, final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder, final Long loanId, final Long savingsId, final String currencyCode) {
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
|
||||
return new SearchParameters(officeId, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null,
|
||||
loanId, savingsId, orphansOnly, currencyCode);
|
||||
}
|
||||
|
||||
public static SearchParameters forPagination(final Integer offset, final Integer limit, final String orderBy, final String sortOrder) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null, loanId,
|
||||
savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forPaginationAndAccountNumberSearch(final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder, final String accountNumber) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, accountNumber,
|
||||
loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forPagination(final Integer offset, final Integer limit) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final String orderBy = null;
|
||||
final String sortOrder = null;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId, null, loanId,
|
||||
savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forProvisioningEntries(final Long provisioningEntryId, final Long officeId, final Long productId,
|
||||
final Long categoryId, final Integer offset, final Integer limit) {
|
||||
return new SearchParameters(provisioningEntryId, officeId, productId, categoryId, offset, limit);
|
||||
}
|
||||
|
||||
public static SearchParameters forSavings(final String externalId, final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
|
||||
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forAccountTransfer(final String externalId, final Integer offset, final Integer limit,
|
||||
final String orderBy, final String sortOrder) {
|
||||
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
|
||||
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forSMSCampaign(final Integer offset, final Integer limit, final String orderBy, final String sortOrder) {
|
||||
|
||||
final String externalId = null;
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
|
||||
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
public static SearchParameters forEmailCampaign(final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder) {
|
||||
|
||||
final String externalId = null;
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
final Long staffId = null;
|
||||
final String accountNo = null;
|
||||
final Long loanId = null;
|
||||
final Long savingsId = null;
|
||||
final Boolean orphansOnly = false;
|
||||
final boolean isSelfUser = false;
|
||||
|
||||
return new SearchParameters(null, externalId, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, staffId,
|
||||
accountNo, loanId, savingsId, orphansOnly, isSelfUser);
|
||||
}
|
||||
|
||||
private SearchParameters(final Long officeId, final String externalId, final String name, final String hierarchy,
|
||||
final String firstname, final String lastname, final String status, final Integer offset, final Integer limit,
|
||||
final String orderBy, final String sortOrder, final Long staffId, final String accountNo, final Long loanId,
|
||||
final Long savingsId, final Boolean orphansOnly, boolean isSelfUser) {
|
||||
this.officeId = officeId;
|
||||
this.externalId = externalId;
|
||||
this.name = name;
|
||||
this.hierarchy = hierarchy;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
this.orderBy = orderBy;
|
||||
this.sortOrder = sortOrder;
|
||||
this.staffId = staffId;
|
||||
this.accountNo = accountNo;
|
||||
this.loanId = loanId;
|
||||
this.savingsId = savingsId;
|
||||
this.orphansOnly = orphansOnly;
|
||||
this.currencyCode = null;
|
||||
this.provisioningEntryId = null;
|
||||
this.productId = null;
|
||||
this.categoryId = null;
|
||||
this.isSelfUser = isSelfUser;
|
||||
this.status = status;
|
||||
|
||||
}
|
||||
|
||||
private SearchParameters(final Long officeId, final String externalId, final String name, final String hierarchy,
|
||||
final String firstname, final String lastname, final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder, final Long staffId, final String accountNo, final Long loanId, final Long savingsId,
|
||||
final Boolean orphansOnly, boolean isSelfUser) {
|
||||
this.officeId = officeId;
|
||||
this.externalId = externalId;
|
||||
this.name = name;
|
||||
this.hierarchy = hierarchy;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
this.orderBy = orderBy;
|
||||
this.sortOrder = sortOrder;
|
||||
this.staffId = staffId;
|
||||
this.accountNo = accountNo;
|
||||
this.loanId = loanId;
|
||||
this.savingsId = savingsId;
|
||||
this.orphansOnly = orphansOnly;
|
||||
this.currencyCode = null;
|
||||
this.provisioningEntryId = null;
|
||||
this.productId = null;
|
||||
this.categoryId = null;
|
||||
this.isSelfUser = isSelfUser;
|
||||
this.status = null;
|
||||
}
|
||||
|
||||
private SearchParameters(final Long provisioningEntryId, final Long officeId, final Long productId, final Long categoryId,
|
||||
final Integer offset, final Integer limit) {
|
||||
this.externalId = null;
|
||||
this.name = null;
|
||||
this.hierarchy = null;
|
||||
this.firstname = null;
|
||||
this.lastname = null;
|
||||
this.orderBy = null;
|
||||
this.sortOrder = null;
|
||||
this.staffId = null;
|
||||
this.accountNo = null;
|
||||
this.loanId = null;
|
||||
this.savingsId = null;
|
||||
this.orphansOnly = null;
|
||||
this.currencyCode = null;
|
||||
this.officeId = officeId;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
this.provisioningEntryId = provisioningEntryId;
|
||||
this.productId = productId;
|
||||
this.categoryId = categoryId;
|
||||
this.isSelfUser = false;
|
||||
this.status = null;
|
||||
|
||||
}
|
||||
|
||||
public SearchParameters(final Long officeId, final String externalId, final String name, final String hierarchy, final String firstname,
|
||||
final String lastname, final Integer offset, final Integer limit, final String orderBy, final String sortOrder,
|
||||
final Long staffId, final String accountNo, final Long loanId, final Long savingsId, final Boolean orphansOnly,
|
||||
final String currencyCode) {
|
||||
this.officeId = officeId;
|
||||
this.externalId = externalId;
|
||||
this.name = name;
|
||||
this.hierarchy = hierarchy;
|
||||
this.firstname = firstname;
|
||||
this.lastname = lastname;
|
||||
this.offset = offset;
|
||||
this.limit = limit;
|
||||
this.orderBy = orderBy;
|
||||
this.sortOrder = sortOrder;
|
||||
this.staffId = staffId;
|
||||
this.accountNo = accountNo;
|
||||
this.loanId = loanId;
|
||||
this.savingsId = savingsId;
|
||||
this.orphansOnly = orphansOnly;
|
||||
this.currencyCode = currencyCode;
|
||||
this.provisioningEntryId = null;
|
||||
this.productId = null;
|
||||
this.categoryId = null;
|
||||
this.isSelfUser = false;
|
||||
this.status = null;
|
||||
|
||||
}
|
||||
|
||||
public boolean isOrderByRequested() {
|
||||
public boolean hasOrderBy() {
|
||||
return StringUtils.isNotBlank(this.orderBy);
|
||||
}
|
||||
|
||||
public boolean isSortOrderProvided() {
|
||||
public boolean hasSortOrder() {
|
||||
return StringUtils.isNotBlank(this.sortOrder);
|
||||
}
|
||||
|
||||
public static Integer getCheckedLimit(final Integer limit) {
|
||||
|
||||
final Integer maxLimitAllowed = 200;
|
||||
// default to max limit first off
|
||||
Integer checkedLimit = maxLimitAllowed;
|
||||
|
||||
if (limit != null && limit > 0) {
|
||||
checkedLimit = limit;
|
||||
} else if (limit != null) {
|
||||
// unlimited case: limit provided and 0 or less
|
||||
checkedLimit = null;
|
||||
}
|
||||
|
||||
return checkedLimit;
|
||||
}
|
||||
|
||||
public boolean isOfficeIdPassed() {
|
||||
public boolean hasOfficeId() {
|
||||
return this.officeId != null && this.officeId != 0;
|
||||
}
|
||||
|
||||
public boolean isCurrencyCodePassed() {
|
||||
return this.currencyCode != null;
|
||||
public boolean hasCurrencyCode() {
|
||||
return StringUtils.isNotBlank(this.currencyCode);
|
||||
}
|
||||
|
||||
public boolean isLimited() {
|
||||
return this.limit != null && this.limit.intValue() > 0;
|
||||
public boolean hasLimit() {
|
||||
return this.limit != null && this.limit > 0;
|
||||
}
|
||||
|
||||
public boolean isOffset() {
|
||||
public boolean hasOffset() {
|
||||
return this.offset != null;
|
||||
}
|
||||
|
||||
public boolean isScopedByOfficeHierarchy() {
|
||||
public boolean hasHierarchy() {
|
||||
return StringUtils.isNotBlank(this.hierarchy);
|
||||
}
|
||||
|
||||
public Long getOfficeId() {
|
||||
return this.officeId;
|
||||
}
|
||||
|
||||
public String getCurrencyCode() {
|
||||
return this.currencyCode;
|
||||
}
|
||||
|
||||
public String getExternalId() {
|
||||
return this.externalId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getHierarchy() {
|
||||
return this.hierarchy;
|
||||
}
|
||||
|
||||
public String getFirstname() {
|
||||
return this.firstname;
|
||||
}
|
||||
|
||||
public String getLastname() {
|
||||
return this.lastname;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public Integer getOffset() {
|
||||
return this.offset;
|
||||
}
|
||||
|
||||
public Integer getLimit() {
|
||||
return this.limit;
|
||||
}
|
||||
|
||||
public String getOrderBy() {
|
||||
return this.orderBy;
|
||||
}
|
||||
|
||||
public String getSortOrder() {
|
||||
return this.sortOrder;
|
||||
}
|
||||
|
||||
public boolean isStaffIdPassed() {
|
||||
public boolean hasStaffId() {
|
||||
return this.staffId != null && this.staffId != 0;
|
||||
}
|
||||
|
||||
public Long getStaffId() {
|
||||
return this.staffId;
|
||||
}
|
||||
|
||||
public String getAccountNo() {
|
||||
return this.accountNo;
|
||||
}
|
||||
|
||||
public boolean isLoanIdPassed() {
|
||||
public boolean hasLoanId() {
|
||||
return this.loanId != null && this.loanId != 0;
|
||||
}
|
||||
|
||||
public boolean isSavingsIdPassed() {
|
||||
public boolean hasSavingsId() {
|
||||
return this.savingsId != null && this.savingsId != 0;
|
||||
}
|
||||
|
||||
public Long getLoanId() {
|
||||
return this.loanId;
|
||||
}
|
||||
|
||||
public Long getSavingsId() {
|
||||
return this.savingsId;
|
||||
}
|
||||
|
||||
public Boolean isOrphansOnly() {
|
||||
if (this.orphansOnly != null) {
|
||||
return this.orphansOnly;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Long getProvisioningEntryId() {
|
||||
return this.provisioningEntryId;
|
||||
}
|
||||
|
||||
public boolean isProvisioningEntryIdPassed() {
|
||||
public boolean hasProvisioningEntryId() {
|
||||
return this.provisioningEntryId != null && this.provisioningEntryId != 0;
|
||||
}
|
||||
|
||||
public Long getProductId() {
|
||||
return this.productId;
|
||||
}
|
||||
|
||||
public boolean isProductIdPassed() {
|
||||
public boolean hasProductId() {
|
||||
return this.productId != null && this.productId != 0;
|
||||
}
|
||||
|
||||
public Long getCategoryId() {
|
||||
return this.categoryId;
|
||||
}
|
||||
|
||||
public boolean isCategoryIdPassed() {
|
||||
public boolean hasCategoryId() {
|
||||
return this.categoryId != null && this.categoryId != 0;
|
||||
}
|
||||
|
||||
public boolean isSelfUser() {
|
||||
return this.isSelfUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* creates an instance of the SearchParameters from a request for the report mailing job run history
|
||||
*
|
||||
* @return SearchParameters object
|
||||
**/
|
||||
public static SearchParameters fromReportMailingJobRunHistory(final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder) {
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
|
||||
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, null, null, null, null,
|
||||
null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* creates an instance of the {@link SearchParameters} from a request for the report mailing job
|
||||
*
|
||||
* @return {@link SearchParameters} object
|
||||
*/
|
||||
public static SearchParameters fromReportMailingJob(final Integer offset, final Integer limit, final String orderBy,
|
||||
final String sortOrder) {
|
||||
final Integer maxLimitAllowed = getCheckedLimit(limit);
|
||||
|
||||
return new SearchParameters(null, null, null, null, null, null, offset, maxLimitAllowed, orderBy, sortOrder, null, null, null, null,
|
||||
null, false);
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
|
||||
|
||||
@Getter
|
||||
public class SqlValidationException extends AbstractPlatformDomainRuleException {
|
||||
|
||||
public SqlValidationException(final String message) {
|
||||
super("error.msg.sql.validation", "SQL validation error: " + message);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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.service;
|
||||
|
||||
import org.apache.fineract.infrastructure.security.exception.SqlValidationException;
|
||||
|
||||
public interface SqlValidator {
|
||||
|
||||
void validate(String statement) throws SqlValidationException;
|
||||
|
||||
void validate(String profile, String statement) throws SqlValidationException;
|
||||
}
|
||||
+8
-11
@@ -31,25 +31,22 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class ColumnValidator {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ColumnValidator.class);
|
||||
private final SqlValidator sqlValidator;
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
@Autowired
|
||||
public ColumnValidator(final JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE", justification = "TODO: fix this!")
|
||||
private void validateColumn(Map<String, Set<String>> tableColumnMap) {
|
||||
Connection connection = null;
|
||||
@@ -87,7 +84,7 @@ public class ColumnValidator {
|
||||
columns.add(rs.getString("column_name"));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
LOG.error("Problem occurred in getTableColumns function", e);
|
||||
log.error("Problem occurred in getTableColumns function", e);
|
||||
}
|
||||
return columns;
|
||||
}
|
||||
@@ -97,7 +94,7 @@ public class ColumnValidator {
|
||||
if (StringUtils.isBlank(condition)) {
|
||||
continue;
|
||||
}
|
||||
SQLInjectionValidator.validateSQLInput(condition);
|
||||
sqlValidator.validate("column", condition);
|
||||
List<String> operator = new ArrayList<>(Arrays.asList("=", ">", "<", "> =", "< =", "! =", "!=", ">=", "<="));
|
||||
condition = condition.trim().replace("( ", "(").replace(" )", ")").toLowerCase();
|
||||
for (String op : operator) {
|
||||
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 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 jakarta.annotation.PostConstruct;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||
import org.apache.fineract.infrastructure.security.exception.SqlValidationException;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class DefaultSqlValidator implements SqlValidator {
|
||||
|
||||
private final FineractProperties properties;
|
||||
|
||||
private Map<String, Pattern> patterns = new LinkedHashMap<>();
|
||||
private Map<String, FineractProperties.FineractSqlValidationProfileProperties> profiles = new LinkedHashMap<>();
|
||||
|
||||
private static final String MAIN_PROFILE = "main";
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
properties.getSqlValidation().getPatterns().forEach(pattern -> {
|
||||
log.info("Setup SQL validation pattern: {}", pattern.getName());
|
||||
|
||||
patterns.put(pattern.getName(), Pattern.compile(pattern.getPattern(), Pattern.DOTALL));
|
||||
});
|
||||
properties.getSqlValidation().getProfiles().forEach(profile -> {
|
||||
log.info("Setup SQL validation profile: {}", profile.getName());
|
||||
|
||||
profile.getPatternRefs()
|
||||
.sort(Comparator.comparing(FineractProperties.FineractSqlValidationPatternReferenceProperties::getOrder));
|
||||
|
||||
profiles.put(profile.getName(), profile);
|
||||
});
|
||||
|
||||
// consistency checks
|
||||
|
||||
if (!profiles.containsKey(MAIN_PROFILE)) {
|
||||
throw new IllegalStateException(
|
||||
"SQL validation profile 'main' missing. This validation profile is the default fallback and has to be provided. NOTE: YOU CANNOT DISABLE SQL VALIDATION!!!");
|
||||
}
|
||||
|
||||
// the default profile needs at least one pattern reference
|
||||
if (profiles.get(MAIN_PROFILE).getPatternRefs().isEmpty()) {
|
||||
throw new IllegalStateException(
|
||||
"SQL Validation pattern references in profile 'main' are empty. Please make sure there is at least one reference available. NOTE: YOU CANNOT DISABLE SQL VALIDATION!!!");
|
||||
}
|
||||
|
||||
// the default profile needs to be enabled
|
||||
profiles.get(MAIN_PROFILE).setEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(final String statement) throws SqlValidationException {
|
||||
validate(MAIN_PROFILE, statement);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(final String profile, final String statement) throws SqlValidationException {
|
||||
if (StringUtils.isBlank(statement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (var ref : profiles.getOrDefault(profile, profiles.get(MAIN_PROFILE)).getPatternRefs()) {
|
||||
Matcher matcher = patterns.get(ref.getName()).matcher(statement);
|
||||
|
||||
if (matcher.matches()) {
|
||||
log.warn("SQL validation error: >> {} <<", statement);
|
||||
throw new SqlValidationException(String.format("invalid SQL statement (detected '%s' pattern)", ref.getName()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -28,8 +28,8 @@ import java.util.regex.Pattern;
|
||||
* Utility to assemble the WHERE clause of an SQL query without the risk of SQL injection.
|
||||
*
|
||||
* <p>
|
||||
* When using this utility instead of manually assembling SQL queries, then {@link SQLInjectionValidator} should not be
|
||||
* required anymore. (Correctly using this means only ever passing completely fixed String literals to .)
|
||||
* When using this utility instead of manually assembling SQL queries, then {@link SqlValidator} should not be required
|
||||
* anymore. (Correctly using this means only ever passing completely fixed String literals to .)
|
||||
*
|
||||
* @author Michael Vorburger <mike@vorburger.ch>
|
||||
*/
|
||||
|
||||
+1
@@ -21,6 +21,7 @@ package org.apache.fineract.infrastructure.security.utils;
|
||||
import java.sql.SQLException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
|
||||
@Deprecated
|
||||
public class SQLInjectionException extends PlatformApiDataValidationException {
|
||||
|
||||
public SQLInjectionException() {
|
||||
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
/**
|
||||
* 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.List;
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public final class SQLInjectionValidator {
|
||||
|
||||
private SQLInjectionValidator() {
|
||||
|
||||
}
|
||||
|
||||
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 = { "--", "({", "/*", "#" };
|
||||
|
||||
private static final String SQL_PATTERN = "[a-zA-Z_=,\\-:'!><.?\"`% ()0-9*\n\r]*";
|
||||
|
||||
// TODO: see here https://rails-sqli.org for and
|
||||
// https://larrysteinle.com/2011/02/20/use-regular-expressions-to-detect-sql-code-injection more examples
|
||||
private static final List<String> INJECTION_PATTERNS = List.of("(?i).*[or|and]\s*[\"']?-1[\"']?\\s*(-*).*",
|
||||
"(?i).*\\s+[\"']?(\\d+)[\"']?\\s*=\\s*[\"']?(\\1)[\"']?\\s*(-*).*");
|
||||
|
||||
public static void validateSQLInput(final String sqlSearch) {
|
||||
if (StringUtils.isBlank(sqlSearch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lowerCaseSQL = sqlSearch.toLowerCase();
|
||||
List<String[]> commandsList = List.of(DDL_COMMANDS, DML_COMMANDS, COMMENTS);
|
||||
validateSQLCommands(lowerCaseSQL, commandsList, String::contains);
|
||||
|
||||
patternMatchSqlInjection(sqlSearch, lowerCaseSQL);
|
||||
}
|
||||
|
||||
public static void validateAdhocQuery(final String sqlSearch) {
|
||||
if (StringUtils.isBlank(sqlSearch)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String lowerCaseSQL = sqlSearch.toLowerCase().trim();
|
||||
validateSQLCommand(lowerCaseSQL, DDL_COMMANDS, String::startsWith);
|
||||
validateSQLCommand(lowerCaseSQL, COMMENTS, String::contains);
|
||||
|
||||
// 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();
|
||||
List<String[]> commandsList = List.of(DDL_COMMANDS, DML_COMMANDS, COMMENTS);
|
||||
validateSQLCommands(lowerCaseSQL, commandsList, String::equals);
|
||||
|
||||
// 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.replaceAll("\\s*=\\s*", "=");
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
for (String injectionPattern : INJECTION_PATTERNS) {
|
||||
Pattern pattern = Pattern.compile(injectionPattern);
|
||||
Matcher matcher = pattern.matcher(sqlSearch);
|
||||
if (matcher.matches()) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile(SQL_PATTERN);
|
||||
Matcher matcher = pattern.matcher(sqlSearch);
|
||||
if (!matcher.matches()) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSQLCommand(String lowerCaseSQL, String[] commands, SQLCommandCondition condition) {
|
||||
for (String command : commands) {
|
||||
if (condition.checkCondition(lowerCaseSQL, command)) {
|
||||
throw new SQLInjectionException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateSQLCommands(String lowerCaseSQL, List<String[]> commandsList, SQLCommandCondition condition) {
|
||||
for (String[] commands : commandsList) {
|
||||
validateSQLCommand(lowerCaseSQL, commands, condition);
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-10
@@ -29,19 +29,23 @@ import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class AdHocQueryDataValidator {
|
||||
|
||||
private final SqlValidator sqlValidator;
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private static final Set<String> AD_HOC_SEARCH_QUERY_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(
|
||||
AdHocQuerySearchConstants.entitiesParamName, AdHocQuerySearchConstants.loanStatusParamName,
|
||||
@@ -66,11 +70,6 @@ public class AdHocQueryDataValidator {
|
||||
AdHocQuerySearchConstants.arrearsLoanStatusOption, AdHocQuerySearchConstants.closedLoanStatusOption,
|
||||
AdHocQuerySearchConstants.writeoffLoanStatusOption };
|
||||
|
||||
@Autowired
|
||||
public AdHocQueryDataValidator(final FromJsonHelper fromApiJsonHelper) {
|
||||
this.fromApiJsonHelper = fromApiJsonHelper;
|
||||
}
|
||||
|
||||
public void validateAdHocQueryParameters(final String json) {
|
||||
|
||||
if (StringUtils.isBlank(json)) {
|
||||
@@ -229,7 +228,7 @@ public class AdHocQueryDataValidator {
|
||||
String loanDateOption = null;
|
||||
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.loanDateOptionParamName, element)) {
|
||||
loanDateOption = this.fromApiJsonHelper.extractStringNamed(AdHocQuerySearchConstants.loanDateOptionParamName, element);
|
||||
SQLInjectionValidator.validateSQLInput(loanDateOption);
|
||||
sqlValidator.validate(loanDateOption);
|
||||
}
|
||||
|
||||
LocalDate loanFromDate = null;
|
||||
@@ -252,7 +251,7 @@ public class AdHocQueryDataValidator {
|
||||
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element)) {
|
||||
outStandingAmountPercentageCondition = this.fromApiJsonHelper
|
||||
.extractStringNamed(AdHocQuerySearchConstants.outStandingAmountPercentageConditionParamName, element);
|
||||
SQLInjectionValidator.validateSQLInput(outStandingAmountPercentageCondition);
|
||||
sqlValidator.validate(outStandingAmountPercentageCondition);
|
||||
}
|
||||
|
||||
BigDecimal minOutStandingAmountPercentage = null;
|
||||
@@ -283,7 +282,7 @@ public class AdHocQueryDataValidator {
|
||||
if (this.fromApiJsonHelper.parameterExists(AdHocQuerySearchConstants.outstandingAmountConditionParamName, element)) {
|
||||
outstandingAmountCondition = this.fromApiJsonHelper
|
||||
.extractStringNamed(AdHocQuerySearchConstants.outstandingAmountConditionParamName, element);
|
||||
SQLInjectionValidator.validateSQLInput(outstandingAmountCondition);
|
||||
sqlValidator.validate(outstandingAmountCondition);
|
||||
}
|
||||
|
||||
BigDecimal minOutstandingAmount = null;
|
||||
|
||||
+32
-27
@@ -39,6 +39,8 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
|
||||
@@ -50,30 +52,34 @@ import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecific
|
||||
import org.apache.fineract.infrastructure.core.service.database.JdbcJavaType;
|
||||
import org.apache.fineract.infrastructure.core.service.database.SqlOperator;
|
||||
import org.apache.fineract.infrastructure.dataqueries.data.ResultsetColumnHeaderData;
|
||||
import org.apache.fineract.infrastructure.security.utils.SQLInjectionValidator;
|
||||
import org.apache.fineract.infrastructure.security.service.SqlValidator;
|
||||
import org.apache.fineract.portfolio.search.data.ColumnFilterData;
|
||||
import org.apache.fineract.portfolio.search.data.FilterData;
|
||||
import org.springframework.jdbc.support.rowset.SqlRowSet;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
public final class SearchUtil {
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Component
|
||||
public class SearchUtil {
|
||||
|
||||
public static final int DEFAULT_PAGE_SIZE = 50;
|
||||
|
||||
private static final JsonParserHelper helper = new JsonParserHelper();
|
||||
|
||||
private SearchUtil() {}
|
||||
private final SqlValidator sqlValidator;
|
||||
|
||||
@NotNull
|
||||
public static Map<String, ResultsetColumnHeaderData> mapHeadersToName(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders) {
|
||||
public Map<String, ResultsetColumnHeaderData> mapHeadersToName(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders) {
|
||||
return columnHeaders.stream().collect(Collectors.toMap(ResultsetColumnHeaderData::getColumnName, e -> e));
|
||||
}
|
||||
|
||||
public static ResultsetColumnHeaderData findFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
|
||||
public ResultsetColumnHeaderData findFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
|
||||
@NotNull Predicate<ResultsetColumnHeaderData> filter) {
|
||||
return columnHeaders.stream().filter(filter).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static ResultsetColumnHeaderData getFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
|
||||
public ResultsetColumnHeaderData getFiltered(@NotNull Collection<ResultsetColumnHeaderData> columnHeaders,
|
||||
@NotNull Predicate<ResultsetColumnHeaderData> filter) {
|
||||
ResultsetColumnHeaderData filtered = findFiltered(columnHeaders, filter);
|
||||
if (filtered == null) {
|
||||
@@ -82,8 +88,8 @@ public final class SearchUtil {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
public static void extractJsonResult(@NotNull SqlRowSet rowSet, @NotNull List<String> selectColumns,
|
||||
@NotNull List<String> resultColumns, @NotNull List<JsonObject> results) {
|
||||
public void extractJsonResult(@NotNull SqlRowSet rowSet, @NotNull List<String> selectColumns, @NotNull List<String> resultColumns,
|
||||
@NotNull List<JsonObject> results) {
|
||||
JsonObject json = new JsonObject();
|
||||
for (int i = 0; i < selectColumns.size(); i++) {
|
||||
Object rowValue = rowSet.getObject(selectColumns.get(i));
|
||||
@@ -115,15 +121,15 @@ public final class SearchUtil {
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<String> validateToJdbcColumnNames(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
|
||||
public List<String> validateToJdbcColumnNames(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
|
||||
boolean allowEmpty) {
|
||||
List<ResultsetColumnHeaderData> columnHeaders = validateToJdbcColumns(columns, headersByName, allowEmpty);
|
||||
return columnHeaders.stream().map(e -> e == null ? null : e.getColumnName()).toList();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public static List<ResultsetColumnHeaderData> validateToJdbcColumns(List<String> columns,
|
||||
Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||
public List<ResultsetColumnHeaderData> validateToJdbcColumns(List<String> columns, Map<String, ResultsetColumnHeaderData> headersByName,
|
||||
boolean allowEmpty) {
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
|
||||
List<ResultsetColumnHeaderData> result = new ArrayList<>();
|
||||
@@ -140,13 +146,13 @@ public final class SearchUtil {
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String validateToJdbcColumnName(String column, Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||
public String validateToJdbcColumnName(String column, Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||
ResultsetColumnHeaderData columnHeader = validateToJdbcColumn(column, headersByName, allowEmpty);
|
||||
return columnHeader == null ? null : columnHeader.getColumnName();
|
||||
}
|
||||
|
||||
public static ResultsetColumnHeaderData validateToJdbcColumn(String column,
|
||||
@NotNull Map<String, ResultsetColumnHeaderData> headersByName, boolean allowEmpty) {
|
||||
public ResultsetColumnHeaderData validateToJdbcColumn(String column, @NotNull Map<String, ResultsetColumnHeaderData> headersByName,
|
||||
boolean allowEmpty) {
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
ResultsetColumnHeaderData columnHeader = validateToJdbcColumnImpl(column, headersByName, errors, allowEmpty);
|
||||
if (!errors.isEmpty()) {
|
||||
@@ -155,8 +161,8 @@ public final class SearchUtil {
|
||||
return columnHeader;
|
||||
}
|
||||
|
||||
private static ResultsetColumnHeaderData validateToJdbcColumnImpl(String column,
|
||||
@NotNull Map<String, ResultsetColumnHeaderData> headersByName, @NotNull List<ApiParameterError> errors, boolean allowEmpty) {
|
||||
private ResultsetColumnHeaderData validateToJdbcColumnImpl(String column, @NotNull Map<String, ResultsetColumnHeaderData> headersByName,
|
||||
@NotNull List<ApiParameterError> errors, boolean allowEmpty) {
|
||||
if (!allowEmpty && column == null) {
|
||||
errors.add(parameterErrorWithValue("error.msg.column.empty", "Column filter is empty", API_PARAM_COLUMN, null));
|
||||
}
|
||||
@@ -173,9 +179,9 @@ public final class SearchUtil {
|
||||
return columnHeader;
|
||||
}
|
||||
|
||||
public static boolean buildQueryCondition(List<ColumnFilterData> columnFilters, @NotNull StringBuilder where,
|
||||
@NotNull List<Object> params, String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat,
|
||||
String dateTimeFormat, Locale locale, boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
public boolean buildQueryCondition(List<ColumnFilterData> columnFilters, @NotNull StringBuilder where, @NotNull List<Object> params,
|
||||
String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat, String dateTimeFormat, Locale locale,
|
||||
boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
if (columnFilters == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -192,7 +198,7 @@ public final class SearchUtil {
|
||||
return added;
|
||||
}
|
||||
|
||||
public static boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List<Object> params,
|
||||
public boolean buildFilterCondition(ColumnFilterData columnFilter, @NotNull StringBuilder where, @NotNull List<Object> params,
|
||||
String alias, Map<String, ResultsetColumnHeaderData> headersByName, String dateFormat, String dateTimeFormat, Locale locale,
|
||||
boolean embedded, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
String columnName = columnFilter.getColumn();
|
||||
@@ -221,9 +227,8 @@ public final class SearchUtil {
|
||||
return size > 0;
|
||||
}
|
||||
|
||||
public static void buildCondition(@NotNull String definition, JdbcJavaType columnType, @NotNull SqlOperator operator,
|
||||
List<Object> values, @NotNull StringBuilder where, @NotNull List<Object> params, String alias,
|
||||
@NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
public void buildCondition(@NotNull String definition, JdbcJavaType columnType, @NotNull SqlOperator operator, List<Object> values,
|
||||
@NotNull StringBuilder where, @NotNull List<Object> params, String alias, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
int paramCount = values == null ? 0 : values.size();
|
||||
where.append(operator.formatPlaceholder(sqlGenerator, definition, paramCount, alias));
|
||||
if (values != null) {
|
||||
@@ -231,13 +236,13 @@ public final class SearchUtil {
|
||||
}
|
||||
}
|
||||
|
||||
public static Object parseJdbcColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
|
||||
public Object parseJdbcColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
|
||||
String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
return columnHeader.getColumnType().toJdbcValue(sqlGenerator.getDialect(),
|
||||
parseColumnValue(columnHeader, columnValue, dateFormat, dateTimeFormat, locale, strict, sqlGenerator), false);
|
||||
}
|
||||
|
||||
public static Object parseColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
|
||||
public Object parseColumnValue(@NotNull ResultsetColumnHeaderData columnHeader, String columnValue, String dateFormat,
|
||||
String dateTimeFormat, Locale locale, boolean strict, @NotNull DatabaseSpecificSQLGenerator sqlGenerator) {
|
||||
JdbcJavaType colType = columnHeader.getColumnType();
|
||||
if (!colType.isStringType() || !columnHeader.isMandatory()) {
|
||||
@@ -254,7 +259,7 @@ public final class SearchUtil {
|
||||
return columnValue;
|
||||
}
|
||||
if (strict) {
|
||||
SQLInjectionValidator.validateDynamicQuery(columnValue);
|
||||
sqlValidator.validate(columnValue);
|
||||
}
|
||||
|
||||
if (columnHeader.hasColumnValues()) {
|
||||
@@ -314,7 +319,7 @@ public final class SearchUtil {
|
||||
return columnValue;
|
||||
}
|
||||
|
||||
public static String camelToSnake(final String camelStr) {
|
||||
public String camelToSnake(final String camelStr) {
|
||||
return camelStr == null ? null
|
||||
: camelStr.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2").replaceAll("([a-z])([A-Z])", "$1_$2").toLowerCase();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user