Skip to content

Commit f3bdaca

Browse files
author
Juan Pablo Alvarez Hernandez
committed
FINERACT-2314: Geolocation tracking
1 parent 4c3da5b commit f3bdaca

File tree

11 files changed

+197
-9
lines changed

11 files changed

+197
-9
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
@@ -34,6 +34,7 @@
3434
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
3535
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
3636
import org.apache.fineract.infrastructure.core.domain.ExternalId;
37+
import org.apache.fineract.infrastructure.core.filters.ClientIpHolder;
3738
import org.apache.fineract.infrastructure.core.service.DateUtils;
3839
import org.apache.fineract.useradministration.domain.AppUser;
3940

@@ -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(ClientIpHolder.getClientIp()) //
165170
.loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); //
166171
}
167172

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 FineractGeolocationProperties geolocation;
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 FineractGeolocationProperties {
161+
162+
private boolean enabled;
163+
}
164+
156165
@Getter
157166
@Setter
158167
public static class FineractPartitionedJob {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
public class ClientIpHolder {
23+
24+
private static final ThreadLocal<String> clientIpHolder = new ThreadLocal<>();
25+
26+
public static void setClientIp(String ip) {
27+
clientIpHolder.set(ip);
28+
}
29+
30+
public static String getClientIp() {
31+
return clientIpHolder.get();
32+
}
33+
34+
public static void clear() {
35+
clientIpHolder.remove();
36+
}
37+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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 GeolocationHeaderFilter extends OncePerRequestFilter {
36+
37+
private final FineractProperties fineractProperties;
38+
39+
private static final String[] IP_HEADER_CANDIDATES = { "X-Forwarded-For", "Proxy-Client-IP", "WL-Proxy-Client-IP",
40+
"HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED", "HTTP_X_CLUSTER_CLIENT_IP", "HTTP_CLIENT_IP", "HTTP_FORWARDED_FOR",
41+
"HTTP_FORWARDED", "HTTP_VIA", "REMOTE_ADDR" };
42+
43+
public static String getClientIpAddress(HttpServletRequest request) {
44+
for (String header : IP_HEADER_CANDIDATES) {
45+
String ip = request.getHeader(header);
46+
if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
47+
log.debug("SEND IP : {}", ip);
48+
return ip;
49+
}
50+
}
51+
log.debug("getRemoteAddr method : {}", request.getRemoteAddr());
52+
return request.getRemoteAddr();
53+
}
54+
55+
@Override
56+
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain)
57+
throws IOException, ServletException {
58+
FineractProperties.FineractGeolocationProperties geolocationProperties = fineractProperties.getGeolocation();
59+
if (geolocationProperties.isEnabled()) {
60+
handleClientIp(request, response, filterChain, geolocationProperties);
61+
} else {
62+
filterChain.doFilter(request, response);
63+
}
64+
65+
}
66+
67+
private void handleClientIp(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain,
68+
FineractProperties.FineractGeolocationProperties geolocationProperties) throws IOException, ServletException {
69+
try {
70+
String clientIpAddress = getClientIpAddress(request);
71+
if (StringUtils.isNotBlank(clientIpAddress)) {
72+
log.info("Found Client IP in header : {}", clientIpAddress);
73+
ClientIpHolder.setClientIp(clientIpAddress);
74+
}
75+
filterChain.doFilter(request, response);
76+
} catch (Exception e) {
77+
e.printStackTrace();
78+
} finally {
79+
ClientIpHolder.clear();
80+
}
81+
}
82+
83+
@Override
84+
protected boolean isAsyncDispatch(final HttpServletRequest request) {
85+
return false;
86+
}
87+
88+
@Override
89+
protected boolean shouldNotFilterErrorDispatch() {
90+
return false;
91+
}
92+
93+
}

fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/migration/TenantDataSourceFactory.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,8 @@ public HikariDataSource create(FineractPlatformTenant tenant) {
6363
dataSource.setUsername(tenantConnection.getSchemaUsername());
6464
dataSource.setPassword(databasePasswordEncryptor.decrypt(tenantConnection.getSchemaPassword()));
6565
String protocol = toProtocol(tenantDataSource);
66-
String tenantJdbcUrl = toJdbcUrl(protocol, tenantConnection.getSchemaServer(), tenantConnection.getSchemaServerPort(),
67-
tenantConnection.getSchemaName(), tenantConnection.getSchemaConnectionParameters());
66+
String tenantJdbcUrl = toJdbcUrl(protocol, tenantConnection.getSchemaServer(), tenantConnection.getSchemaServerPort(), tenantConnection.getSchemaName(),
67+
tenantConnection.getSchemaConnectionParameters());
6868
LOG.debug("JDBC URL for tenant {} is {}", tenant.getTenantIdentifier(), tenantJdbcUrl);
6969
dataSource.setJdbcUrl(tenantJdbcUrl);
7070
return dataSource;

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") != null) ? rs.getString("ip") : "SN/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
@@ -33,6 +33,7 @@
3333
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
3434
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
3535
import org.apache.fineract.infrastructure.core.filters.CorrelationHeaderFilter;
36+
import org.apache.fineract.infrastructure.core.filters.GeolocationHeaderFilter;
3637
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreFilter;
3738
import org.apache.fineract.infrastructure.core.filters.IdempotencyStoreHelper;
3839
import org.apache.fineract.infrastructure.core.filters.RequestResponseFilter;
@@ -165,7 +166,9 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
165166
.addFilterBefore(tenantAwareBasicAuthenticationFilter(), SecurityContextHolderFilter.class) //
166167
.addFilterAfter(requestResponseFilter(), ExceptionTranslationFilter.class) //
167168
.addFilterAfter(correlationHeaderFilter(), RequestResponseFilter.class) //
168-
.addFilterAfter(fineractInstanceModeApiFilter(), CorrelationHeaderFilter.class); //
169+
.addFilterAfter(fineractInstanceModeApiFilter(), CorrelationHeaderFilter.class) //
170+
.addFilterAfter(fineractInstanceModeApiFilter(), CorrelationHeaderFilter.class) //
171+
.addFilterAfter(geolocationHeaderFilter(), RequestResponseFilter.class); //
169172
if (!Objects.isNull(loanCOBFilterHelper)) {
170173
http.addFilterAfter(loanCOBApiFilter(), FineractInstanceModeApiFilter.class) //
171174
.addFilterAfter(idempotencyStoreFilter(), LoanCOBApiFilter.class); //
@@ -227,6 +230,10 @@ public TenantAwareBasicAuthenticationFilter tenantAwareBasicAuthenticationFilter
227230
return filter;
228231
}
229232

233+
public GeolocationHeaderFilter geolocationHeaderFilter() {
234+
return new GeolocationHeaderFilter(fineractProperties);
235+
}
236+
230237
@Bean
231238
public BasicAuthenticationEntryPoint basicAuthenticationEntryPoint() {
232239
BasicAuthenticationEntryPoint basicAuthenticationEntryPoint = new BasicAuthenticationEntryPoint();

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.geolocation.enabled=${FINERACT_GEOLOCATION_ENABLED:true}
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

fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,5 @@
204204
<include file="parts/0183_add_LoanCapitalizedIncomeTransactionCreatedBusinessEvent.xml" relativeToChangelogFile="true" />
205205
<include file="parts/0184_add_document_event_configuration.xml" relativeToChangelogFile="true" />
206206
<include file="parts/0185_loan_buy_down_fee.xml" relativeToChangelogFile="true" />
207+
<include file="parts/0186_add_column_ip.xml" relativeToChangelogFile="true" />
207208
</databaseChangeLog>

0 commit comments

Comments
 (0)