Skip to content

Commit 7daf4b8

Browse files
airajenaadamsaghy
authored andcommitted
FINERACT-2005: Prohibit password re-use with configurable global setting
1 parent 054d18b commit 7daf4b8

File tree

9 files changed

+227
-9
lines changed

9 files changed

+227
-9
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public final class GlobalConfigurationConstants {
8080
public static final String ALLOWED_LOAN_STATUSES_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-for-external-asset-transfer";
8181
public static final String ALLOWED_LOAN_STATUSES_OF_DELAYED_SETTLEMENT_FOR_EXTERNAL_ASSET_TRANSFER = "allowed-loan-statuses-of-delayed-settlement-for-external-asset-transfer";
8282
public static final String ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION = "enable-originator-creation-during-loan-application";
83+
public static final String PASSWORD_REUSE_CHECK_HISTORY_COUNT = "password-reuse-check-history-count";
8384

8485
private GlobalConfigurationConstants() {}
8586
}

fineract-core/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,4 +151,6 @@ public interface ConfigurationDomainService {
151151
boolean isImmediateChargeAccrualPostMaturityEnabled();
152152

153153
String getAssetOwnerTransferOustandingInterestStrategy();
154+
155+
Integer getPasswordReuseRestrictionCount();
154156
}

fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/domain/ConfigurationDomainServiceJpa.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -548,4 +548,15 @@ public String getAssetOwnerTransferOustandingInterestStrategy() {
548548
return getGlobalConfigurationPropertyData(
549549
GlobalConfigurationConstants.ASSET_OWNER_TRANSFER_OUTSTANDING_INTEREST_CALCULATION_STRATEGY).getStringValue();
550550
}
551+
552+
@Override
553+
public Integer getPasswordReuseRestrictionCount() {
554+
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
555+
GlobalConfigurationConstants.PASSWORD_REUSE_CHECK_HISTORY_COUNT);
556+
if (!property.isEnabled()) {
557+
return null;
558+
}
559+
Long value = property.getValue();
560+
return value != null && value > 0 ? value.intValue() : 0;
561+
}
551562
}

fineract-provider/src/main/java/org/apache/fineract/useradministration/service/AppUserWritePlatformServiceJpaRepositoryImpl.java

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import lombok.extern.slf4j.Slf4j;
3333
import org.apache.commons.lang3.exception.ExceptionUtils;
3434
import org.apache.fineract.commands.service.CommandWrapperBuilder;
35+
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
3536
import org.apache.fineract.infrastructure.core.api.JsonCommand;
3637
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
3738
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -48,7 +49,6 @@
4849
import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
4950
import org.apache.fineract.portfolio.client.domain.Client;
5051
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
51-
import org.apache.fineract.useradministration.api.AppUserApiConstant;
5252
import org.apache.fineract.useradministration.domain.AppUser;
5353
import org.apache.fineract.useradministration.domain.AppUserPreviousPassword;
5454
import org.apache.fineract.useradministration.domain.AppUserPreviousPasswordRepository;
@@ -83,6 +83,7 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
8383
private final AppUserPreviousPasswordRepository appUserPreviewPasswordRepository;
8484
private final StaffRepositoryWrapper staffRepositoryWrapper;
8585
private final ClientRepositoryWrapper clientRepositoryWrapper;
86+
private final ConfigurationDomainService configurationDomainService;
8687

