FINERACT-2185: User management improvement
This commit is contained in:
committed by
Adam Saghy
parent
8d83b5547c
commit
79c006774e
+14
@@ -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) {
|
||||
|
||||
+19
-9
@@ -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");
|
||||
}
|
||||
|
||||
+56
-6
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-2
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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();
|
||||
|
||||
+3
-8
@@ -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
|
||||
|
||||
|
||||
+23
-19
@@ -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);
|
||||
|
||||
+2
@@ -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";
|
||||
|
||||
+2
-2
@@ -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());
|
||||
|
||||
+1
-1
@@ -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) //
|
||||
|
||||
+1
-1
@@ -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) //
|
||||
|
||||
+1
-1
@@ -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) {
|
||||
|
||||
+22
-1
@@ -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.")
|
||||
|
||||
+37
@@ -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 {
|
||||
|
||||
|
||||
+47
@@ -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);
|
||||
}
|
||||
}
|
||||
+2
@@ -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);
|
||||
|
||||
+30
@@ -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) })
|
||||
|
||||
+46
-26
@@ -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>
|
||||
|
||||
+9
@@ -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>
|
||||
|
||||
+53
@@ -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"> </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"> </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>
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
|
||||
+1
-1
@@ -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);
|
||||
}
|
||||
|
||||
+2
-3
@@ -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);
|
||||
|
||||
+37
-1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user