Skip to content

Commit b0ca0a2

Browse files
JohnAlvaJuan Pablo Alvarez Hernandez
andauthored
FINERACT-2314: IP tracking (#4825)
Co-authored-by: Juan Pablo Alvarez Hernandez <work_jpa@hotmailcom>
1 parent 39050aa commit b0ca0a2

File tree

15 files changed

+424
-8
lines changed

15 files changed

+424
-8
lines changed

fineract-core/src/main/java/org/apache/fineract/commands/domain/CommandSource.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
3636
import org.apache.fineract.infrastructure.core.domain.ExternalId;
3737
import org.apache.fineract.infrastructure.core.service.DateUtils;
38+
import org.apache.fineract.infrastructure.core.service.IpAddressUtils;
3839
import org.apache.fineract.useradministration.domain.AppUser;
3940

4041
@Entity
@@ -137,6 +138,9 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
137138
@Column(name = "loan_external_id", length = 100)
138139
private ExternalId loanExternalId;
139140

141+
@Column(name = "client_ip", nullable = true)
142+
private String clientIp;
143+
140144
@Column(name = "is_sanitized", nullable = false)
141145
private boolean sanitized;
142146

@@ -162,6 +166,7 @@ public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final Js
162166
.transactionId(command.getTransactionId()) //
163167
.creditBureauId(command.getCreditBureauId()) //
164168
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
169+
.clientIp(IpAddressUtils.getClientIp()) //
165170
.loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); //
166171
}
167172

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/api/JsonCommand.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,5 +596,4 @@ public Locale extractLocale() {
596596
public void checkForUnsupportedParameters(final Type typeOfMap, final String json, final Set<String> requestDataParameters) {
597597
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, requestDataParameters);
598598
}
599-
600599
}

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/config/FineractProperties.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ public class FineractProperties {
5050

5151
private FineractCorrelationProperties correlation;
5252

53+
private FineractIpTrackingProperties ipTracking;
54+
5355
private FineractPartitionedJob partitionedJob;
5456

5557
private FineractRemoteJobMessageHandlerProperties remoteJobMessageHandler;
@@ -153,6 +155,13 @@ public static class FineractCorrelationProperties {
153155
private String headerName;
154156
}
155157

158+
@Getter
159+
@Setter
160+
public static class FineractIpTrackingProperties {
161+
162+
private boolean enabled;
163+
}
164+
156165
@Getter
157166
@Setter
158167
public static class FineractPartitionedJob {
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.fineract.infrastructure.core.filters;
21+
22+
import jakarta.servlet.FilterChain;
23+
import jakarta.servlet.ServletException;
24+
import jakarta.servlet.http.HttpServletRequest;
25+
import jakarta.servlet.http.HttpServletResponse;
26+
import java.io.IOException;
27+
import lombok.RequiredArgsConstructor;
28+
import lombok.extern.slf4j.Slf4j;
29+
import org.apache.commons.lang3.StringUtils;
30+
import org.apache.fineract.infrastructure.core.config.FineractProperties;
31+
import org.springframework.web.filter.OncePerRequestFilter;
32+
33+
@RequiredArgsConstructor
34+
@Slf4j
35+
public class CallerIpTrackingFilter extends OncePerRequestFilter {
36+
37+
private final FineractProperties fineractProperties;
38+
39+
/**
40+
* Common headers used to get client IP from different proxies.
41+
*
42+
* "X-Forwarded-For", // Standard header used by proxies "Proxy-Client-IP", // Used by some Apache proxies
43+
* "WL-Proxy-Client-IP", // Used by WebLogic "HTTP_X_FORWARDED_FOR", // Alternative to X-Forwarded-For
44+
* "HTTP_X_FORWARDED", // Variation of X-Forwarded "HTTP_X_CLUSTER_CLIENT_IP", // Used in clustered environments
45+
* "HTTP_CLIENT_IP", // Fallback, less common "HTTP_FORWARDED_FOR", // Less standard, used in some setups
46+
* "HTTP_FORWARDED", // Standardized header (RFC 7239) that can include client IP, proxy info, and protocol
47+
* "HTTP_VIA", // Shows intermediate proxies "REMOTE_ADDR" // Server's perceived client IP
48+
*/
49+
50+
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
51+
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR",
52+
"HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" };
53+
54+
public String getClientIpAddress(HttpServletRequest request) {
55+
for (String header : IP_HEADER_CANDIDATES) {
56+
String ip = request.getHeader(header);
57+
if (ip != null && ip.length() != 0 && !ip.isEmpty()) {
58+
log.trace("CALLER IP : {}", ip);
59+
return ip;
60+
}
61+
}
62+
log.trace("getRemoteAddr method : {}", request.getRemoteAddr());
63+
return request.getRemoteAddr();
64+
}
65+
66+
@Override
67+
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
68+
throws IOException, ServletException {
69+
if (fineractProperties.getIpTracking().isEnabled()) {
70+
handleClientIp(request, response, filterChain);
71+
} else {
72+
filterChain.doFilter(request, response);
73+
}
74+
75+
}
76+
77+
private void handleClientIp(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
78+
throws IOException, ServletException {
79+
try {
80+
String clientIpAddress = getClientIpAddress(request);
81+
if (StringUtils.isNotBlank(clientIpAddress)) {
82+
log.trace("Found Client IP in header : {}", clientIpAddress);
83+
request.setAttribute("IP", clientIpAddress);
84+
}
85+
filterChain.doFilter(request, response);
86+
} finally {
87+
request.setAttribute("IP", "");
88+
}
89+
}
90+
91+
@Override
92+
protected boolean isAsyncDispatch(final HttpServletRequest request) {
93+
return false;
94+
}
95+
96+
@Override
97+
protected boolean shouldNotFilterErrorDispatch() {
98+
return false;
99+
}
100+
101+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.fineract.infrastructure.core.service;
20+
21+
import org.springframework.web.context.request.RequestContextHolder;
22+
import org.springframework.web.context.request.ServletRequestAttributes;
23+
24+
public final class IpAddressUtils {
25+
26+
private IpAddressUtils() {}
27+
28+
public static String getClientIp() {
29+
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
30+
String clientIp = "";
31+
if (attrs != null) {
32+
Object ipAttr = attrs.getRequest().getAttribute("IP");
33+
if (ipAttr != null) {
34+
clientIp = ipAttr.toString();
35+
}
36+
}
37+
return clientIp;
38+
}
39+
}

fineract-core/src/test/java/org/apache/fineract/infrastructure/core/exception/IdempotencyCommandProcessFailedExceptionTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public void tearDown() {
5656

5757
@Test
5858
public void testInconsistentStatus() {
59+
5960
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
6061
CommandWrapper command = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
6162
null, null, null, null, null, null);
@@ -64,5 +65,6 @@ public void testInconsistentStatus() {
6465
Response result = mapper.toResponse(exception);
6566
assertEquals(500, result.getStatus());
6667
assertEquals("true", result.getHeaderString(IDEMPOTENT_CACHE_HEADER));
68+
6769
}
6870
}

fineract-provider/src/main/java/org/apache/fineract/commands/data/AuditData.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,5 @@ public final class AuditData implements Serializable {
5656
private final Long clientId;
5757
private final Long loanId;
5858
private final String url;
59+
private final String ip;
5960
}

fineract-provider/src/main/java/org/apache/fineract/commands/service/AuditReadPlatformServiceImpl.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,12 @@ public String schema(final boolean includeJson, final String hierarchy) {
110110
+ "ck.username as checker, aud.checked_on_date as checkedOnDate, aud.checked_on_date_utc as checkedOnDateUTC, ev.enum_message_property as processingResult "
111111
+ commandAsJsonString + ", "
112112
+ " o.name as officeName, gl.level_name as groupLevelName, g.display_name as groupName, c.display_name as clientName, "
113-
+ " l.account_no as loanAccountNo, s.account_no as savingsAccountNo " + " from m_portfolio_command_source aud "
114-
+ " left join m_appuser mk on mk.id = aud.maker_id" + " left join m_appuser ck on ck.id = aud.checker_id"
115-
+ " left join m_office o on o.id = aud.office_id" + " left join m_group g on g.id = aud.group_id"
116-
+ " left join m_group_level gl on gl.id = g.level_id" + " left join m_client c on c.id = aud.client_id"
117-
+ " left join m_loan l on l.id = aud.loan_id" + " left join m_savings_account s on s.id = aud.savings_account_id"
113+
+ " l.account_no as loanAccountNo, s.account_no as savingsAccountNo , aud.client_ip as ip "
114+
+ " from m_portfolio_command_source aud " + " left join m_appuser mk on mk.id = aud.maker_id"
115+
+ " left join m_appuser ck on ck.id = aud.checker_id" + " left join m_office o on o.id = aud.office_id"
116+
+ " left join m_group g on g.id = aud.group_id" + " left join m_group_level gl on gl.id = g.level_id"
117+
+ " left join m_client c on c.id = aud.client_id" + " left join m_loan l on l.id = aud.loan_id"
118+
+ " left join m_savings_account s on s.id = aud.savings_account_id"
118119
+ " left join r_enum_value ev on ev.enum_name = 'status' and ev.enum_id = aud.status";
119120

120121
// data scoping: head office (hierarchy = ".") can see all audit
@@ -158,13 +159,14 @@ public AuditData mapRow(final ResultSet rs, @SuppressWarnings("unused") final in
158159
final String clientName = rs.getString("clientName");
159160
final String loanAccountNo = rs.getString("loanAccountNo");
160161
final String savingsAccountNo = rs.getString("savingsAccountNo");
162+
final String ip = rs.getString("ip");
161163

162164
ZonedDateTime madeOnDate = madeOnDateUTC != null ? madeOnDateUTC.toZonedDateTime() : madeOnDateTenant;
163165
ZonedDateTime checkedOnDate = checkedOnDateUTC != null ? checkedOnDateUTC.toZonedDateTime() : checkedOnDateTenant;
164166

165167
return new AuditData(id, actionName, entityName, resourceId, subresourceId, maker, madeOnDate, checker, checkedOnDate,
166168
processingResult, commandAsJson, officeName, groupLevelName, groupName, clientName, loanAccountNo, savingsAccountNo,
167-
clientId, loanId, resourceGetUrl);
169+
clientId, loanId, resourceGetUrl, ip);
168170
}
169171
}
170172

fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/SecurityConfig.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.apache.fineract.infrastructure.cache.service.CacheWritePlatformService;
3333
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
3434
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
35+
import org.apache.fineract.infrastructure.core.filters.CallerIpTrackingFilter;
3536
import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter;
3637
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
3738
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper;
@@ -172,7 +173,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
172173
} else {
173174
http.addFilterAfter(idempotencyStoreFilter(), FineractInstanceModeApiFilter.class); //
174175
}
175-
176+
if (fineractProperties.getIpTracking().isEnabled()) {
177+
http.addFilterAfter(callerIpTrackingFilter(), RequestResponseFilter.class);
178+
}
176179
if (fineractProperties.getSecurity().getTwoFactor().isEnabled()) {
177180
http.addFilterAfter(twoFactorAuthenticationFilter(), CorrelationHeaderFilter.class);
178181
} else {
@@ -219,6 +222,10 @@ public CorrelationHeaderFilter correlationHeaderFilter() {
219222
return new CorrelationHeaderFilter(fineractProperties, mdcWrapper);
220223
}
221224

225+
public CallerIpTrackingFilter callerIpTrackingFilter() {
226+
return new CallerIpTrackingFilter(fineractProperties);
227+
}
228+
222229
public TenantAwareBasicAuthenticationFilter tenantAwareBasicAuthenticationFilter() throws Exception {
223230
TenantAwareBasicAuthenticationFilter filter = new TenantAwareBasicAuthenticationFilter(authenticationManagerBean(),
224231
basicAuthenticationEntryPoint(), toApiJsonSerializer, configurationDomainService, cacheWritePlatformService,

fineract-provider/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ fineract.correlation.enabled=${FINERACT_LOGGING_HTTP_CORRELATION_ID_ENABLED:fals
6262
fineract.correlation.header-name=${FINERACT_LOGGING_HTTP_CORRELATION_ID_HEADER_NAME:X-Correlation-ID}
6363

6464
fineract.job.stuck-retry-threshold=${FINERACT_JOB_STUCK_RETRY_THRESHOLD:5}
65+
fineract.ip-tracking.enabled=${FINERACT_CLIENT_IP_TRACKING_ENABLED:false}
6566
fineract.job.loan-cob-enabled=${FINERACT_JOB_LOAN_COB_ENABLED:true}
6667

6768
fineract.partitioned-job.partitioned-job-properties[0].job-name=LOAN_COB

0 commit comments

Comments
 (0)