FINERACT-2314: IP tracking (#4825)

Co-authored-by: Juan Pablo Alvarez Hernandez <work_jpa@hotmailcom>
This commit is contained in:
Juan Pablo
2025-08-08 10:58:19 -06:00
committed by GitHub
co-authored by Juan Pablo Alvarez Hernandez
parent 39050aa01f
commit b0ca0a24cc
15 changed files with 424 additions and 8 deletions
@@ -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.ExternalId;
import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.IpAddressUtils;
import org.apache.fineract.useradministration.domain.AppUser;
@Entity
@@ -137,6 +138,9 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
@Column(name = "loan_external_id", length = 100)
private ExternalId loanExternalId;
@Column(name = "client_ip", nullable = true)
private String clientIp;
@Column(name = "is_sanitized", nullable = false)
private boolean sanitized;
@@ -162,6 +166,7 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
.transactionId(command.getTransactionId()) //
.creditBureauId(command.getCreditBureauId()) //
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
.clientIp(IpAddressUtils.getClientIp()) //
.loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); //
}
@@ -596,5 +596,4 @@ public final class JsonCommand {
public void checkForUnsupportedParameters(final Type typeOfMap, final String json, final Set<String> requestDataParameters) {
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, requestDataParameters);
}
}
@@ -50,6 +50,8 @@ public class FineractProperties {
private FineractCorrelationProperties correlation;
private FineractIpTrackingProperties ipTracking;
private FineractPartitionedJob partitionedJob;
private FineractRemoteJobMessageHandlerProperties remoteJobMessageHandler;
@@ -153,6 +155,13 @@ public class FineractProperties {
private String headerName;
}
@Getter
@Setter
public static class FineractIpTrackingProperties {
private boolean enabled;
}
@Getter
@Setter
public static class FineractPartitionedJob {
@@ -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;
}
}
@@ -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;
}
}
@@ -56,6 +56,7 @@ public class IdempotencyCommandProcessFailedExceptionTest {
@Test
public void testInconsistentStatus() {
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
CommandWrapper command = new CommandWrapper(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);
assertEquals(500, result.getStatus());
assertEquals("true", result.getHeaderString(IDEMPOTENT_CACHE_HEADER));
}
}