FINERACT-2185: User management improvement

This commit is contained in:
Marta Jankovics
2025-04-23 09:56:05 +02:00
committed by Adam Saghy
parent 8d83b5547c
commit 79c006774e
28 changed files with 506 additions and 95 deletions
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.commands.domain;
import jakarta.validation.constraints.NotNull;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
@@ -41,8 +42,21 @@ public enum CommandProcessingResultType {
private final Integer value;
private final String code;
@NotNull
public static CommandProcessingResultType fromInt(final Integer value) {
CommandProcessingResultType transactionType = BY_ID.get(value);
return transactionType == null ? INVALID : transactionType;
}
public boolean isProcessed() {
return this == PROCESSED;
}
public boolean isAwaitingApproval() {
return this == AWAITING_APPROVAL;
}
public boolean isRejected() {
return this == REJECTED;
}
}
@@ -23,6 +23,7 @@ import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import java.time.OffsetDateTime;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -136,9 +137,11 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
@Column(name = "loan_external_id", length = 100)
private ExternalId loanExternalId;
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status) {
@Column(name = "is_sanitized", nullable = false)
private boolean sanitized;
public static CommandSource fullEntryFrom(final CommandWrapper wrapper, final JsonCommand command, final AppUser maker,
String idempotencyKey, Integer status, boolean sanitized) {
return CommandSource.builder() //
.actionName(wrapper.actionName()) //
.entityName(wrapper.entityName()) //
@@ -159,31 +162,52 @@ public class CommandSource extends AbstractPersistableCustom<Long> {
.transactionId(command.getTransactionId()) //
.creditBureauId(command.getCreditBureauId()) //
.organisationCreditBureauId(command.getOrganisationCreditBureauId()) //
.loanExternalId(command.getLoanExternalId()).build(); //
.loanExternalId(command.getLoanExternalId()).sanitized(sanitized).build(); //
}
public String getPermissionCode() {
return this.actionName + "_" + this.entityName;
}
public void markAsAwaitingApproval() {
this.status = CommandProcessingResultType.AWAITING_APPROVAL.getValue();
@NotNull
public CommandProcessingResultType getStatusEnum() {
return CommandProcessingResultType.fromInt(status);
}
public boolean isMarkedAsAwaitingApproval() {
return this.status.equals(CommandProcessingResultType.AWAITING_APPROVAL.getValue());
public void setStatus(@NotNull CommandProcessingResultType status) {
this.status = status.getValue();
}
public void markAsAwaitingApproval() {
setStatus(CommandProcessingResultType.AWAITING_APPROVAL);
}
public boolean isAwaitingApproval() {
return getStatusEnum().isAwaitingApproval();
}
public boolean isProcessed() {
return getStatusEnum().isProcessed();
}
public void markAsChecked(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getAuditOffsetDateTime();
this.status = CommandProcessingResultType.PROCESSED.getValue();
setStatus(CommandProcessingResultType.PROCESSED);
}
public boolean isChecked() {
return checker != null && isProcessed();
}
public void markAsRejected(final AppUser checker) {
this.checker = checker;
this.checkedOnDate = DateUtils.getAuditOffsetDateTime();
this.status = CommandProcessingResultType.REJECTED.getValue();
setStatus(CommandProcessingResultType.REJECTED);
}
public boolean isRejected() {
return checker != null && isRejected();
}
public void updateForAudit(final CommandProcessingResult result) {
@@ -18,6 +18,7 @@
*/
package org.apache.fineract.commands.domain;
import java.util.Set;
import lombok.Getter;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.useradministration.api.PasswordPreferencesApiConstants;
@@ -45,6 +46,7 @@ public class CommandWrapper {
private final Long organisationCreditBureauId;
private final String jobName;
private final ExternalId loanExternalId;
private final Set<String> sanitizeJsonKeys;
private final String idempotencyKey;
@@ -67,7 +69,7 @@ public class CommandWrapper {
final ExternalId loanExternalId) {
return new CommandWrapper(commandId, actionName, entityName, resourceId, subresourceId, resourceGetUrl, productId, officeId,
groupId, clientId, loanId, savingsId, transactionId, creditBureauId, organisationCreditBureauId, idempotencyKey,
loanExternalId);
loanExternalId, null);
}
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId,
@@ -92,12 +94,14 @@ public class CommandWrapper {
this.jobName = null;
this.idempotencyKey = null;
this.loanExternalId = null;
this.sanitizeJsonKeys = null;
}
public CommandWrapper(final Long officeId, final Long groupId, final Long clientId, final Long loanId, final Long savingsId,
final String actionName, final String entityName, final Long entityId, final Long subentityId, final String href,
final String json, final String transactionId, final Long productId, final Long templateId, final Long creditBureauId,
final Long organisationCreditBureauId, final String jobName, final String idempotencyKey, final ExternalId loanExternalId) {
final Long organisationCreditBureauId, final String jobName, final String idempotencyKey, final ExternalId loanExternalId,
final Set<String> sanitizeJsonKeys) {
this.commandId = null;
this.officeId = officeId;
@@ -120,12 +124,14 @@ public class CommandWrapper {
this.jobName = jobName;
this.idempotencyKey = idempotencyKey;
this.loanExternalId = loanExternalId;
this.sanitizeJsonKeys = sanitizeJsonKeys;
}
private CommandWrapper(final Long commandId, final String actionName, final String entityName, final Long resourceId,
final Long subresourceId, final String resourceGetUrl, final Long productId, final Long officeId, final Long groupId,
final Long clientId, final Long loanId, final Long savingsId, final String transactionId, final Long creditBureauId,
final Long organisationCreditBureauId, final String idempotencyKey, final ExternalId loanExternalId) {
final Long organisationCreditBureauId, final String idempotencyKey, final ExternalId loanExternalId,
final Set<String> sanitizeJsonKeys) {
this.commandId = commandId;
this.officeId = officeId;
@@ -147,6 +153,7 @@ public class CommandWrapper {
this.jobName = null;
this.idempotencyKey = idempotencyKey;
this.loanExternalId = loanExternalId;
this.sanitizeJsonKeys = sanitizeJsonKeys;
}
public boolean isCreate() {
@@ -200,21 +207,24 @@ public class CommandWrapper {
return isnoteResource;
}
public boolean isUpdateOfOwnUserDetails(final Long loggedInUserId) {
return isUserResource() && isUpdate() && loggedInUserId.equals(this.entityId);
public boolean isChangeOfOwnUserDetails(final Long loggedInUserId) {
return isUserResource() && loggedInUserId.equals(this.entityId) && (isUpdate() || isChangePasswordOperation());
}
public boolean isUpdate() {
// permissions resource has special update which involves no resource.
return (isPermissionResource() && isUpdateOperation()) || (isCurrencyResource() && isUpdateOperation())
|| (isCacheResource() && isUpdateOperation()) || (isWorkingDaysResource() && isUpdateOperation())
|| (isPasswordPreferencesResource() && isUpdateOperation()) || (isUpdateOperation() && (this.entityId != null));
// some resources have special update which involves no resource identifier.
return isUpdateOperation() && (this.entityId != null || isPermissionResource() || isCurrencyResource() || isCacheResource()
|| isWorkingDaysResource() || isPasswordPreferencesResource());
}
public boolean isCacheResource() {
return this.entityName.equalsIgnoreCase("CACHE");
}
public boolean isChangePasswordOperation() {
return this.actionName.equalsIgnoreCase("CHANGEPWD");
}
public boolean isUpdateOperation() {
return this.actionName.equalsIgnoreCase("UPDATE");
}
@@ -20,6 +20,9 @@ package org.apache.fineract.commands.service;
import static org.apache.fineract.commands.domain.CommandProcessingResultType.UNDER_PROCESSING;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.batch.exception.ErrorInfo;
import org.apache.fineract.commands.domain.CommandSource;
@@ -28,10 +31,13 @@ import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.commands.exception.CommandNotFoundException;
import org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessUnderProcessingException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.useradministration.domain.AppUser;
import org.jetbrains.annotations.NotNull;
import org.springframework.orm.jpa.JpaSystemException;
@@ -49,8 +55,13 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
public class CommandSourceService {
public static final String COMMAND_MASK_VALUE = "***";
public static final String COMMAND_SANITIZE_ALL = "SANITIZE_ALL";
private final ConfigurationDomainService configurationDomainService;
private final CommandSourceRepository commandSourceRepository;
private final ErrorHandler errorHandler;
private final FromJsonHelper fromApiJsonHelper;
@NotNull
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.REPEATABLE_READ)
@@ -110,7 +121,8 @@ public class CommandSourceService {
public CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
CommandSource commandSourceResult = CommandSource.fullEntryFrom(wrapper, jsonCommand, maker, idempotencyKey,
UNDER_PROCESSING.getValue());
UNDER_PROCESSING.getValue(), false);
sanitizeJson(commandSourceResult, wrapper.getSanitizeJsonKeys());
if (commandSourceResult.getCommandAsJson() == null) {
commandSourceResult.setCommandAsJson("{}");
}
@@ -119,13 +131,51 @@ public class CommandSourceService {
@Transactional
public CommandProcessingResult processCommand(NewCommandSourceHandler handler, JsonCommand command, CommandSource commandSource,
AppUser user, boolean isApprovedByChecker, boolean isMakerChecker) {
AppUser user, boolean isApprovedByChecker) {
final CommandProcessingResult result = handler.processCommand(command);
boolean isRollback = !isApprovedByChecker && !user.isCheckerSuperUser() && (isMakerChecker || result.isRollbackTransaction());
if (isRollback) {
commandSource.markAsAwaitingApproval();
throw new RollbackTransactionNotApprovedException(commandSource.getId(), commandSource.getResourceId());
String permission = commandSource.getPermissionCode();
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
if (isMakerChecker || result.isRollbackTransaction()) {
if (isApprovedByChecker || user.isCheckerSuperUser()) {
commandSource.markAsChecked(user);
} else {
if (commandSource.isSanitized()) {
throw new GeneralPlatformDomainRuleException("error.msg.invalid.sanitization",
"Maker-checker command can not be sanitized, please change the permission configuration", permission);
}
commandSource.markAsAwaitingApproval();
throw new RollbackTransactionNotApprovedException(commandSource.getId(), commandSource.getResourceId());
}
}
return result;
}
private void sanitizeJson(@NotNull CommandSource commandSource, Set<String> sanitizeKeys) {
if (sanitizeKeys == null || sanitizeKeys.isEmpty()) {
return;
}
String commandAsJson = commandSource.getCommandAsJson();
if (commandAsJson == null || commandAsJson.isEmpty()) {
return;
}
final JsonElement parsedCommand = this.fromApiJsonHelper.parse(commandAsJson);
if (!parsedCommand.isJsonObject()) {
return;
}
String sanitizedJson;
if (sanitizeKeys.contains(COMMAND_SANITIZE_ALL)) {
sanitizedJson = "";
} else {
JsonObject jsonObject = parsedCommand.getAsJsonObject();
for (String key : sanitizeKeys) {
if (jsonObject.has(key)) {
jsonObject.addProperty(key, COMMAND_MASK_VALUE);
}
}
sanitizedJson = jsonObject.toString();
}
commandSource.setCommandAsJson(sanitizedJson);
commandSource.setSanitized(true);
}
}
@@ -18,7 +18,13 @@
*/
package org.apache.fineract.commands.service;
import static org.apache.fineract.useradministration.service.AppUserConstants.PASSWORD;
import static org.apache.fineract.useradministration.service.AppUserConstants.REPEAT_PASSWORD;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.infrastructure.accountnumberformat.service.AccountNumberFormatConstants;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
@@ -49,18 +55,21 @@ public class CommandWrapperBuilder {
private String jobName;
private String idempotencyKey;
private ExternalId loanExternalId;
private Set<String> sanitizeJsonKeys;
@SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD", justification = "TODO: fix this!")
public CommandWrapper build() {
return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, this.entityName,
this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId,
this.creditBureauId, this.organisationCreditBureauId, this.jobName, this.idempotencyKey, this.loanExternalId);
this.creditBureauId, this.organisationCreditBureauId, this.jobName, this.idempotencyKey, this.loanExternalId,
this.sanitizeJsonKeys);
}
public CommandWrapper build(String idempotencyKey) {
return new CommandWrapper(this.officeId, this.groupId, this.clientId, this.loanId, this.savingsId, this.actionName, this.entityName,
this.entityId, this.subentityId, this.href, this.json, this.transactionId, this.productId, this.templateId,
this.creditBureauId, this.organisationCreditBureauId, this.jobName, idempotencyKey, this.loanExternalId);
this.creditBureauId, this.organisationCreditBureauId, this.jobName, idempotencyKey, this.loanExternalId,
this.sanitizeJsonKeys);
}
public CommandWrapperBuilder updateCreditBureau() {
@@ -264,6 +273,16 @@ public class CommandWrapperBuilder {
this.entityName = "USER";
this.entityId = null;
this.href = "/users/template";
this.sanitizeJsonKeys = new HashSet<>(Arrays.asList(PASSWORD, REPEAT_PASSWORD));
return this;
}
public CommandWrapperBuilder changeUserPassword(final Long userId) {
this.actionName = "CHANGEPWD";
this.entityName = "USER";
this.entityId = userId;
this.href = "/users/" + userId + "/pwd";
this.sanitizeJsonKeys = new HashSet<>(Arrays.asList(PASSWORD, REPEAT_PASSWORD));
return this;
}
@@ -272,6 +291,7 @@ public class CommandWrapperBuilder {
this.entityName = "USER";
this.entityId = userId;
this.href = "/users/" + userId;
this.sanitizeJsonKeys = new HashSet<>(Arrays.asList(PASSWORD, REPEAT_PASSWORD));
return this;
}
@@ -55,7 +55,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
boolean isApprovedByChecker = false;
// check if is update of own account details
if (wrapper.isUpdateOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) {
if (wrapper.isChangeOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) {
// then allow this operation to proceed.
// maker checker doesnt mean anything here.
isApprovedByChecker = true; // set to true in case permissions have
@@ -117,7 +117,7 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
private CommandSource validateMakerCheckerTransaction(final Long makerCheckerId) {
final CommandSource commandSource = this.commandSourceRepository.findById(makerCheckerId)
.orElseThrow(() -> new CommandNotFoundException(makerCheckerId));
if (!commandSource.isMarkedAsAwaitingApproval()) {
if (!commandSource.isAwaitingApproval()) {
throw new CommandNotAwaitingApprovalException(makerCheckerId);
}
AppUser appUser = this.context.authenticatedUser();
@@ -117,16 +117,11 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
storeCommandIdInContext(commandSource); // Store command id as a request attribute
}
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName());
if (isApprovedByChecker || (isMakerChecker && user.isCheckerSuperUser())) {
commandSource.markAsChecked(user);
}
setIdempotencyKeyStoreFlag(true);
final CommandProcessingResult result;
try {
result = commandSourceService.processCommand(findCommandHandler(wrapper), command, commandSource, user, isApprovedByChecker,
isMakerChecker);
result = commandSourceService.processCommand(findCommandHandler(wrapper), command, commandSource, user, isApprovedByChecker);
} catch (Throwable t) { // NOSONAR
RuntimeException mappable = ErrorHandler.getMappable(t);
ErrorInfo errorInfo = commandSourceService.generateErrorInfo(mappable);
@@ -134,7 +129,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
commandSource.setResultStatusCode(statusCode);
commandSource.setResult(errorInfo.getMessage());
if (statusCode != SC_OK) {
commandSource.setStatus(ERROR.getValue());
commandSource.setStatus(ERROR);
}
if (!isEnclosingTransaction) { // TODO: temporary solution
commandSource = commandSourceService.saveResultNewTransaction(commandSource);
@@ -148,7 +143,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
commandSource.setResultStatusCode(SC_OK);
commandSource.updateForAudit(result);
commandSource.setResult(toApiResultJsonSerializer.serializeResult(result));
commandSource.setStatus(PROCESSED.getValue());
commandSource.setStatus(PROCESSED);
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
storeCommandIdInContext(commandSource); // Store command id as a request attribute
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.useradministration.domain;
import static org.apache.fineract.useradministration.service.AppUserConstants.PASSWORD;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@@ -195,8 +197,27 @@ public class AppUser extends AbstractPersistableCustom<Long> implements Platform
return organisationalRole;
}
public Map<String, Object> changePassword(final JsonCommand command, final PlatformPasswordEncoder platformPasswordEncoder) {
// unencoded password provided
final Map<String, Object> actualChanges = new LinkedHashMap<>(1);
updatePassword(command, platformPasswordEncoder, actualChanges);
return actualChanges;
}
private void updatePassword(JsonCommand command, PlatformPasswordEncoder platformPasswordEncoder, Map<String, Object> actualChanges) {
final String passwordParamName = PASSWORD;
if (command.hasParameter(passwordParamName)) {
if (command.isChangeInPasswordParameterNamed(passwordParamName, this.password, platformPasswordEncoder, getId())) {
final String passwordEncodedValue = command.passwordValueOfParameterNamed(passwordParamName, platformPasswordEncoder,
getId());
actualChanges.put(passwordParamName, true);
updatePassword(passwordEncodedValue);
}
}
}
public void updatePassword(final String encodePassword) {
if (cannotChangePassword != null && cannotChangePassword == true) {
if (Boolean.TRUE.equals(cannotChangePassword)) {
throw new NoAuthorizationException("Password of this user may not be modified");
}
@@ -223,27 +244,10 @@ public class AppUser extends AbstractPersistableCustom<Long> implements Platform
public Map<String, Object> update(final JsonCommand command, final PlatformPasswordEncoder platformPasswordEncoder,
final Collection<Client> clients) {
final Map<String, Object> actualChanges = new LinkedHashMap<>(7);
// unencoded password provided
final String passwordParamName = "password";
final String passwordEncodedParamName = "passwordEncoded";
if (command.hasParameter(passwordParamName)) {
if (command.isChangeInPasswordParameterNamed(passwordParamName, this.password, platformPasswordEncoder, getId())) {
final String passwordEncodedValue = command.passwordValueOfParameterNamed(passwordParamName, platformPasswordEncoder,
getId());
updatePassword(passwordEncodedValue);
}
}
if (command.hasParameter(passwordEncodedParamName)) {
if (command.isChangeInStringParameterNamed(passwordEncodedParamName, this.password)) {
final String newValue = command.stringValueOfParameterNamed(passwordEncodedParamName);
updatePassword(newValue);
}
}
updatePassword(command, platformPasswordEncoder, actualChanges);
final String officeIdParamName = "officeId";
if (command.isChangeInLongParameterNamed(officeIdParamName, this.office.getId())) {
final Long newValue = command.longValueOfParameterNamed(officeIdParamName);
@@ -24,6 +24,8 @@ public final class AppUserConstants {
}
public static final String PASSWORD = "password";
public static final String REPEAT_PASSWORD = "repeatPassword";
public static final String PASSWORD_NEVER_EXPIRES = "passwordNeverExpires";
public static final String IS_SELF_SERVICE_USER = "isSelfServiceUser";
public static final String CLIENTS = "clients";
@@ -58,8 +58,8 @@ public class IdempotencyCommandProcessFailedExceptionTest {
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);
CommandSource source = CommandSource.fullEntryFrom(command, JsonCommand.from("{}"), null, "dummy-key", null);
null, null, null, null, null, null);
CommandSource source = CommandSource.fullEntryFrom(command, JsonCommand.from("{}"), null, "dummy-key", null, false);
IdempotentCommandProcessFailedException exception = new IdempotentCommandProcessFailedException(command, null, source);
Response result = mapper.toResponse(exception);
assertEquals(500, result.getStatus());
@@ -268,7 +268,7 @@ public class CenterImportHandler implements ImportHandler {
String payload = gsonBuilder.create().toJson(calendarData);
CommandWrapper commandWrapper = new CommandWrapper(result.getOfficeId(), result.getGroupId(), result.getClientId(),
result.getLoanId(), result.getSavingsId(), null, null, null, null, null, payload, result.getTransactionId(),
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create(), null);
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create(), null, null);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createCalendar(commandWrapper, TemplatePopulateImportConstants.CENTER_ENTITY_TYPE, result.getGroupId()) //
.withJson(payload) //
@@ -252,7 +252,7 @@ public class GroupImportHandler implements ImportHandler {
CommandWrapper commandWrapper = new CommandWrapper(result.getOfficeId(), result.getGroupId(), result.getClientId(),
result.getLoanId(), result.getSavingsId(), null, null, null, null, null, payload, result.getTransactionId(),
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create(), null);
result.getProductId(), null, null, null, null, idempotencyKeyGenerator.create(), null, null);
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.createCalendar(commandWrapper, TemplatePopulateImportConstants.CENTER_ENTITY_TYPE, result.getGroupId()) //
.withJson(payload) //
@@ -37,7 +37,7 @@ public class InteropWrapperBuilder {
public CommandWrapper build() {
return new CommandWrapper(null, null, null, null, null, actionName, entityName, null, null, href, json, null, null, null, null,
null, null, null, null);
null, null, null, null, null);
}
public InteropWrapperBuilder withJson(final String json) {
@@ -171,7 +171,7 @@ public class UsersApiResource {
@PUT
@Path("{userId}")
@Operation(summary = "Update a User", description = "When updating a password you must provide the repeatPassword parameter also.")
@Operation(summary = "Update a User", description = "Updates the user")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = UsersApiResourceSwagger.PutUsersUserIdRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = UsersApiResourceSwagger.PutUsersUserIdResponse.class))) })
@@ -190,6 +190,27 @@ public class UsersApiResource {
return this.toApiJsonSerializer.serialize(result);
}
@POST
@Path("{userId}/pwd")
@Operation(summary = "Change the password of a User", description = "When updating a password you must provide the repeatPassword parameter also.")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = UsersApiResourceSwagger.ChangePwdUsersUserIdRequest.class)))
@ApiResponses({
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = UsersApiResourceSwagger.ChangePwdUsersUserIdResponse.class))) })
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
public String changePassword(@PathParam("userId") @Parameter(description = "userId") final Long userId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
.changeUserPassword(userId) //
.withJson(apiRequestBodyAsJson) //
.build();
final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
return this.toApiJsonSerializer.serialize(result);
}
@DELETE
@Path("{userId}")
@Operation(summary = "Delete a User", description = "Removes the user and the associated roles and permissions.")
@@ -151,6 +151,43 @@ final class UsersApiResourceSwagger {
public Long resourceId;
}
@Schema(description = "ChangePwdUsersUserIdRequest")
public static final class ChangePwdUsersUserIdRequest {
private ChangePwdUsersUserIdRequest() {
}
@Schema(example = "password")
public String password;
@Schema(example = "repeatPassword")
public String repeatPassword;
}
@Schema(description = "ChangePwdUsersUserIdResponse")
public static final class ChangePwdUsersUserIdResponse {
private ChangePwdUsersUserIdResponse() {
}
static final class ChangePwdUsersUserIdResponseChanges {
private ChangePwdUsersUserIdResponseChanges() {
}
@Schema(example = "true")
public boolean password;
}
@Schema(example = "1")
public Long officeId;
@Schema(example = "11")
public Long resourceId;
public ChangePwdUsersUserIdResponseChanges changes;
}
@Schema(description = "PutUsersUserIdRequest")
public static final class PutUsersUserIdRequest {
@@ -0,0 +1,47 @@
/**
* 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.useradministration.handler;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.useradministration.service.AppUserWritePlatformService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@CommandType(entity = "USER", action = "CHANGEPWD")
public class ChangeUserPasswordCommandHandler implements NewCommandSourceHandler {
private final AppUserWritePlatformService writePlatformService;
@Autowired
public ChangeUserPasswordCommandHandler(final AppUserWritePlatformService writePlatformService) {
this.writePlatformService = writePlatformService;
}
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
final Long userId = command.entityId();
return this.writePlatformService.changeUserPassword(userId, command);
}
}
@@ -25,6 +25,8 @@ public interface AppUserWritePlatformService {
CommandProcessingResult createUser(JsonCommand command);
CommandProcessingResult changeUserPassword(Long userId, JsonCommand command);
CommandProcessingResult updateUser(Long userId, JsonCommand command);
CommandProcessingResult deleteUser(Long userId);
@@ -154,6 +154,36 @@ public class AppUserWritePlatformServiceJpaRepositoryImpl implements AppUserWrit
}
}
@Override
@Transactional
@Caching(evict = { @CacheEvict(value = "users", allEntries = true), @CacheEvict(value = "usersByUsername", allEntries = true) })
public CommandProcessingResult changeUserPassword(final Long userId, final JsonCommand command) {
try {
this.context.authenticatedUser(new CommandWrapperBuilder().updateUser(null).build());
this.fromApiJsonDeserializer.validateForChangePassword(command.json(), this.context.authenticatedUser());
final AppUser userToUpdate = this.appUserRepository.findById(userId).orElseThrow(() -> new UserNotFoundException(userId));
final AppUserPreviousPassword currentPasswordToSaveAsPreview = getCurrentPasswordToSaveAsPreview(userToUpdate, command);
final Map<String, Object> changes = userToUpdate.changePassword(command, this.platformPasswordEncoder);
if (!changes.isEmpty()) {
this.appUserRepository.saveAndFlush(userToUpdate);
if (currentPasswordToSaveAsPreview != null) {
this.appUserPreviewPasswordRepository.save(currentPasswordToSaveAsPreview);
}
}
return new CommandProcessingResultBuilder() //
.withEntityId(userId) //
.withOfficeId(userToUpdate.getOffice().getId()) //
.with(changes) //
.build();
} catch (final DataIntegrityViolationException dve) {
throw handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve);
} catch (final JpaSystemException | PersistenceException | AuthenticationServiceException dve) {
log.error("changeUserPassword: JpaSystemException | PersistenceException | AuthenticationServiceException", dve);
Throwable throwable = ExceptionUtils.getRootCause(dve.getCause());
throw handleDataIntegrityIssues(command, throwable, dve);
}
}
@Override
@Transactional
@Caching(evict = { @CacheEvict(value = "users", allEntries = true), @CacheEvict(value = "usersByUsername", allEntries = true) })
@@ -19,6 +19,8 @@
package org.apache.fineract.useradministration.service;
import static org.apache.fineract.useradministration.service.AppUserConstants.CLIENTS;
import static org.apache.fineract.useradministration.service.AppUserConstants.PASSWORD;
import static org.apache.fineract.useradministration.service.AppUserConstants.REPEAT_PASSWORD;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
@@ -49,8 +51,6 @@ public final class UserDataValidator {
public static final String USERNAME = "username";
public static final String FIRSTNAME = "firstname";
public static final String LASTNAME = "lastname";
public static final String PASSWORD = "password";
public static final String REPEAT_PASSWORD = "repeatPassword";
public static final String EMAIL = "email";
public static final String OFFICE_ID = "officeId";
public static final String NOT_SELECTED_ROLES = "notSelectedRoles";
@@ -61,9 +61,13 @@ public final class UserDataValidator {
/**
* The parameters supported for this command.
*/
private static final Set<String> SUPPORTED_PARAMETERS = new HashSet<>(
private static final Set<String> CREATE_SUPPORTED_PARAMETERS = new HashSet<>(
Arrays.asList(USERNAME, FIRSTNAME, LASTNAME, PASSWORD, REPEAT_PASSWORD, EMAIL, OFFICE_ID, NOT_SELECTED_ROLES, ROLES,
SEND_PASSWORD_TO_EMAIL, STAFF_ID, PASSWORD_NEVER_EXPIRES, AppUserConstants.IS_SELF_SERVICE_USER, CLIENTS));
private static final Set<String> UPDATE_SUPPORTED_PARAMETERS = new HashSet<>(
Arrays.asList(USERNAME, FIRSTNAME, LASTNAME, PASSWORD, REPEAT_PASSWORD, EMAIL, OFFICE_ID, NOT_SELECTED_ROLES, ROLES,
SEND_PASSWORD_TO_EMAIL, STAFF_ID, PASSWORD_NEVER_EXPIRES, AppUserConstants.IS_SELF_SERVICE_USER, CLIENTS));
private static final Set<String> CHANGE_PASSWORD_SUPPORTED_PARAMETERS = new HashSet<>(Arrays.asList(PASSWORD, REPEAT_PASSWORD));
public static final String PASSWORD_NEVER_EXPIRE = "passwordNeverExpire";
private final FromJsonHelper fromApiJsonHelper;
@@ -84,7 +88,7 @@ public final class UserDataValidator {
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_SUPPORTED_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("user");
@@ -106,16 +110,7 @@ public final class UserDataValidator {
final String email = this.fromApiJsonHelper.extractStringNamed(EMAIL, element);
baseDataValidator.reset().parameter(EMAIL).value(email).notBlank().notExceedingLengthOf(100);
} else {
final String password = this.fromApiJsonHelper.extractStringNamed(PASSWORD, element);
final String repeatPassword = this.fromApiJsonHelper.extractStringNamed(REPEAT_PASSWORD, element);
final PasswordValidationPolicy validationPolicy = this.passwordValidationPolicy.findActivePasswordValidationPolicy();
final String regex = validationPolicy.getRegex();
final String description = validationPolicy.getDescription();
baseDataValidator.reset().parameter(PASSWORD).value(password).matchesRegularExpression(regex, description);
if (StringUtils.isNotBlank(password)) {
baseDataValidator.reset().parameter(PASSWORD).value(password).equalToParameter(REPEAT_PASSWORD, repeatPassword);
}
validatePassword(baseDataValidator, element);
}
} else {
baseDataValidator.reset().parameter(SEND_PASSWORD_TO_EMAIL).value(sendPasswordToEmail).trueOrFalseRequired(false);
@@ -196,6 +191,27 @@ public final class UserDataValidator {
}
}
public void validateForChangePassword(final String json, AppUser authenticatedUser) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
}
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CHANGE_PASSWORD_SUPPORTED_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("user");
final JsonElement element = this.fromApiJsonHelper.parse(json);
validatePassword(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
validateFieldLevelACL(json, authenticatedUser);
}
public void validateForUpdate(final String json, AppUser authenticatedUser) {
if (StringUtils.isBlank(json)) {
throw new InvalidJsonException();
@@ -204,7 +220,7 @@ public final class UserDataValidator {
final Type typeOfMap = new TypeToken<Map<String, Object>>() {
}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, SUPPORTED_PARAMETERS);
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_SUPPORTED_PARAMETERS);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("user");
@@ -247,17 +263,7 @@ public final class UserDataValidator {
}
if (this.fromApiJsonHelper.parameterExists(PASSWORD, element)) {
final String password = this.fromApiJsonHelper.extractStringNamed(PASSWORD, element);
final String repeatPassword = this.fromApiJsonHelper.extractStringNamed(REPEAT_PASSWORD, element);
final PasswordValidationPolicy validationPolicy = this.passwordValidationPolicy.findActivePasswordValidationPolicy();
final String regex = validationPolicy.getRegex();
final String description = validationPolicy.getDescription();
baseDataValidator.reset().parameter(PASSWORD).value(password).matchesRegularExpression(regex, description);
if (StringUtils.isNotBlank(password)) {
baseDataValidator.reset().parameter(PASSWORD).value(password).equalToParameter(REPEAT_PASSWORD, repeatPassword);
}
validatePassword(baseDataValidator, element);
}
if (this.fromApiJsonHelper.parameterExists(PASSWORD_NEVER_EXPIRE, element)) {
@@ -291,4 +297,18 @@ public final class UserDataValidator {
throwExceptionIfValidationWarningsExist(dataValidationErrors);
validateFieldLevelACL(json, authenticatedUser);
}
private void validatePassword(DataValidatorBuilder baseDataValidator, JsonElement element) {
final String password = this.fromApiJsonHelper.extractStringNamed(PASSWORD, element);
final String repeatPassword = this.fromApiJsonHelper.extractStringNamed(REPEAT_PASSWORD, element);
final PasswordValidationPolicy validationPolicy = this.passwordValidationPolicy.findActivePasswordValidationPolicy();
final String regex = validationPolicy.getRegex();
final String description = validationPolicy.getDescription();
DataValidatorBuilder validator = baseDataValidator.reset().parameter(PASSWORD).value(password).matchesRegularExpression(regex,
description);
if (StringUtils.isNotBlank(password)) {
validator.equalToParameter(REPEAT_PASSWORD, repeatPassword);
}
}
}
@@ -191,4 +191,5 @@
<include file="parts/0170_days_in_year_custom_strategy.xml" relativeToChangelogFile="true"/>
<include file="parts/0171_loan_capitalized_income.xml" relativeToChangelogFile="true"/>
<include file="parts/0172_create_loan_capitalized_income_balance.xml" relativeToChangelogFile="true"/>
<include file="parts/0173_user_change_pwd.xml" relativeToChangelogFile="true" />
</databaseChangeLog>
@@ -89,4 +89,13 @@
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="3">
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DEACTIVATEOVERDUE_LOANCHARGE"/>
<column name="entity_name" value="LOANCHARGE"/>
<column name="action_name" value="DEACTIVATEOVERDUE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet author="fineract" id="1">
<addColumn tableName="m_portfolio_command_source">
<column name="is_sanitized" type="boolean" defaultValueBoolean="false" >
<constraints nullable="false"/>
</column>
</addColumn>
</changeSet>
<changeSet author="fineract" id="2">
<insert tableName="m_permission">
<column name="grouping" value="authorisation"/>
<column name="code" value="CHANGEPWD_USER"/>
<column name="entity_name" value="USER"/>
<column name="action_name" value="CHANGEPWD"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet author="fineract" id="3">
<sql>
INSERT INTO m_role_permission (role_id, permission_id)
SELECT mrp.role_id, mp_changepwd.id
FROM m_role_permission mrp
LEFT JOIN m_permission mp_update ON mp_update.id = mrp.permission_id
LEFT JOIN m_permission mp_changepwd ON mp_changepwd.action_name = 'CHANGEPWD'
AND mp_changepwd.entity_name = 'USER'
WHERE mp_update.action_name = 'UPDATE'
AND mp_update.entity_name = 'USER';
</sql>
</changeSet>
</databaseChangeLog>
@@ -39423,6 +39423,40 @@ Request body:
</div>
</div>
<a id="users_changepwd" name="users_changepwd" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
<h4>Change the password of a User</h4>
<p>
<b>Note:</b> When updating a password you must provide the
repeatPassword parameter also.
</p>
</div>
<div class="method-example">
<code class="method-declaration">
POST https://DomainName/api/v1/users/{userId)/pwd
</code>
<code class="method-request">
POST users/3/pwd
Content-Type: application/json
Request body:
{
"password": "window75",
"repeatPassword": "window75"
}
</code>
<code class="method-response">
{
"officeId": 1,
"resourceId": 3,
"changes": {
"passwordEncoded": "abc3326b1bb376351c7baeb4175f5e0504e33aadf6a158474a6d71de1befae51"
}
}
</code>
</div>
</div>
<a id="users_update" name="users_update" class="old-syle-anchor">&nbsp;</a>
<div class="method-section">
<div class="method-description">
@@ -39451,8 +39485,7 @@ Request body:
"officeId": 1,
"resourceId": 3,
"changes": {
"firstname": "Test",
"passwordEncoded": "abc3326b1bb376351c7baeb4175f5e0504e33aadf6a158474a6d71de1befae51"
"firstname": "Test"
}
}
</code>
@@ -101,7 +101,7 @@ public class CommandServiceStepDefinitions implements En {
public DummyCommand() {
super(null, null, null, null, null, null, null, null, null, null, "{}", null, null, null, null, null, null,
UUID.randomUUID().toString(), null);
UUID.randomUUID().toString(), null, null);
}
@Override
@@ -29,6 +29,7 @@ import org.apache.fineract.commands.domain.CommandSource;
import org.apache.fineract.commands.domain.CommandSourceRepository;
import org.apache.fineract.commands.domain.CommandWrapper;
import org.apache.fineract.infrastructure.codes.exception.CodeNotFoundException;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
@@ -48,6 +49,9 @@ import org.mockito.MockitoAnnotations;
@SuppressFBWarnings(value = "RV_EXCEPTION_NOT_THROWN", justification = "False positive")
public class CommandSourceServiceTest {
@Mock
private ConfigurationDomainService configurationDomainService;
@Mock
private CommandSourceRepository commandSourceRepository;
@@ -77,7 +77,7 @@ public class IdempotencyKeyResolverTest {
public void testIPKResolveFromWrapper() {
String idk = "idk";
CommandWrapper wrapper = new CommandWrapper(null, null, null, null, null, null, null, null, null, null, null, null, null, null,
null, null, null, idk, null);
null, null, null, idk, null, null);
String resolvedIdk = underTest.resolve(wrapper);
Assertions.assertEquals(idk, resolvedIdk);
}
@@ -117,7 +117,7 @@ public class SynchronousCommandProcessingServiceTest {
when(commandSource.getStatus()).thenReturn(CommandProcessingResultType.PROCESSED.getValue());
when(context.authenticatedUser(Mockito.any(CommandWrapper.class))).thenReturn(appUser);
when(commandSourceService.processCommand(commandHandler, jsonCommand, commandSource, appUser, false, false))
when(commandSourceService.processCommand(commandHandler, jsonCommand, commandSource, appUser, false))
.thenReturn(commandProcessingResult);
CommandProcessingResult actualCommandProcessingResult = underTest.executeCommand(commandWrapper, jsonCommand, false);
@@ -163,8 +163,7 @@ public class SynchronousCommandProcessingServiceTest {
when(commandSourceService.findCommandSource(commandWrapper, idk)).thenReturn(initialCommandSource);
when(commandSourceService.processCommand(commandHandler, jsonCommand, commandSource, appUser, false, false))
.thenThrow(runtimeException);
when(commandSourceService.processCommand(commandHandler, jsonCommand, commandSource, appUser, false)).thenThrow(runtimeException);
assertThrows(RuntimeException.class, () -> {
underTest.executeCommand(commandWrapper, jsonCommand, false);
@@ -28,6 +28,8 @@ import io.restassured.specification.ResponseSpecification;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.apache.fineract.client.models.ChangePwdUsersUserIdRequest;
import org.apache.fineract.client.models.ChangePwdUsersUserIdResponse;
import org.apache.fineract.client.models.GetOfficesResponse;
import org.apache.fineract.client.models.GetUsersUserIdResponse;
import org.apache.fineract.client.models.PostUsersRequest;
@@ -183,7 +185,7 @@ public class UserAdministrationTest extends IntegrationTest {
}
@Test
public void testApplicationUserCanChangeOwnPassword() {
public void testApplicationUserCanUpdateOwnPassword() {
// Admin creates a new user with an empty role
Integer roleId = RolesHelper.createRole(requestSpec, responseSpec);
String originalPassword = "QwE!5rTy#9uP0";
@@ -215,6 +217,40 @@ public class UserAdministrationTest extends IntegrationTest {
GetUsersUserIdResponse ok = ok(newFineractClient(simpleUsername, updatedPassword).users.retrieveOne31(userId));
}
@Test
public void testApplicationUserCanChangeOwnPassword() {
// Admin creates a new user with an empty role
Integer roleId = RolesHelper.createRole(requestSpec, responseSpec);
String originalPassword = "QwE!5rTy#9uP0";
String simpleUsername = Utils.uniqueRandomStringGenerator("NotificationUser", 4);
GetOfficesResponse headOffice = OfficeHelper.getHeadOffice(requestSpec, responseSpec);
PostUsersRequest createUserRequest = new PostUsersRequest().username(simpleUsername)
.firstname(Utils.randomStringGenerator("NotificationFN", 4)).lastname(Utils.randomStringGenerator("NotificationLN", 4))
.email("whatever@mifos.org").password(originalPassword).repeatPassword(originalPassword).sendPasswordToEmail(false)
.officeId(headOffice.getId()).roles(List.of(Long.valueOf(roleId)));
PostUsersResponse userCreationResponse = UserHelper.createUser(requestSpec, responseSpec, createUserRequest);
Long userId = userCreationResponse.getResourceId();
Assertions.assertNotNull(userId);
// User changes its own password
String updatedPassword = "pX268-4Pfv|kF6";
ChangePwdUsersUserIdResponse changePwdUsersUserIdResponse = ok(newFineractClient(simpleUsername, originalPassword).users
.changePassword(userId, new ChangePwdUsersUserIdRequest().password(updatedPassword).repeatPassword(updatedPassword)));
Assertions.assertNotNull(changePwdUsersUserIdResponse.getResourceId());
// From then on the originalPassword is not working anymore
CallFailedRuntimeException callFailedRuntimeException = Assertions.assertThrows(CallFailedRuntimeException.class, () -> {
ok(newFineractClient(simpleUsername, originalPassword).users.retrieveOne31(userId));
});
Assertions.assertEquals(401, callFailedRuntimeException.getResponse().raw().code());
Assertions.assertTrue(callFailedRuntimeException.getMessage().contains("Unauthorized"));
// The update password is still working perfectly
GetUsersUserIdResponse ok = ok(newFineractClient(simpleUsername, updatedPassword).users.retrieveOne31(userId));
}
@Test
public void testApplicationUserShallNotBeAbleToChangeItsOwnRoles() {
// Admin creates a new user with one role assigned