From b0ca0a24ccb185ea7757e424eb5c43acaa4cf246 Mon Sep 17 00:00:00 2001 From: Juan Pablo Date: Fri, 8 Aug 2025 10:58:19 -0600 Subject: [PATCH] FINERACT-2314: IP tracking (#4825) Co-authored-by: Juan Pablo Alvarez Hernandez --- .../commands/domain/CommandSource.java | 5 + .../infrastructure/core/api/JsonCommand.java | 1 - .../core/config/FineractProperties.java | 9 ++ .../core/filters/CallerIpTrackingFilter.java | 101 +++++++++++++ .../core/service/IpAddressUtils.java | 39 +++++ ...encyCommandProcessFailedExceptionTest.java | 2 + .../fineract/commands/data/AuditData.java | 1 + .../service/AuditReadPlatformServiceImpl.java | 14 +- .../core/config/SecurityConfig.java | 9 +- .../src/main/resources/application.properties | 1 + .../db/changelog/tenant/changelog-tenant.xml | 1 + .../tenant/parts/0193_add_column_ip.xml | 32 ++++ .../core/filters/IpTrackingFilterTest.java | 142 ++++++++++++++++++ .../resources/application-test.properties | 1 + .../IpTrackingIntegrationTest.java | 74 +++++++++ 15 files changed, 424 insertions(+), 8 deletions(-) create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/CallerIpTrackingFilter.java create mode 100644 fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/IpAddressUtils.java create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0193_add_column_ip.xml create mode 100644 fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/IpTrackingFilterTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/IpTrackingIntegrationTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java index 574d2cd09..e73946f94 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java @@ -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 { @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 { .transactionId(command.getTransactionId()) // .creditBureauId(command.getCreditBureauId()) // .organisationCreditBureauId(command.getOrganisationCreditBureauId()) // + .clientIp(IpAddressUtils.getClientIp()) // .loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); // } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java index a74064e09..23a21556a 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java @@ -596,5 +596,4 @@ public final class JsonCommand { public void checkForUnsupportedParameters(final Type typeOfMap, final String json, final Set requestDataParameters) { this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, requestDataParameters); } - } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java index 3b8b594a5..59384d005 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java @@ -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 { diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/CallerIpTrackingFilter.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/CallerIpTrackingFilter.java new file mode 100644 index 000000000..4e2dd1384 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/filters/CallerIpTrackingFilter.java @@ -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; + } + +} diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/IpAddressUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/IpAddressUtils.java new file mode 100644 index 000000000..d6621fc27 --- /dev/null +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/IpAddressUtils.java @@ -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; + } +} diff --git a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java index 18a7ad192..97d9a2f35 100644 --- a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java +++ b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java @@ -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)); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditData.java b/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditData.java index 7cb158017..6a415676e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditData.java @@ -56,4 +56,5 @@ public final class AuditData implements Serializable { private final Long clientId; private final Long loanId; private final String url; + private final String ip; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java index 2d18544e1..0feb1610a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java @@ -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 " + commandAsJsonString + ", " + " 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 " - + " left join m_appuser mk on mk.id = aud.maker_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 g on g.id = aud.group_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_loan l on l.id = aud.loan_id" + " left join m_savings_account s on s.id = aud.savings_account_id" + + " l.account_no as loanAccountNo, s.account_no as savingsAccountNo , aud.client_ip as ip " + + " from m_portfolio_command_source aud " + " left join m_appuser mk on mk.id = aud.maker_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 g on g.id = aud.group_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_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"; // 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 loanAccountNo = rs.getString("loanAccountNo"); final String savingsAccountNo = rs.getString("savingsAccountNo"); + final String ip = rs.getString("ip"); ZonedDateTime madeOnDate = madeOnDateUTC != null ? madeOnDateUTC.toZonedDateTime() : madeOnDateTenant; ZonedDateTime checkedOnDate = checkedOnDateUTC != null ? checkedOnDateUTC.toZonedDateTime() : checkedOnDateTenant; return new AuditData(id, actionName, entityName, resourceId, subresourceId, maker, madeOnDate, checker, checkedOnDate, processingResult, commandAsJson, officeName, groupLevelName, groupName, clientName, loanAccountNo, savingsAccountNo, - clientId, loanId, resourceGetUrl); + clientId, loanId, resourceGetUrl, ip); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java index 6159fa96e..3f723b1a3 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java @@ -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.configuration.domain.ConfigurationDomainService; 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.IdempotencyStoreFilter; import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper; @@ -172,7 +173,9 @@ public class SecurityConfig { } else { http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); // } - + if (fineractProperties.getIpTracking().isEnabled()) { + http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class); + } if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) { http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class); } else { @@ -219,6 +222,10 @@ public class SecurityConfig { return new CorrelationHeaderFilter(fineractProperties, mdcWrapper); } + public CallerIpTrackingFilter callerIpTrackingFilter() { + return new CallerIpTrackingFilter(fineractProperties); + } + public TenantAwareBasicAuthenticationFilter tenantAwareBasicAuthenticationFilter() throws Exception { TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(), basicAuthenticationEntryPoint(), toApiJsonSerializer, configurationDomainService, cacheWritePlatformService, diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 955645f54..38c92e467 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -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.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.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index fa8027a4e..1e12348ff 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -211,4 +211,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0193_add_column_ip.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0193_add_column_ip.xml new file mode 100644 index 000000000..95e47c820 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0193_add_column_ip.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/IpTrackingFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/IpTrackingFilterTest.java new file mode 100644 index 000000000..893298369 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/core/filters/IpTrackingFilterTest.java @@ -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); + + } + +} diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index b2caf5dd7..d7c5d8fc6 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -20,6 +20,7 @@ fineract.node-id=1 fineract.security.basicauth.enabled=true +fineract.ip-tracking.enabled=false fineract.security.oauth.enabled=false fineract.security.2fa.enabled=false fineract.security.hsts.enabled=false diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/IpTrackingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/IpTrackingIntegrationTest.java new file mode 100644 index 000000000..99339be4b --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/IpTrackingIntegrationTest.java @@ -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> 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"); + } + +}