8788
@Override
8889
@Transactional
@@ -269,12 +270,20 @@ private AppUserPreviousPassword getCurrentPasswordToSaveAsPreview(final AppUser
269270
AppUserPreviousPassword currentPasswordToSaveAsPreview = null;
270271

271272
if (passWordEncodedValue != null) {
272-
PageRequest pageRequest = PageRequest.of(0, AppUserApiConstant.numberOfPreviousPasswords, Sort.Direction.DESC, "removalDate");
273-
final List<AppUserPreviousPassword> nLastUsedPasswords = this.appUserPreviewPasswordRepository.findByUserId(user.getId(),
274-
pageRequest);
275-
for (AppUserPreviousPassword aPreviewPassword : nLastUsedPasswords) {
276-
if (aPreviewPassword.getPassword().equals(passWordEncodedValue)) {
277-
throw new PasswordPreviouslyUsedException();
273+
final Integer passwordReuseRestrictionCount = this.configurationDomainService.getPasswordReuseRestrictionCount();
274+
if (passwordReuseRestrictionCount != null) {
275+
List<AppUserPreviousPassword> previousPasswords;
276+
if (passwordReuseRestrictionCount == 0) {
277+
previousPasswords = this.appUserPreviewPasswordRepository.findByUserId(user.getId(),
278+
PageRequest.of(0, Integer.MAX_VALUE, Sort.Direction.DESC, "removalDate"));
279+
} else {
280+
PageRequest pageRequest = PageRequest.of(0, passwordReuseRestrictionCount, Sort.Direction.DESC, "removalDate");
281+
previousPasswords = this.appUserPreviewPasswordRepository.findByUserId(user.getId(), pageRequest);
282+
}
283+
for (AppUserPreviousPassword aPreviewPassword : previousPasswords) {
284+
if (aPreviewPassword.getPassword().equals(passWordEncodedValue)) {
285+
throw new PasswordPreviouslyUsedException();
286+
}
278287
}
279288
}
280289

fineract-provider/src/main/java/org/apache/fineract/useradministration/starter/UserAdministrationConfiguration.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
*/
1919
package org.apache.fineract.useradministration.starter;
2020

21+
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
2122
import org.apache.fineract.infrastructure.core.service.database.DatabaseSpecificSQLGenerator;
2223
import org.apache.fineract.infrastructure.security.service.PlatformPasswordEncoder;
2324
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
@@ -75,10 +76,10 @@ public AppUserWritePlatformService appUserWritePlatformService(PlatformSecurityC
7576
PlatformPasswordEncoder platformPasswordEncoder, AppUserRepository appUserRepository,
7677
OfficeRepositoryWrapper officeRepositoryWrapper, RoleRepository roleRepository, UserDataValidator fromApiJsonDeserializer,
7778
AppUserPreviousPasswordRepository appUserPreviewPasswordRepository, StaffRepositoryWrapper staffRepositoryWrapper,
78-
ClientRepositoryWrapper clientRepositoryWrapper) {
79+
ClientRepositoryWrapper clientRepositoryWrapper, ConfigurationDomainService configurationDomainService) {
7980
return new AppUserWritePlatformServiceJpaRepositoryImpl(context, userDomainService, platformPasswordEncoder, appUserRepository,
8081
officeRepositoryWrapper, roleRepository, fromApiJsonDeserializer, appUserPreviewPasswordRepository, staffRepositoryWrapper,
81-
clientRepositoryWrapper);
82+
clientRepositoryWrapper, configurationDomainService);
8283
}
8384

8485
@Bean

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
@@ -228,4 +228,5 @@
228228
<include file="parts/0207_add_allow_full_term_for_tranche.xml" relativeToChangelogFile="true" />
229229
<include file="parts/0208_trial_balance_summary_with_asset_owner_journal_entry_aggregation_fix.xml" relativeToChangelogFile="true" />
230230
<include file="parts/0209_transaction_summary_with_asset_owner_and_from_asset_owner_id_for_asset_sales.xml" relativeToChangelogFile="true" />
231+
<include file="parts/0210_add_configuration_password_reuse_check_history_count.xml" relativeToChangelogFile="true" />
231232
</databaseChangeLog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
Licensed to the Apache Software Foundation (ASF) under one
5+
or more contributor license agreements. See the NOTICE file
6+
distributed with this work for additional information
7+
regarding copyright ownership. The ASF licenses this file
8+
to you under the Apache License, Version 2.0 (the
9+
"License"); you may not use this file except in compliance
10+
with the License. You may obtain a copy of the License at
11+
12+
http://www.apache.org/licenses/LICENSE-2.0
13+
14+
Unless required by applicable law or agreed to in writing,
15+
software distributed under the License is distributed on an
16+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17+
KIND, either express or implied. See the License for the
18+
specific language governing permissions and limitations
19+
under the License.
20+
21+
-->
22+
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
23+
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
24+
<changeSet author="fineract" id="1">
25+
<insert tableName="c_configuration">
26+
<column name="name" value="password-reuse-check-history-count"/>
27+
<column name="value" valueNumeric="3"/>
28+
<column name="date_value"/>
29+
<column name="string_value"/>
30+
<column name="enabled" valueBoolean="false"/>
31+
<column name="is_trap_door" valueBoolean="false"/>
32+
<column name="description" value="When enabled, prevents password reuse. The value specifies how many previous passwords to check (e.g., 3 = last 3 passwords). Set to 0 to check ALL previous passwords. Disable this setting to allow password reuse."/>
33+
</insert>
34+
</changeSet>
35+
</databaseChangeLog>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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.useradministration.service;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
import static org.junit.jupiter.api.Assertions.assertThrows;
23+
import static org.mockito.ArgumentMatchers.any;
24+
import static org.mockito.ArgumentMatchers.anyString;
25+
import static org.mockito.ArgumentMatchers.eq;
26+
import static org.mockito.ArgumentMatchers.nullable;
27+
import static org.mockito.Mockito.doNothing;
28+
import static org.mockito.Mockito.mock;
29+
import static org.mockito.Mockito.never;
30+
import static org.mockito.Mockito.verify;
31+
import static org.mockito.Mockito.when;
32+
33+
import java.util.List;
34+
import java.util.Map;
35+
import java.util.Optional;
36+
import org.apache.fineract.commands.domain.CommandWrapper;
37+
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
38+
import org.apache.fineract.infrastructure.core.api.JsonCommand;
39+
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
40+
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
41+
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
42+
import org.apache.fineract.infrastructure.security.service.PlatformPasswordEncoder;
43+
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
44+
import org.apache.fineract.organisation.office.domain.Office;
45+
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
46+
import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
47+
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
48+
import org.apache.fineract.useradministration.domain.AppUser;
49+
import org.apache.fineract.useradministration.domain.AppUserPreviousPassword;
50+
import org.apache.fineract.useradministration.domain.AppUserPreviousPasswordRepository;
51+
import org.apache.fineract.useradministration.domain.AppUserRepository;
52+
import org.apache.fineract.useradministration.domain.RoleRepository;
53+
import org.apache.fineract.useradministration.domain.UserDomainService;
54+
import org.apache.fineract.useradministration.exception.PasswordPreviouslyUsedException;
55+
import org.junit.jupiter.api.AfterEach;
56+
import org.junit.jupiter.api.BeforeEach;
57+
import org.junit.jupiter.api.Test;
58+
import org.junit.jupiter.api.extension.ExtendWith;
59+
import org.mockito.InjectMocks;
60+
import org.mockito.Mock;
61+
import org.mockito.junit.jupiter.MockitoExtension;
62+
import org.springframework.data.domain.PageRequest;
63+
64+
@ExtendWith(MockitoExtension.class)
65+
public class AppUserWritePlatformServiceJpaRepositoryImplTest {
66+
67+
private static final Long USER_ID = 1L;
68+
69+
@Mock
70+
private PlatformSecurityContext context;
71+
@Mock
72+
private UserDomainService userDomainService;
73+
@Mock
74+
private PlatformPasswordEncoder platformPasswordEncoder;
75+
@Mock
76+
private AppUserRepository appUserRepository;
77+
@Mock
78+
private OfficeRepositoryWrapper officeRepositoryWrapper;
79+
@Mock
80+
private RoleRepository roleRepository;
81+
@Mock
82+
private UserDataValidator fromApiJsonDeserializer;
83+
@Mock
84+
private AppUserPreviousPasswordRepository appUserPreviewPasswordRepository;
85+
@Mock
86+
private StaffRepositoryWrapper staffRepositoryWrapper;
87+
@Mock
88+
private ClientRepositoryWrapper clientRepositoryWrapper;
89+
@Mock
90+
private ConfigurationDomainService configurationDomainService;
91+
92+
@InjectMocks
93+
private AppUserWritePlatformServiceJpaRepositoryImpl underTest;
94+
95+
private JsonCommand command;
96+
private AppUser user;
97+
private AppUser authenticatedUser;
98+
99+
@BeforeEach
100+
void setUp() {
101+
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
102+
command = mock(JsonCommand.class);
103+
user = mock(AppUser.class);
104+
authenticatedUser = mock(AppUser.class);
105+
106+
when(command.json()).thenReturn("{}");
107+
when(appUserRepository.findById(USER_ID)).thenReturn(Optional.of(user));
108+
when(context.authenticatedUser(any(CommandWrapper.class))).thenReturn(authenticatedUser);
109+
doNothing().when(fromApiJsonDeserializer).validateForChangePassword(anyString(), nullable(AppUser.class));
110+
}
111+
112+
@AfterEach
113+
void tearDown() {
114+
ThreadLocalContextUtil.reset();
115+
}
116+
117+
@Test
118+
void changeUserPasswordThrowsWhenPasswordPreviouslyUsed() {
119+
when(user.getId()).thenReturn(USER_ID);
120+
when(user.getEncodedPassword(command, platformPasswordEncoder)).thenReturn("encoded");
121+
when(configurationDomainService.getPasswordReuseRestrictionCount()).thenReturn(2);
122+
123+
AppUserPreviousPassword previousPassword = mock(AppUserPreviousPassword.class);
124+
when(previousPassword.getPassword()).thenReturn("encoded");
125+
when(appUserPreviewPasswordRepository.findByUserId(eq(USER_ID), any(PageRequest.class))).thenReturn(List.of(previousPassword));
126+
127+
assertThrows(PasswordPreviouslyUsedException.class, () -> underTest.changeUserPassword(USER_ID, command));
128+
129+
verify(appUserRepository, never()).saveAndFlush(user);
130+
verify(appUserPreviewPasswordRepository, never()).save(any(AppUserPreviousPassword.class));
131+
}
132+
133+
@Test
134+
void changeUserPasswordSavesPreviousPasswordWhenAllowed() {
135+
Office office = mock(Office.class);
136+
when(office.getId()).thenReturn(7L);
137+
when(user.getOffice()).thenReturn(office);
138+
when(user.getId()).thenReturn(USER_ID);
139+
when(user.getPassword()).thenReturn("currentEncoded");
140+
when(user.getEncodedPassword(command, platformPasswordEncoder)).thenReturn("newEncoded");
141+
when(user.changePassword(command, platformPasswordEncoder)).thenReturn(Map.of("password", "new"));
142+
when(configurationDomainService.getPasswordReuseRestrictionCount()).thenReturn(2);
143+
when(appUserPreviewPasswordRepository.findByUserId(eq(USER_ID), any(PageRequest.class))).thenReturn(List.of());
144+
145+
CommandProcessingResult result = underTest.changeUserPassword(USER_ID, command);
146+
147+
assertEquals(USER_ID, result.getResourceId());
148+
verify(appUserRepository).saveAndFlush(user);
149+
verify(appUserPreviewPasswordRepository).save(any(AppUserPreviousPassword.class));
150+
}
151+
}

integration-tests/src/test/java/org/apache/fineract/integrationtests/common/GlobalConfigurationHelper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,13 @@ private static ArrayList<HashMap> getAllDefaultGlobalConfigurations() {
217217
forcePasswordResetDaysDefault.put("trapDoor", false);
218218
defaults.add(forcePasswordResetDaysDefault);
219219

220+
HashMap<String, Object> passwordReuseCheckHistoryCountDefault = new HashMap<>();
221+
passwordReuseCheckHistoryCountDefault.put("name", GlobalConfigurationConstants.PASSWORD_REUSE_CHECK_HISTORY_COUNT);
222+
passwordReuseCheckHistoryCountDefault.put("value", 3L);
223+
passwordReuseCheckHistoryCountDefault.put("enabled", false);
224+
passwordReuseCheckHistoryCountDefault.put("trapDoor", false);
225+
defaults.add(passwordReuseCheckHistoryCountDefault);
226+
220227
HashMap<String, Object> graceOnPenaltyPostingDefault = new HashMap<>();
221228
graceOnPenaltyPostingDefault.put("name", GlobalConfigurationConstants.GRACE_ON_PENALTY_POSTING);
222229
graceOnPenaltyPostingDefault.put("value", 0L);

0 commit comments

Comments
 (0)