FINERACT-2314: IP tracking (#4825)
Co-authored-by: Juan Pablo Alvarez Hernandez <work_jpa@hotmailcom>
This commit is contained in:
co-authored by
Juan Pablo Alvarez Hernandez
parent
39050aa01f
commit
b0ca0a24cc
@@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
|||||||
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.IpAddressUtils;
|
||||||
import org.apache.fineract.useradministration.domain.AppUser;
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -137,6 +138,9 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
|
|||||||
@Column(name = "loan_external_id", length = 100)
|
@Column(name = "loan_external_id", length = 100)
|
||||||
private ExternalId loanExternalId;
|
private ExternalId loanExternalId;
|
||||||
|
|
||||||
|
@Column(name = "client_ip", nullable = true)
|
||||||
|
private String clientIp;
|
||||||
|
|
||||||
@Column(name = "is_sanitized", nullable = false)
|
@Column(name = "is_sanitized", nullable = false)
|
||||||
private boolean sanitized;
|
private boolean sanitized;
|
||||||
|
|
||||||
@@ -162,6 +166,7 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
|
|||||||
.transactionId(command.getTransactionId()) //
|
.transactionId(command.getTransactionId()) //
|
||||||
.creditBureauId(command.getCreditBureauId()) //
|
.creditBureauId(command.getCreditBureauId()) //
|
||||||
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
|
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
|
||||||
|
.clientIp(IpAddressUtils.getClientIp()) //
|
||||||
.loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); //
|
.loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); //
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-1
@@ -596,5 +596,4 @@ public final class JsonCommand {
|
|||||||
public void checkForUnsupportedParameters(final Type typeOfMap, final String json, final Set<String> requestDataParameters) {
|
public void checkForUnsupportedParameters(final Type typeOfMap, final String json, final Set<String> requestDataParameters) {
|
||||||
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, requestDataParameters);
|
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, requestDataParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+9
@@ -50,6 +50,8 @@ public class FineractProperties {
|
|||||||
|
|
||||||
private FineractCorrelationProperties correlation;
|
private FineractCorrelationProperties correlation;
|
||||||
|
|
||||||
|
private FineractIpTrackingProperties ipTracking;
|
||||||
|
|
||||||
private FineractPartitionedJob partitionedJob;
|
private FineractPartitionedJob partitionedJob;
|
||||||
|
|
||||||
private FineractRemoteJobMessageHandlerProperties remoteJobMessageHandler;
|
private FineractRemoteJobMessageHandlerProperties remoteJobMessageHandler;
|
||||||
@@ -153,6 +155,13 @@ public class FineractProperties {
|
|||||||
private String headerName;
|
private String headerName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public static class FineractIpTrackingProperties {
|
||||||
|
|
||||||
|
private boolean enabled;
|
||||||
|
}
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
public static class FineractPartitionedJob {
|
public static class FineractPartitionedJob {
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
/**
|
||||||
|
* 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.core.filters;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class CallerIpTrackingFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final FineractProperties fineractProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common headers used to get client IP from different proxies.
|
||||||
|
*
|
||||||
|
* "X-Forwarded-For", // Standard header used by proxies "Proxy-Client-IP", // Used by some Apache proxies
|
||||||
|
* "WL-Proxy-Client-IP", // Used by WebLogic "HTTP_X_FORWARDED_FOR", // Alternative to X-Forwarded-For
|
||||||
|
* "HTTP_X_FORWARDED", // Variation of X-Forwarded "HTTP_X_CLUSTER_CLIENT_IP", // Used in clustered environments
|
||||||
|
* "HTTP_CLIENT_IP", // Fallback, less common "HTTP_FORWARDED_FOR", // Less standard, used in some setups
|
||||||
|
* "HTTP_FORWARDED", // Standardized header (RFC 7239) that can include client IP, proxy info, and protocol
|
||||||
|
* "HTTP_VIA", // Shows intermediate proxies "REMOTE_ADDR" // Server's perceived client IP
|
||||||
|
*/
|
||||||
|
|
||||||
|
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
|
||||||
|
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR",
|
||||||
|
"HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" };
|
||||||
|
|
||||||
|
public String getClientIpAddress(HttpServletRequest request) {
|
||||||
|
for (String header : IP_HEADER_CANDIDATES) {
|
||||||
|
String ip = request.getHeader(header);
|
||||||
|
if (ip != null && ip.length() != 0 && !ip.isEmpty()) {
|
||||||
|
log.trace("CALLER IP : {}", ip);
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.trace("getRemoteAddr method : {}", request.getRemoteAddr());
|
||||||
|
return request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
if (fineractProperties.getIpTracking().isEnabled()) {
|
||||||
|
handleClientIp(request, response, filterChain);
|
||||||
|
} else {
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClientIp(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
try {
|
||||||
|
String clientIpAddress = getClientIpAddress(request);
|
||||||
|
if (StringUtils.isNotBlank(clientIpAddress)) {
|
||||||
|
log.trace("Found Client IP in header : {}", clientIpAddress);
|
||||||
|
request.setAttribute("IP", clientIpAddress);
|
||||||
|
}
|
||||||
|
filterChain.doFilter(request, response);
|
||||||
|
} finally {
|
||||||
|
request.setAttribute("IP", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean isAsyncDispatch(final HttpServletRequest request) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean shouldNotFilterErrorDispatch() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 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.core.service;
|
||||||
|
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
public final class IpAddressUtils {
|
||||||
|
|
||||||
|
private IpAddressUtils() {}
|
||||||
|
|
||||||
|
public static String getClientIp() {
|
||||||
|
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
String clientIp = "";
|
||||||
|
if (attrs != null) {
|
||||||
|
Object ipAttr = attrs.getRequest().getAttribute("IP");
|
||||||
|
if (ipAttr != null) {
|
||||||
|
clientIp = ipAttr.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clientIp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
@@ -56,6 +56,7 @@ public class IdempotencyCommandProcessFailedExceptionTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testInconsistentStatus() {
|
public void testInconsistentStatus() {
|
||||||
|
|
||||||
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
|
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
|
||||||
CommandWrapper command = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
CommandWrapper command = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
|
||||||
null, null, null, null, null, null);
|
null, null, null, null, null, null);
|
||||||
@@ -64,5 +65,6 @@ public class IdempotencyCommandProcessFailedExceptionTest {
|
|||||||
Response result = mapper.toResponse(exception);
|
Response result = mapper.toResponse(exception);
|
||||||
assertEquals(500, result.getStatus());
|
assertEquals(500, result.getStatus());
|
||||||
assertEquals("true", result.getHeaderString(IDEMPOTENT_CACHE_HEADER));
|
assertEquals("true", result.getHeaderString(IDEMPOTENT_CACHE_HEADER));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,4 +56,5 @@ public final class AuditData implements Serializable {
|
|||||||
private final Long clientId;
|
private final Long clientId;
|
||||||
private final Long loanId;
|
private final Long loanId;
|
||||||
private final String url;
|
private final String url;
|
||||||
|
private final String ip;
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -110,11 +110,12 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
|
|||||||
+ "ck.username as checker, aud.checked_on_date as checkedOnDate, aud.checked_on_date_utc as checkedOnDateUTC, ev.enum_message_property as processingResult "
|
+ "ck.username as checker, aud.checked_on_date as checkedOnDate, aud.checked_on_date_utc as checkedOnDateUTC, ev.enum_message_property as processingResult "
|
||||||
+ commandAsJsonString + ", "
|
+ commandAsJsonString + ", "
|
||||||
+ " o.name as officeName, gl.level_name as groupLevelName, g.display_name as groupName, c.display_name as clientName, "
|
+ " o.name as officeName, gl.level_name as groupLevelName, g.display_name as groupName, c.display_name as clientName, "
|
||||||
+ " l.account_no as loanAccountNo, s.account_no as savingsAccountNo " + " from m_portfolio_command_source aud "
|
+ " l.account_no as loanAccountNo, s.account_no as savingsAccountNo , aud.client_ip as ip "
|
||||||
+ " left join m_appuser mk on mk.id = aud.maker_id" + " left join m_appuser ck on ck.id = aud.checker_id"
|
+ " from m_portfolio_command_source aud " + " left join m_appuser mk on mk.id = aud.maker_id"
|
||||||
+ " left join m_office o on o.id = aud.office_id" + " left join m_group g on g.id = aud.group_id"
|
+ " left join m_appuser ck on ck.id = aud.checker_id" + " left join m_office o on o.id = aud.office_id"
|
||||||
+ " left join m_group_level gl on gl.id = g.level_id" + " left join m_client c on c.id = aud.client_id"
|
+ " left join m_group g on g.id = aud.group_id" + " left join m_group_level gl on gl.id = g.level_id"
|
||||||
+ " left join m_loan l on l.id = aud.loan_id" + " left join m_savings_account s on s.id = aud.savings_account_id"
|
+ " left join m_client c on c.id = aud.client_id" + " left join m_loan l on l.id = aud.loan_id"
|
||||||
|
+ " left join m_savings_account s on s.id = aud.savings_account_id"
|
||||||
+ " left join r_enum_value ev on ev.enum_name = 'status' and ev.enum_id = aud.status";
|
+ " left join r_enum_value ev on ev.enum_name = 'status' and ev.enum_id = aud.status";
|
||||||
|
|
||||||
// data scoping: head office (hierarchy = ".") can see all audit
|
// data scoping: head office (hierarchy = ".") can see all audit
|
||||||
@@ -158,13 +159,14 @@ public class AuditReadPlatformServiceImpl implements AuditReadPlatformService {
|
|||||||
final String clientName = rs.getString("clientName");
|
final String clientName = rs.getString("clientName");
|
||||||
final String loanAccountNo = rs.getString("loanAccountNo");
|
final String loanAccountNo = rs.getString("loanAccountNo");
|
||||||
final String savingsAccountNo = rs.getString("savingsAccountNo");
|
final String savingsAccountNo = rs.getString("savingsAccountNo");
|
||||||
|
final String ip = rs.getString("ip");
|
||||||
|
|
||||||
ZonedDateTime madeOnDate = madeOnDateUTC != null ? madeOnDateUTC.toZonedDateTime() : madeOnDateTenant;
|
ZonedDateTime madeOnDate = madeOnDateUTC != null ? madeOnDateUTC.toZonedDateTime() : madeOnDateTenant;
|
||||||
ZonedDateTime checkedOnDate = checkedOnDateUTC != null ? checkedOnDateUTC.toZonedDateTime() : checkedOnDateTenant;
|
ZonedDateTime checkedOnDate = checkedOnDateUTC != null ? checkedOnDateUTC.toZonedDateTime() : checkedOnDateTenant;
|
||||||
|
|
||||||
return new AuditData(id, actionName, entityName, resourceId, subresourceId, maker, madeOnDate, checker, checkedOnDate,
|
return new AuditData(id, actionName, entityName, resourceId, subresourceId, maker, madeOnDate, checker, checkedOnDate,
|
||||||
processingResult, commandAsJson, officeName, groupLevelName, groupName, clientName, loanAccountNo, savingsAccountNo,
|
processingResult, commandAsJson, officeName, groupLevelName, groupName, clientName, loanAccountNo, savingsAccountNo,
|
||||||
clientId, loanId, resourceGetUrl);
|
clientId, loanId, resourceGetUrl, ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+8
-1
@@ -32,6 +32,7 @@ import org.apache.fineract.infrastructure.businessdate.service.BusinessDateReadP
|
|||||||
import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService;
|
import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService;
|
||||||
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
|
||||||
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
|
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
|
||||||
|
import org.apache.fineract.infrastructure.core.filters.CallerIpTrackingFilter;
|
||||||
import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter;
|
import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter;
|
||||||
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
|
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
|
||||||
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper;
|
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper;
|
||||||
@@ -172,7 +173,9 @@ public class SecurityConfig {
|
|||||||
} else {
|
} else {
|
||||||
http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); //
|
http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); //
|
||||||
}
|
}
|
||||||
|
if (fineractProperties.getIpTracking().isEnabled()) {
|
||||||
|
http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class);
|
||||||
|
}
|
||||||
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
|
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
|
||||||
http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class);
|
http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class);
|
||||||
} else {
|
} else {
|
||||||
@@ -219,6 +222,10 @@ public class SecurityConfig {
|
|||||||
return new CorrelationHeaderFilter(fineractProperties, mdcWrapper);
|
return new CorrelationHeaderFilter(fineractProperties, mdcWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CallerIpTrackingFilter callerIpTrackingFilter() {
|
||||||
|
return new CallerIpTrackingFilter(fineractProperties);
|
||||||
|
}
|
||||||
|
|
||||||
public TenantAwareBasicAuthenticationFilter tenantAwareBasicAuthenticationFilter() throws Exception {
|
public TenantAwareBasicAuthenticationFilter tenantAwareBasicAuthenticationFilter() throws Exception {
|
||||||
TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(),
|
TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(),
|
||||||
basicAuthenticationEntryPoint(), toApiJsonSerializer, configurationDomainService, cacheWritePlatformService,
|
basicAuthenticationEntryPoint(), toApiJsonSerializer, configurationDomainService, cacheWritePlatformService,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ fineract.correlation.enabled=${FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED:fals
|
|||||||
fineract.correlation.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME:X-Correlation-ID}
|
fineract.correlation.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME:X-Correlation-ID}
|
||||||
|
|
||||||
fineract.job.stuck-retry-threshold=${FINERACT_JOB_STUCK_RETRY_THRESHOLD:5}
|
fineract.job.stuck-retry-threshold=${FINERACT_JOB_STUCK_RETRY_THRESHOLD:5}
|
||||||
|
fineract.ip-tracking.enabled=${FINERACT_CLIENT_IP_TRACKING_ENABLED:false}
|
||||||
fineract.job.loan-cob-enabled=${FINERACT_JOB_LOAN_COB_ENABLED:true}
|
fineract.job.loan-cob-enabled=${FINERACT_JOB_LOAN_COB_ENABLED:true}
|
||||||
|
|
||||||
fineract.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB
|
fineract.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB
|
||||||
|
|||||||
@@ -211,4 +211,5 @@
|
|||||||
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
|
<include file="parts/0190_buy_down_fee_amortization.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" relativeToChangelogFile="true" />
|
<include file="parts/0191_add_LoanApprovedAmountChangedBusinessEvent.xml" relativeToChangelogFile="true" />
|
||||||
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
|
<include file="parts/0192_create_loan_approved_amount_history.xml" relativeToChangelogFile="true" />
|
||||||
|
<include file="parts/0193_add_column_ip.xml" relativeToChangelogFile="true" />
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
-->
|
||||||
|
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
|
||||||
|
<changeSet author="fineract" id="1">
|
||||||
|
<addColumn tableName="m_portfolio_command_source">
|
||||||
|
<column name="client_ip" type="varchar(100)">
|
||||||
|
<constraints nullable="true"/>
|
||||||
|
</column>
|
||||||
|
</addColumn>
|
||||||
|
</changeSet>
|
||||||
|
</databaseChangeLog>
|
||||||
+142
@@ -0,0 +1,142 @@
|
|||||||
|
/**
|
||||||
|
* 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.core.filters;
|
||||||
|
|
||||||
|
import static org.mockito.BDDMockito.given;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
|
import jakarta.servlet.FilterChain;
|
||||||
|
import jakarta.servlet.ServletException;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import org.apache.fineract.infrastructure.core.config.FineractProperties;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.IpAddressUtils;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
public class IpTrackingFilterTest {
|
||||||
|
|
||||||
|
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
|
||||||
|
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR",
|
||||||
|
"HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" };
|
||||||
|
@Mock
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private HttpServletResponse response;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private FilterChain filterChain;
|
||||||
|
|
||||||
|
private FineractProperties fineractProperties;
|
||||||
|
private CallerIpTrackingFilter underTest;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
fineractProperties = new FineractProperties();
|
||||||
|
FineractProperties.FineractIpTrackingProperties props = new FineractProperties.FineractIpTrackingProperties();
|
||||||
|
props.setEnabled(true);
|
||||||
|
fineractProperties.setIpTracking(props);
|
||||||
|
underTest = new CallerIpTrackingFilter(fineractProperties);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED",
|
||||||
|
"HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" })
|
||||||
|
void testGetClientIp_UsesCorrectHeaderEnabled(String headerName) throws ServletException, IOException {
|
||||||
|
// given
|
||||||
|
given(request.getHeader(headerName)).willReturn("192.168.1.100");
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.doFilterInternal(request, response, filterChain);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(request, times(1)).setAttribute("IP", "192.168.1.100");
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED",
|
||||||
|
"HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" })
|
||||||
|
void testGetClientIpAddress_UsesCorrectHeaderDisabled(String headerName) throws ServletException, IOException {
|
||||||
|
// given
|
||||||
|
FineractProperties.FineractIpTrackingProperties props = new FineractProperties.FineractIpTrackingProperties();
|
||||||
|
props.setEnabled(false);
|
||||||
|
fineractProperties.setIpTracking(props);
|
||||||
|
underTest = new CallerIpTrackingFilter(fineractProperties);
|
||||||
|
|
||||||
|
given(request.getHeader(headerName)).willReturn("192.168.1.100");
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.doFilterInternal(request, response, filterChain);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(request, never()).setAttribute("IP", "192.168.1.100");
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED",
|
||||||
|
"HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" })
|
||||||
|
void testGetClientIpAddress_UsesCorrectHeaderIpAdressUtilsEnable(String headerName) throws ServletException, IOException {
|
||||||
|
// given
|
||||||
|
given(request.getHeader(headerName)).willReturn("192.168.1.100");
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.doFilterInternal(request, response, filterChain);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(request, times(1)).setAttribute("IP", IpAddressUtils.getClientIp());
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ParameterizedTest
|
||||||
|
@ValueSource(strings = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED",
|
||||||
|
"HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR", "HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" })
|
||||||
|
void testGetClientIpAddress_UsesCorrectHeaderIpAdressUtilsDisabled(String headerName) throws ServletException, IOException {
|
||||||
|
// given
|
||||||
|
FineractProperties.FineractIpTrackingProperties props = new FineractProperties.FineractIpTrackingProperties();
|
||||||
|
props.setEnabled(false);
|
||||||
|
fineractProperties.setIpTracking(props);
|
||||||
|
underTest = new CallerIpTrackingFilter(fineractProperties);
|
||||||
|
given(request.getHeader(headerName)).willReturn("192.168.1.100");
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.doFilterInternal(request, response, filterChain);
|
||||||
|
|
||||||
|
// then
|
||||||
|
verify(request, never()).setAttribute("IP", IpAddressUtils.getClientIp());
|
||||||
|
verify(filterChain).doFilter(request, response);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
fineract.node-id=1
|
fineract.node-id=1
|
||||||
|
|
||||||
fineract.security.basicauth.enabled=true
|
fineract.security.basicauth.enabled=true
|
||||||
|
fineract.ip-tracking.enabled=false
|
||||||
fineract.security.oauth.enabled=false
|
fineract.security.oauth.enabled=false
|
||||||
fineract.security.2fa.enabled=false
|
fineract.security.2fa.enabled=false
|
||||||
fineract.security.hsts.enabled=false
|
fineract.security.hsts.enabled=false
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* 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.integrationtests;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
||||||
|
|
||||||
|
import io.restassured.builder.RequestSpecBuilder;
|
||||||
|
import io.restassured.builder.ResponseSpecBuilder;
|
||||||
|
import io.restassured.http.ContentType;
|
||||||
|
import io.restassured.specification.RequestSpecification;
|
||||||
|
import io.restassured.specification.ResponseSpecification;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.integrationtests.common.AuditHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class IpTrackingIntegrationTest {
|
||||||
|
|
||||||
|
private AuditHelper auditHelper;
|
||||||
|
private static final String EXPECTED_LOCAL_IP = "127.0.0.1";
|
||||||
|
private RequestSpecification requestSpec;
|
||||||
|
private ResponseSpecification responseSpec;
|
||||||
|
private ResponseSpecification responseSpecForSearch;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
public void setup() {
|
||||||
|
Utils.initializeRESTAssured();
|
||||||
|
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||||
|
this.requestSpec.header("Fineract-Platform-TenantId", "default");
|
||||||
|
this.requestSpec.auth().basic("mifos", "password");
|
||||||
|
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||||
|
this.responseSpecForSearch = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||||
|
this.auditHelper = new AuditHelper(this.requestSpec, this.responseSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void capturesIpAddressWhenCreatingClient() throws Exception {
|
||||||
|
assumeTrue(Boolean.parseBoolean(System.getenv().getOrDefault("FINERACT_CLIENT_IP_TRACKING_ENABLED", "true")),
|
||||||
|
"Saltando test porque el tracking de IP está deshabilitado");
|
||||||
|
|
||||||
|
// given
|
||||||
|
final Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec);
|
||||||
|
ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientId);
|
||||||
|
List<HashMap<String, Object>> auditsRecieved = auditHelper.getAuditDetails(clientId, "CREATE", "CLIENT");
|
||||||
|
|
||||||
|
// when
|
||||||
|
String ip = auditsRecieved.get(0).get("ip").toString();
|
||||||
|
|
||||||
|
assumeTrue(!ip.isEmpty(), "IP not arrived: skipping capture test when enabled");
|
||||||
|
// then
|
||||||
|
assertEquals(EXPECTED_LOCAL_IP, ip, "Expected local IP when tracking is enabled");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user