feat: implement maker-checker workflow for user creation and update requests
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s
This commit is contained in:
+4
-1
@@ -161,7 +161,10 @@ public class CommandSourceService {
|
|||||||
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "LOANPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "SAVINGSPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
|| "CURRENTPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())) {
|
|| "SHAREPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "FIXEDDEPOSITPRODUCT".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "USER".equalsIgnoreCase(commandSource.getEntityName())
|
||||||
|
|| "ROLE".equalsIgnoreCase(commandSource.getEntityName())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
+2
@@ -26,6 +26,8 @@ public interface ConfigurationDomainService {
|
|||||||
|
|
||||||
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
||||||
|
|
||||||
|
boolean isMakerCheckerEnabled();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
|
* Global kill-switch for MFA-factor-backed login enforcement. When true, users with at least
|
||||||
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
|
* one verified, non-disabled row in {@code m_user_mfa_factor} must complete a 2FA challenge
|
||||||
|
|||||||
+5
@@ -61,6 +61,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isMakerCheckerEnabled() {
|
||||||
|
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.MAKER_CHECKER).isEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<String> getAllowedLoanStatusesForExternalAssetTransfer() {
|
public List<String> getAllowedLoanStatusesForExternalAssetTransfer() {
|
||||||
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
||||||
|
|||||||
+67
@@ -88,6 +88,11 @@ public class UsersApiResource {
|
|||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
||||||
private final BulkImportWorkbookService bulkImportWorkbookService;
|
private final BulkImportWorkbookService bulkImportWorkbookService;
|
||||||
|
private final org.apache.fineract.useradministration.service.UserPendingWritePlatformService userPendingWritePlatformService;
|
||||||
|
private final org.apache.fineract.useradministration.service.UserPendingReadPlatformService userPendingReadPlatformService;
|
||||||
|
private final org.apache.fineract.infrastructure.core.serialization.FromJsonHelper fromApiJsonHelper;
|
||||||
|
private final org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService configurationDomainService;
|
||||||
|
private final DefaultToApiJsonSerializer<org.apache.fineract.useradministration.data.UserPendingRequestData> pendingRequestApiJsonSerializer;
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Operation(summary = "Retrieve list of users", description = "Example Requests:\n" + "\n" + "users\n" + "\n" + "\n"
|
@Operation(summary = "Retrieve list of users", description = "Example Requests:\n" + "\n" + "users\n" + "\n" + "\n"
|
||||||
@@ -158,6 +163,12 @@ public class UsersApiResource {
|
|||||||
@Consumes({ MediaType.APPLICATION_JSON })
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
public String create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
if (this.configurationDomainService.isMakerCheckerEnabled()) {
|
||||||
|
final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand
|
||||||
|
.fromJsonElement(null, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper);
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.submitCreateUser(command);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||||
.createUser() //
|
.createUser() //
|
||||||
@@ -179,6 +190,12 @@ public class UsersApiResource {
|
|||||||
@Produces({ MediaType.APPLICATION_JSON })
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
public String update(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
public String update(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
||||||
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
if (this.configurationDomainService.isMakerCheckerEnabled()) {
|
||||||
|
final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand
|
||||||
|
.fromJsonElement(userId, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper);
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.submitUpdateUser(userId, command);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
final CommandWrapper commandRequest = new CommandWrapperBuilder() //
|
||||||
.updateUser(userId) //
|
.updateUser(userId) //
|
||||||
@@ -190,6 +207,56 @@ public class UsersApiResource {
|
|||||||
return this.toApiJsonSerializer.serialize(result);
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("pending")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "List User creations & edits pending approval")
|
||||||
|
public org.apache.fineract.infrastructure.core.service.Page<org.apache.fineract.useradministration.data.UserPendingRequestData> retrieveAllPendingUsers(
|
||||||
|
@QueryParam("status") final String status,
|
||||||
|
@QueryParam("username") final String username,
|
||||||
|
@QueryParam("fromDate") final String fromDate,
|
||||||
|
@QueryParam("toDate") final String toDate,
|
||||||
|
@QueryParam("offset") final Integer offset,
|
||||||
|
@QueryParam("limit") final Integer limit) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
return this.userPendingReadPlatformService.retrieveAllPendingRequests(status, username, fromDate, toDate, offset, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("pending/{id}")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Retrieve a pending user request by ID")
|
||||||
|
public String retrievePendingUser(@PathParam("id") final Long id, @Context final UriInfo uriInfo) {
|
||||||
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
final org.apache.fineract.useradministration.data.UserPendingRequestData data = this.userPendingReadPlatformService.retrievePendingRequest(id);
|
||||||
|
final ApiRequestJsonSerializationSettings settings = this.apiRequestParameterHelper.process(uriInfo.getQueryParameters());
|
||||||
|
return this.pendingRequestApiJsonSerializer.serialize(settings, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("pending/{id}/approve")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Approve a pending User creation or update")
|
||||||
|
public String approvePendingUser(@PathParam("id") final Long id) {
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.approvePendingRequest(id);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("pending/{id}/reject")
|
||||||
|
@Consumes({ MediaType.APPLICATION_JSON })
|
||||||
|
@Produces({ MediaType.APPLICATION_JSON })
|
||||||
|
@Operation(summary = "Reject a pending User creation or update")
|
||||||
|
public String rejectPendingUser(@PathParam("id") final Long id, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
final org.apache.fineract.infrastructure.core.api.JsonCommand command = org.apache.fineract.infrastructure.core.api.JsonCommand
|
||||||
|
.fromJsonElement(id, this.fromApiJsonHelper.parse(apiRequestBodyAsJson), this.fromApiJsonHelper);
|
||||||
|
final CommandProcessingResult result = this.userPendingWritePlatformService.rejectPendingRequest(id, command);
|
||||||
|
return this.toApiJsonSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
@POST
|
@POST
|
||||||
@Path("{userId}/pwd")
|
@Path("{userId}/pwd")
|
||||||
@Operation(summary = "Change the password of a User", description = "When updating a password you must provide the repeatPassword parameter also.")
|
@Operation(summary = "Change the password of a User", description = "When updating a password you must provide the repeatPassword parameter also.")
|
||||||
|
|||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* 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.data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Builder
|
||||||
|
public class UserPendingRequestData implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
private final Long id;
|
||||||
|
private final Long userId;
|
||||||
|
private final String requestType;
|
||||||
|
private final String status;
|
||||||
|
private final String username;
|
||||||
|
private final String firstname;
|
||||||
|
private final String lastname;
|
||||||
|
private final String email;
|
||||||
|
private final String mobileNo;
|
||||||
|
private final Long officeId;
|
||||||
|
private final String officeName;
|
||||||
|
private final Long staffId;
|
||||||
|
private final String staffName;
|
||||||
|
private final String otpDeliveryMethod;
|
||||||
|
private final Boolean passwordNeverExpires;
|
||||||
|
private final Collection<String> roleNames;
|
||||||
|
private final Long makerId;
|
||||||
|
private final String makerUsername;
|
||||||
|
private final OffsetDateTime submittedDate;
|
||||||
|
private final Long checkerId;
|
||||||
|
private final String checkerUsername;
|
||||||
|
private final OffsetDateTime checkedDate;
|
||||||
|
private final String rejectReason;
|
||||||
|
private final Map<String, Object> originalValues;
|
||||||
|
private final Map<String, Object> proposedValues;
|
||||||
|
}
|
||||||
+113
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* 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.domain;
|
||||||
|
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "m_user_pending_request")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class UserPendingRequest extends AbstractPersistableCustom<Long> {
|
||||||
|
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Column(name = "request_type", nullable = false, length = 20)
|
||||||
|
private String requestType;
|
||||||
|
|
||||||
|
@Column(name = "status", nullable = false, length = 20)
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Column(name = "payload_json", nullable = false)
|
||||||
|
private String payloadJson;
|
||||||
|
|
||||||
|
@Column(name = "original_json")
|
||||||
|
private String originalJson;
|
||||||
|
|
||||||
|
@Column(name = "password_hash")
|
||||||
|
private String passwordHash;
|
||||||
|
|
||||||
|
@Column(name = "maker_id", nullable = false)
|
||||||
|
private Long makerId;
|
||||||
|
|
||||||
|
@Column(name = "submitted_date", nullable = false)
|
||||||
|
private OffsetDateTime submittedDate;
|
||||||
|
|
||||||
|
@Column(name = "checker_id")
|
||||||
|
private Long checkerId;
|
||||||
|
|
||||||
|
@Column(name = "checked_date")
|
||||||
|
private OffsetDateTime checkedDate;
|
||||||
|
|
||||||
|
@Column(name = "reject_reason", length = 500)
|
||||||
|
private String rejectReason;
|
||||||
|
|
||||||
|
@Column(name = "pending_lock_key", length = 100, unique = true)
|
||||||
|
private String pendingLockKey;
|
||||||
|
|
||||||
|
public static UserPendingRequest create(final UserPendingRequestType requestType, final Long userId,
|
||||||
|
final String payloadJson, final String originalJson, final String passwordHash,
|
||||||
|
final Long makerId, final String pendingLockKey) {
|
||||||
|
final UserPendingRequest request = new UserPendingRequest();
|
||||||
|
request.requestType = requestType.name();
|
||||||
|
request.userId = userId;
|
||||||
|
request.status = UserPendingRequestStatus.PENDING.name();
|
||||||
|
request.payloadJson = payloadJson;
|
||||||
|
request.originalJson = originalJson;
|
||||||
|
request.passwordHash = passwordHash;
|
||||||
|
request.makerId = makerId;
|
||||||
|
request.submittedDate = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
request.pendingLockKey = pendingLockKey;
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPending() {
|
||||||
|
return UserPendingRequestStatus.PENDING.name().equals(this.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMadeBy(final Long appUserId) {
|
||||||
|
return this.makerId != null && this.makerId.equals(appUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void approve(final Long checkerId) {
|
||||||
|
this.status = UserPendingRequestStatus.APPROVED.name();
|
||||||
|
this.checkerId = checkerId;
|
||||||
|
this.checkedDate = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
this.pendingLockKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reject(final Long checkerId, final String reason) {
|
||||||
|
this.status = UserPendingRequestStatus.REJECTED.name();
|
||||||
|
this.checkerId = checkerId;
|
||||||
|
this.checkedDate = OffsetDateTime.now(ZoneOffset.UTC);
|
||||||
|
this.rejectReason = reason;
|
||||||
|
this.pendingLockKey = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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.domain;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||||
|
|
||||||
|
public interface UserPendingRequestRepository extends JpaRepository<UserPendingRequest, Long>, JpaSpecificationExecutor<UserPendingRequest> {
|
||||||
|
|
||||||
|
Optional<UserPendingRequest> findByPendingLockKey(String pendingLockKey);
|
||||||
|
|
||||||
|
boolean existsByPendingLockKey(String pendingLockKey);
|
||||||
|
}
|
||||||
+49
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* 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.domain;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.useradministration.exception.UserPendingRequestNotFoundException;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPendingRequestRepositoryWrapper {
|
||||||
|
|
||||||
|
private final UserPendingRequestRepository repository;
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserPendingRequest findOneWithNotFoundDetection(final Long id) {
|
||||||
|
return this.repository.findById(id).orElseThrow(() -> new UserPendingRequestNotFoundException(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPendingRequest saveAndFlush(final UserPendingRequest request) {
|
||||||
|
return this.repository.saveAndFlush(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UserPendingRequest save(final UserPendingRequest request) {
|
||||||
|
return this.repository.save(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public boolean existsByPendingLockKey(final String pendingLockKey) {
|
||||||
|
return this.repository.existsByPendingLockKey(pendingLockKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* 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.domain;
|
||||||
|
|
||||||
|
public enum UserPendingRequestStatus {
|
||||||
|
PENDING,
|
||||||
|
APPROVED,
|
||||||
|
REJECTED;
|
||||||
|
|
||||||
|
public static UserPendingRequestStatus fromString(final String status) {
|
||||||
|
if (status == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final UserPendingRequestStatus requestStatus : values()) {
|
||||||
|
if (requestStatus.name().equalsIgnoreCase(status)) {
|
||||||
|
return requestStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown UserPendingRequestStatus: " + status);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* 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.domain;
|
||||||
|
|
||||||
|
public enum UserPendingRequestType {
|
||||||
|
CREATE_USER,
|
||||||
|
UPDATE_USER;
|
||||||
|
|
||||||
|
public static UserPendingRequestType fromString(final String type) {
|
||||||
|
if (type == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (final UserPendingRequestType requestType : values()) {
|
||||||
|
if (requestType.name().equalsIgnoreCase(type)) {
|
||||||
|
return requestType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IllegalArgumentException("Unknown UserPendingRequestType: " + type);
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* 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.exception;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException;
|
||||||
|
|
||||||
|
public class UserPendingRequestNotFoundException extends AbstractPlatformResourceNotFoundException {
|
||||||
|
|
||||||
|
public UserPendingRequestNotFoundException(final Long id) {
|
||||||
|
super("error.msg.userpendingrequest.id.invalid", "User pending request with identifier " + id + " does not exist", id);
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.useradministration.data.UserPendingRequestData;
|
||||||
|
|
||||||
|
public interface UserPendingReadPlatformService {
|
||||||
|
|
||||||
|
Page<UserPendingRequestData> retrieveAllPendingRequests(String status, String username, String fromDate, String toDate,
|
||||||
|
Integer offset, Integer limit);
|
||||||
|
|
||||||
|
UserPendingRequestData retrievePendingRequest(Long id);
|
||||||
|
}
|
||||||
+329
@@ -0,0 +1,329 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.domain.JdbcSupport;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.Page;
|
||||||
|
import org.apache.fineract.useradministration.data.UserPendingRequestData;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequest;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper;
|
||||||
|
import org.springframework.dao.EmptyResultDataAccessException;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPendingReadPlatformServiceImpl implements UserPendingReadPlatformService {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper;
|
||||||
|
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
|
||||||
|
|
||||||
|
private static final String SELECT_SQL = """
|
||||||
|
SELECT r.id AS id,
|
||||||
|
r.user_id AS userId,
|
||||||
|
r.request_type AS requestType,
|
||||||
|
r.status AS status,
|
||||||
|
r.payload_json AS payloadJson,
|
||||||
|
r.original_json AS originalJson,
|
||||||
|
r.maker_id AS makerId,
|
||||||
|
maker.username AS makerUsername,
|
||||||
|
r.submitted_date AS submittedDate,
|
||||||
|
r.checker_id AS checkerId,
|
||||||
|
checker.username AS checkerUsername,
|
||||||
|
r.checked_date AS checkedDate,
|
||||||
|
r.reject_reason AS rejectReason
|
||||||
|
FROM m_user_pending_request r
|
||||||
|
JOIN m_appuser maker ON maker.id = r.maker_id
|
||||||
|
LEFT JOIN m_appuser checker ON checker.id = r.checker_id
|
||||||
|
""";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public Page<UserPendingRequestData> retrieveAllPendingRequests(final String status, final String username,
|
||||||
|
final String fromDate, final String toDate, final Integer offset, final Integer limit) {
|
||||||
|
final StringBuilder where = new StringBuilder(" WHERE 1=1");
|
||||||
|
final List<Object> args = new ArrayList<>();
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(status)) {
|
||||||
|
where.append(" AND r.status = ?");
|
||||||
|
args.add(status.trim().toUpperCase());
|
||||||
|
} else {
|
||||||
|
where.append(" AND r.status = 'PENDING'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(fromDate)) {
|
||||||
|
where.append(" AND r.submitted_date::date >= ?::date");
|
||||||
|
args.add(fromDate);
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(toDate)) {
|
||||||
|
where.append(" AND r.submitted_date::date <= ?::date");
|
||||||
|
args.add(toDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String countSql = "SELECT COUNT(*) FROM m_user_pending_request r " + where;
|
||||||
|
final Long totalFilteredRecords = this.jdbcTemplate.queryForObject(countSql, Long.class, args.toArray());
|
||||||
|
|
||||||
|
final int pageOffset = offset == null ? 0 : offset;
|
||||||
|
final int pageLimit = limit == null ? 20 : limit;
|
||||||
|
final String pageSql = SELECT_SQL + where + " ORDER BY r.submitted_date DESC LIMIT ? OFFSET ?";
|
||||||
|
final List<Object> pageArgs = new ArrayList<>(args);
|
||||||
|
pageArgs.add(pageLimit);
|
||||||
|
pageArgs.add(pageOffset);
|
||||||
|
|
||||||
|
final List<UserPendingRequestData> items = this.jdbcTemplate.query(pageSql, new UserPendingRowMapper(), pageArgs.toArray());
|
||||||
|
|
||||||
|
return new Page<>(items, totalFilteredRecords == null ? 0 : totalFilteredRecords.intValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserPendingRequestData retrievePendingRequest(final Long id) {
|
||||||
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
|
final String sql = SELECT_SQL + " WHERE r.id = ?";
|
||||||
|
return this.jdbcTemplate.queryForObject(sql, new UserPendingDetailRowMapper(), id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UserPendingRowMapper implements RowMapper<UserPendingRequestData> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||||
|
final Long id = rs.getLong("id");
|
||||||
|
final Long userId = JdbcSupport.getLong(rs, "userId");
|
||||||
|
final String requestType = rs.getString("requestType");
|
||||||
|
final String status = rs.getString("status");
|
||||||
|
final String payloadJson = rs.getString("payloadJson");
|
||||||
|
final Long makerId = rs.getLong("makerId");
|
||||||
|
final String makerUsername = rs.getString("makerUsername");
|
||||||
|
final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class);
|
||||||
|
final Long checkerId = JdbcSupport.getLong(rs, "checkerId");
|
||||||
|
final String checkerUsername = rs.getString("checkerUsername");
|
||||||
|
final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class);
|
||||||
|
final String rejectReason = rs.getString("rejectReason");
|
||||||
|
|
||||||
|
String username = null;
|
||||||
|
String firstname = null;
|
||||||
|
String lastname = null;
|
||||||
|
String email = null;
|
||||||
|
String mobileNo = null;
|
||||||
|
Long officeId = null;
|
||||||
|
String otpDeliveryMethod = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (payloadJson != null) {
|
||||||
|
final JsonObject payloadObj = JsonParser.parseString(payloadJson).getAsJsonObject();
|
||||||
|
if (payloadObj.has("username")) {
|
||||||
|
username = payloadObj.get("username").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("firstname")) {
|
||||||
|
firstname = payloadObj.get("firstname").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("lastname")) {
|
||||||
|
lastname = payloadObj.get("lastname").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("email")) {
|
||||||
|
email = payloadObj.get("email").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("mobileNo")) {
|
||||||
|
mobileNo = payloadObj.get("mobileNo").getAsString();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("officeId")) {
|
||||||
|
officeId = payloadObj.get("officeId").getAsLong();
|
||||||
|
}
|
||||||
|
if (payloadObj.has("otpDeliveryMethod")) {
|
||||||
|
otpDeliveryMethod = payloadObj.get("otpDeliveryMethod").getAsString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserPendingRequestData.builder()
|
||||||
|
.id(id)
|
||||||
|
.userId(userId)
|
||||||
|
.requestType(requestType)
|
||||||
|
.status(status)
|
||||||
|
.username(username)
|
||||||
|
.firstname(firstname)
|
||||||
|
.lastname(lastname)
|
||||||
|
.email(email)
|
||||||
|
.mobileNo(mobileNo)
|
||||||
|
.officeId(officeId)
|
||||||
|
.otpDeliveryMethod(otpDeliveryMethod)
|
||||||
|
.makerId(makerId)
|
||||||
|
.makerUsername(makerUsername)
|
||||||
|
.submittedDate(submittedDate)
|
||||||
|
.checkerId(checkerId)
|
||||||
|
.checkerUsername(checkerUsername)
|
||||||
|
.checkedDate(checkedDate)
|
||||||
|
.rejectReason(rejectReason)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class UserPendingDetailRowMapper implements RowMapper<UserPendingRequestData> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserPendingRequestData mapRow(final ResultSet rs, final int rowNum) throws SQLException {
|
||||||
|
final Long id = rs.getLong("id");
|
||||||
|
final Long userId = JdbcSupport.getLong(rs, "userId");
|
||||||
|
final String requestType = rs.getString("requestType");
|
||||||
|
final String status = rs.getString("status");
|
||||||
|
final String payloadJson = rs.getString("payloadJson");
|
||||||
|
final String originalJson = rs.getString("originalJson");
|
||||||
|
final Long makerId = rs.getLong("makerId");
|
||||||
|
final String makerUsername = rs.getString("makerUsername");
|
||||||
|
final OffsetDateTime submittedDate = rs.getObject("submittedDate", OffsetDateTime.class);
|
||||||
|
final Long checkerId = JdbcSupport.getLong(rs, "checkerId");
|
||||||
|
final String checkerUsername = rs.getString("checkerUsername");
|
||||||
|
final OffsetDateTime checkedDate = rs.getObject("checkedDate", OffsetDateTime.class);
|
||||||
|
final String rejectReason = rs.getString("rejectReason");
|
||||||
|
|
||||||
|
final Map<String, Object> proposedValues = new LinkedHashMap<>();
|
||||||
|
final Map<String, Object> originalValues = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
String username = null;
|
||||||
|
String firstname = null;
|
||||||
|
String lastname = null;
|
||||||
|
String email = null;
|
||||||
|
String mobileNo = null;
|
||||||
|
Long officeId = null;
|
||||||
|
String otpDeliveryMethod = null;
|
||||||
|
Boolean passwordNeverExpires = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (payloadJson != null) {
|
||||||
|
final JsonObject payloadObj = JsonParser.parseString(payloadJson).getAsJsonObject();
|
||||||
|
for (final String key : payloadObj.keySet()) {
|
||||||
|
final JsonElement val = payloadObj.get(key);
|
||||||
|
if ("password".equals(key) || "repeatPassword".equals(key)) {
|
||||||
|
proposedValues.put("password", "Password will be set on approval");
|
||||||
|
} else if (val.isJsonPrimitive()) {
|
||||||
|
if (val.getAsJsonPrimitive().isBoolean()) {
|
||||||
|
proposedValues.put(key, val.getAsBoolean());
|
||||||
|
} else if (val.getAsJsonPrimitive().isNumber()) {
|
||||||
|
proposedValues.put(key, val.getAsNumber());
|
||||||
|
} else {
|
||||||
|
proposedValues.put(key, val.getAsString());
|
||||||
|
}
|
||||||
|
} else if (val.isJsonArray()) {
|
||||||
|
final List<String> list = new ArrayList<>();
|
||||||
|
for (final JsonElement elem : val.getAsJsonArray()) {
|
||||||
|
list.add(elem.getAsString());
|
||||||
|
}
|
||||||
|
proposedValues.put(key, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proposedValues.containsKey("username")) {
|
||||||
|
username = (String) proposedValues.get("username");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("firstname")) {
|
||||||
|
firstname = (String) proposedValues.get("firstname");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("lastname")) {
|
||||||
|
lastname = (String) proposedValues.get("lastname");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("email")) {
|
||||||
|
email = (String) proposedValues.get("email");
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("mobileNo")) {
|
||||||
|
mobileNo = (String) proposedValues.get("mobileNo");
|
||||||
|
}
|
||||||
|
if (proposedValues.get("officeId") instanceof Number n) {
|
||||||
|
officeId = n.longValue();
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("otpDeliveryMethod")) {
|
||||||
|
otpDeliveryMethod = (String) proposedValues.get("otpDeliveryMethod");
|
||||||
|
}
|
||||||
|
if (proposedValues.get("passwordNeverExpires") instanceof Boolean b) {
|
||||||
|
passwordNeverExpires = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originalJson != null) {
|
||||||
|
final JsonObject origObj = JsonParser.parseString(originalJson).getAsJsonObject();
|
||||||
|
for (final String key : origObj.keySet()) {
|
||||||
|
final JsonElement val = origObj.get(key);
|
||||||
|
if (val.isJsonPrimitive()) {
|
||||||
|
if (val.getAsJsonPrimitive().isBoolean()) {
|
||||||
|
originalValues.put(key, val.getAsBoolean());
|
||||||
|
} else if (val.getAsJsonPrimitive().isNumber()) {
|
||||||
|
originalValues.put(key, val.getAsNumber());
|
||||||
|
} else {
|
||||||
|
originalValues.put(key, val.getAsString());
|
||||||
|
}
|
||||||
|
} else if (val.isJsonArray()) {
|
||||||
|
final List<String> list = new ArrayList<>();
|
||||||
|
for (final JsonElement elem : val.getAsJsonArray()) {
|
||||||
|
list.add(elem.getAsString());
|
||||||
|
}
|
||||||
|
originalValues.put(key, list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proposedValues.containsKey("password")) {
|
||||||
|
originalValues.put("password", "[PROTECTED]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return UserPendingRequestData.builder()
|
||||||
|
.id(id)
|
||||||
|
.userId(userId)
|
||||||
|
.requestType(requestType)
|
||||||
|
.status(status)
|
||||||
|
.username(username)
|
||||||
|
.firstname(firstname)
|
||||||
|
.lastname(lastname)
|
||||||
|
.email(email)
|
||||||
|
.mobileNo(mobileNo)
|
||||||
|
.officeId(officeId)
|
||||||
|
.otpDeliveryMethod(otpDeliveryMethod)
|
||||||
|
.passwordNeverExpires(passwordNeverExpires)
|
||||||
|
.makerId(makerId)
|
||||||
|
.makerUsername(makerUsername)
|
||||||
|
.submittedDate(submittedDate)
|
||||||
|
.checkerId(checkerId)
|
||||||
|
.checkerUsername(checkerUsername)
|
||||||
|
.checkedDate(checkedDate)
|
||||||
|
.rejectReason(rejectReason)
|
||||||
|
.originalValues(originalValues)
|
||||||
|
.proposedValues(proposedValues)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
|
||||||
|
public interface UserPendingWritePlatformService {
|
||||||
|
|
||||||
|
CommandProcessingResult submitCreateUser(JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult submitUpdateUser(Long userId, JsonCommand command);
|
||||||
|
|
||||||
|
CommandProcessingResult approvePendingRequest(Long id);
|
||||||
|
|
||||||
|
CommandProcessingResult rejectPendingRequest(Long id, JsonCommand command);
|
||||||
|
}
|
||||||
+386
@@ -0,0 +1,386 @@
|
|||||||
|
/**
|
||||||
|
* 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.service;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.commons.lang3.ObjectUtils;
|
||||||
|
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.data.CommandProcessingResultBuilder;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.GoogleGsonSerializerHelper;
|
||||||
|
import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
|
||||||
|
import org.apache.fineract.organisation.office.domain.Office;
|
||||||
|
import org.apache.fineract.organisation.office.domain.OfficeRepositoryWrapper;
|
||||||
|
import org.apache.fineract.organisation.staff.domain.Staff;
|
||||||
|
import org.apache.fineract.organisation.staff.domain.StaffRepositoryWrapper;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.Client;
|
||||||
|
import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper;
|
||||||
|
import org.apache.fineract.useradministration.mfa.domain.MfaFactorType;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUser;
|
||||||
|
import org.apache.fineract.useradministration.domain.AppUserRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.Role;
|
||||||
|
import org.apache.fineract.useradministration.domain.RoleRepository;
|
||||||
|
import org.apache.fineract.useradministration.mfa.domain.UserMfaFactor;
|
||||||
|
import org.apache.fineract.useradministration.mfa.domain.UserMfaFactorRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequest;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepository;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestRepositoryWrapper;
|
||||||
|
import org.apache.fineract.useradministration.domain.UserPendingRequestType;
|
||||||
|
import org.apache.fineract.useradministration.exception.RoleNotFoundException;
|
||||||
|
import org.apache.fineract.useradministration.exception.UserNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserPendingWritePlatformServiceImpl implements UserPendingWritePlatformService {
|
||||||
|
|
||||||
|
private final PlatformSecurityContext context;
|
||||||
|
private final UserPendingRequestRepositoryWrapper pendingRequestRepositoryWrapper;
|
||||||
|
private final UserPendingRequestRepository pendingRequestRepository;
|
||||||
|
private final AppUserRepository appUserRepository;
|
||||||
|
private final OfficeRepositoryWrapper officeRepositoryWrapper;
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
private final StaffRepositoryWrapper staffRepositoryWrapper;
|
||||||
|
private final ClientRepositoryWrapper clientRepositoryWrapper;
|
||||||
|
private final UserMfaFactorRepository userMfaFactorRepository;
|
||||||
|
private final ConfigurationDomainService configurationDomainService;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final UserDataValidator fromApiJsonDeserializer;
|
||||||
|
private final FromJsonHelper fromApiJsonHelper;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult submitCreateUser(final JsonCommand command) {
|
||||||
|
final AppUser maker = this.context.authenticatedUser();
|
||||||
|
|
||||||
|
this.fromApiJsonDeserializer.validateForCreate(command.json());
|
||||||
|
|
||||||
|
final String username = command.stringValueOfParameterNamed("username");
|
||||||
|
final String email = command.stringValueOfParameterNamed("email");
|
||||||
|
final String mobileNo = command.stringValueOfParameterNamed("mobileNo");
|
||||||
|
|
||||||
|
// Pre-flight checks on live users and pending creations
|
||||||
|
if (this.appUserRepository.findAppUserByName(username) != null) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.user.duplicate.username",
|
||||||
|
"User with username " + username + " already exists.", "username", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String lockKey = "CREATE_USER:" + username.toLowerCase();
|
||||||
|
if (this.pendingRequestRepository.existsByPendingLockKey(lockKey)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.duplicate",
|
||||||
|
"A pending creation request for username '" + username + "' is already awaiting checker approval.", username);
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEmailAndMobileUnique(email, mobileNo, null);
|
||||||
|
|
||||||
|
final JsonObject jsonObject = JsonParser.parseString(command.json()).getAsJsonObject();
|
||||||
|
String passwordHash = null;
|
||||||
|
if (jsonObject.has("password") && !jsonObject.get("password").isJsonNull()) {
|
||||||
|
final String rawPassword = jsonObject.get("password").getAsString();
|
||||||
|
if (!rawPassword.isBlank()) {
|
||||||
|
passwordHash = this.passwordEncoder.encode(rawPassword);
|
||||||
|
}
|
||||||
|
jsonObject.addProperty("password", "[PROTECTED]");
|
||||||
|
if (jsonObject.has("repeatPassword")) {
|
||||||
|
jsonObject.addProperty("repeatPassword", "[PROTECTED]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String sanitizedPayload = jsonObject.toString();
|
||||||
|
final UserPendingRequest request = UserPendingRequest.create(UserPendingRequestType.CREATE_USER, null,
|
||||||
|
sanitizedPayload, null, passwordHash, maker.getId(), lockKey);
|
||||||
|
|
||||||
|
final UserPendingRequest saved = this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(saved.getId())
|
||||||
|
.setRollbackTransaction(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult submitUpdateUser(final Long userId, final JsonCommand command) {
|
||||||
|
final AppUser maker = this.context.authenticatedUser();
|
||||||
|
final AppUser userToUpdate = this.appUserRepository.findById(userId)
|
||||||
|
.orElseThrow(() -> new UserNotFoundException(userId));
|
||||||
|
|
||||||
|
this.fromApiJsonDeserializer.validateForUpdate(command.json(), maker);
|
||||||
|
|
||||||
|
final String email = command.stringValueOfParameterNamed("email");
|
||||||
|
final String mobileNo = command.stringValueOfParameterNamed("mobileNo");
|
||||||
|
assertEmailAndMobileUnique(email, mobileNo, userId);
|
||||||
|
|
||||||
|
final String lockKey = "UPDATE_USER:" + userId;
|
||||||
|
if (this.pendingRequestRepository.existsByPendingLockKey(lockKey)) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.duplicate",
|
||||||
|
"A pending update request for user ID " + userId + " is already awaiting checker approval.", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build point-in-time original JSON snapshot
|
||||||
|
final Map<String, Object> originalSnapshot = new LinkedHashMap<>();
|
||||||
|
originalSnapshot.put("username", userToUpdate.getUsername());
|
||||||
|
originalSnapshot.put("firstname", userToUpdate.getFirstname());
|
||||||
|
originalSnapshot.put("lastname", userToUpdate.getLastname());
|
||||||
|
originalSnapshot.put("email", userToUpdate.getEmail());
|
||||||
|
originalSnapshot.put("mobileNo", userToUpdate.getMobileNo());
|
||||||
|
originalSnapshot.put("officeId", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getId() : null);
|
||||||
|
originalSnapshot.put("officeName", userToUpdate.getOffice() != null ? userToUpdate.getOffice().getName() : null);
|
||||||
|
originalSnapshot.put("staffId", userToUpdate.getStaff() != null ? userToUpdate.getStaff().getId() : null);
|
||||||
|
originalSnapshot.put("passwordNeverExpires", userToUpdate.getPasswordNeverExpires());
|
||||||
|
originalSnapshot.put("isSelfServiceUser", userToUpdate.isSelfServiceUser());
|
||||||
|
final Set<String> roleNames = new HashSet<>();
|
||||||
|
final Set<Long> roleIds = new HashSet<>();
|
||||||
|
if (userToUpdate.getRoles() != null) {
|
||||||
|
for (final Role r : userToUpdate.getRoles()) {
|
||||||
|
roleNames.add(r.getName());
|
||||||
|
roleIds.add(r.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
originalSnapshot.put("roles", roleIds);
|
||||||
|
originalSnapshot.put("roleNames", roleNames);
|
||||||
|
final String originalJson = GoogleGsonSerializerHelper.createSimpleGson().toJson(originalSnapshot);
|
||||||
|
|
||||||
|
final JsonObject jsonObject = JsonParser.parseString(command.json()).getAsJsonObject();
|
||||||
|
String passwordHash = null;
|
||||||
|
if (jsonObject.has("password") && !jsonObject.get("password").isJsonNull()) {
|
||||||
|
final String rawPassword = jsonObject.get("password").getAsString();
|
||||||
|
if (!rawPassword.isBlank()) {
|
||||||
|
passwordHash = this.passwordEncoder.encode(rawPassword);
|
||||||
|
}
|
||||||
|
jsonObject.addProperty("password", "[PROTECTED]");
|
||||||
|
if (jsonObject.has("repeatPassword")) {
|
||||||
|
jsonObject.addProperty("repeatPassword", "[PROTECTED]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final String sanitizedPayload = jsonObject.toString();
|
||||||
|
final UserPendingRequest request = UserPendingRequest.create(UserPendingRequestType.UPDATE_USER, userId,
|
||||||
|
sanitizedPayload, originalJson, passwordHash, maker.getId(), lockKey);
|
||||||
|
|
||||||
|
final UserPendingRequest saved = this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(saved.getId())
|
||||||
|
.setRollbackTransaction(true)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult approvePendingRequest(final Long id) {
|
||||||
|
final AppUser checker = this.context.authenticatedUser();
|
||||||
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
|
|
||||||
|
if (!request.isPending()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.not.pending",
|
||||||
|
"User pending request " + id + " has already been actioned.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isMadeBy(checker.getId())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.self.approval",
|
||||||
|
"You cannot approve a user request you submitted yourself — a different user must action it.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Long resourceId;
|
||||||
|
if (UserPendingRequestType.CREATE_USER.name().equals(request.getRequestType())) {
|
||||||
|
resourceId = finalizeCreateUser(request);
|
||||||
|
} else if (UserPendingRequestType.UPDATE_USER.name().equals(request.getRequestType())) {
|
||||||
|
resourceId = finalizeUpdateUser(request);
|
||||||
|
} else {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.type.invalid",
|
||||||
|
"Unknown request type: " + request.getRequestType(), request.getRequestType());
|
||||||
|
}
|
||||||
|
|
||||||
|
request.approve(checker.getId());
|
||||||
|
this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(resourceId)
|
||||||
|
.withResourceIdAsString(resourceId.toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public CommandProcessingResult rejectPendingRequest(final Long id, final JsonCommand command) {
|
||||||
|
final AppUser checker = this.context.authenticatedUser();
|
||||||
|
final UserPendingRequest request = this.pendingRequestRepositoryWrapper.findOneWithNotFoundDetection(id);
|
||||||
|
|
||||||
|
if (!request.isPending()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.not.pending",
|
||||||
|
"User pending request " + id + " has already been actioned.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.isMadeBy(checker.getId())) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.self.approval",
|
||||||
|
"You cannot reject a user request you submitted yourself — a different user must action it.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
String reason = command != null ? command.stringValueOfParameterNamedAllowingNull("reason") : null;
|
||||||
|
if (reason == null && command != null) {
|
||||||
|
reason = command.stringValueOfParameterNamedAllowingNull("rejectReason");
|
||||||
|
}
|
||||||
|
if (reason == null || reason.isBlank()) {
|
||||||
|
throw new GeneralPlatformDomainRuleException("error.msg.userpendingrequest.reject.reason.required",
|
||||||
|
"A reason is required to reject a user pending request.", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.reject(checker.getId(), reason);
|
||||||
|
this.pendingRequestRepositoryWrapper.saveAndFlush(request);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder()
|
||||||
|
.withEntityId(request.getId())
|
||||||
|
.withResourceIdAsString(request.getId().toString())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long finalizeCreateUser(final UserPendingRequest request) {
|
||||||
|
final JsonElement parsedElement = this.fromApiJsonHelper.parse(request.getPayloadJson());
|
||||||
|
final JsonCommand replay = JsonCommand.fromJsonElement(null, parsedElement, this.fromApiJsonHelper);
|
||||||
|
|
||||||
|
final Long officeId = replay.longValueOfParameterNamed("officeId");
|
||||||
|
final Office userOffice = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
|
|
||||||
|
final String[] roles = replay.arrayValueOfParameterNamed("roles");
|
||||||
|
final Set<Role> allRoles = assembleSetOfRoles(roles);
|
||||||
|
|
||||||
|
final Long staffId = replay.longValueOfParameterNamed("staffId");
|
||||||
|
final Staff linkedStaff = staffId != null
|
||||||
|
? this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userOffice.getId())
|
||||||
|
: null;
|
||||||
|
|
||||||
|
final Collection<Client> clients = null;
|
||||||
|
|
||||||
|
final AppUser appUser = AppUser.fromJson(userOffice, linkedStaff, allRoles, clients, replay);
|
||||||
|
|
||||||
|
if (this.configurationDomainService.isForcePasswordResetOnFirstLoginEnabled()) {
|
||||||
|
appUser.updatePasswordResetRequired(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set pre-hashed password
|
||||||
|
if (request.getPasswordHash() != null) {
|
||||||
|
appUser.updatePassword(request.getPasswordHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appUserRepository.saveAndFlush(appUser);
|
||||||
|
|
||||||
|
// MFA Enrollment
|
||||||
|
final String otpDeliveryMethod = replay.stringValueOfParameterNamed("otpDeliveryMethod");
|
||||||
|
if (otpDeliveryMethod != null) {
|
||||||
|
final MfaFactorType factorType = MfaFactorType.fromString(otpDeliveryMethod);
|
||||||
|
final UserMfaFactor factor = new UserMfaFactor();
|
||||||
|
factor.setAppUser(appUser);
|
||||||
|
factor.setFactorType(factorType);
|
||||||
|
factor.setDestination(factorType == MfaFactorType.SMS ? appUser.getMobileNo() : appUser.getEmail());
|
||||||
|
factor.setVerified(true);
|
||||||
|
factor.setVerifiedOn(OffsetDateTime.now(ZoneOffset.UTC));
|
||||||
|
this.userMfaFactorRepository.save(factor);
|
||||||
|
}
|
||||||
|
|
||||||
|
return appUser.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long finalizeUpdateUser(final UserPendingRequest request) {
|
||||||
|
final AppUser userToUpdate = this.appUserRepository.findById(request.getUserId())
|
||||||
|
.orElseThrow(() -> new UserNotFoundException(request.getUserId()));
|
||||||
|
|
||||||
|
final JsonElement parsedElement = this.fromApiJsonHelper.parse(request.getPayloadJson());
|
||||||
|
final JsonCommand replay = JsonCommand.fromJsonElement(request.getUserId(), parsedElement, this.fromApiJsonHelper);
|
||||||
|
|
||||||
|
final Map<String, Object> changes = userToUpdate.update(replay, null, null);
|
||||||
|
|
||||||
|
if (changes.containsKey("officeId")) {
|
||||||
|
final Long officeId = (Long) changes.get("officeId");
|
||||||
|
final Office office = this.officeRepositoryWrapper.findOneWithNotFoundDetection(officeId);
|
||||||
|
userToUpdate.changeOffice(office);
|
||||||
|
}
|
||||||
|
if (changes.containsKey("staffId")) {
|
||||||
|
final Long staffId = (Long) changes.get("staffId");
|
||||||
|
Staff linkedStaff = null;
|
||||||
|
if (staffId != null) {
|
||||||
|
linkedStaff = this.staffRepositoryWrapper.findByOfficeWithNotFoundDetection(staffId, userToUpdate.getOffice().getId());
|
||||||
|
}
|
||||||
|
userToUpdate.changeStaff(linkedStaff);
|
||||||
|
}
|
||||||
|
if (changes.containsKey("roles")) {
|
||||||
|
final String[] roleIds = (String[]) changes.get("roles");
|
||||||
|
final Set<Role> allRoles = assembleSetOfRoles(roleIds);
|
||||||
|
userToUpdate.updateRoles(allRoles);
|
||||||
|
}
|
||||||
|
if (request.getPasswordHash() != null) {
|
||||||
|
userToUpdate.updatePassword(request.getPasswordHash());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.appUserRepository.saveAndFlush(userToUpdate);
|
||||||
|
return userToUpdate.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Set<Role> assembleSetOfRoles(final String[] rolesArray) {
|
||||||
|
final Set<Role> allRoles = new HashSet<>();
|
||||||
|
if (!ObjectUtils.isEmpty(rolesArray)) {
|
||||||
|
for (final String roleId : rolesArray) {
|
||||||
|
final Long id = Long.valueOf(roleId);
|
||||||
|
final Role role = this.roleRepository.findById(id).orElseThrow(() -> new RoleNotFoundException(id));
|
||||||
|
allRoles.add(role);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertEmailAndMobileUnique(final String email, final String mobileNo, final Long excludeUserId) {
|
||||||
|
if (email != null && !email.isBlank()) {
|
||||||
|
final java.util.Optional<AppUser> emailOwner = excludeUserId == null
|
||||||
|
? this.appUserRepository.findByEmailIgnoreCase(email)
|
||||||
|
: this.appUserRepository.findByEmailIgnoreCaseAndIdNot(email, excludeUserId);
|
||||||
|
if (emailOwner.isPresent()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.user.duplicate.email",
|
||||||
|
"User with email " + email + " already exists.", "email", email);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mobileNo != null && !mobileNo.isBlank()) {
|
||||||
|
final java.util.Optional<AppUser> mobileOwner = excludeUserId == null
|
||||||
|
? this.appUserRepository.findByMobileNo(mobileNo)
|
||||||
|
: this.appUserRepository.findByMobileNoAndIdNot(mobileNo, excludeUserId);
|
||||||
|
if (mobileOwner.isPresent()) {
|
||||||
|
throw new PlatformDataIntegrityException("error.msg.user.duplicate.mobile",
|
||||||
|
"User with mobile number " + mobileNo + " already exists.", "mobileNo", mobileNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -543,4 +543,6 @@
|
|||||||
<include file="parts/0471_enable_charge_maker_checker.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0471_enable_charge_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0472_enable_product_creation_maker_checker.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0472_enable_product_creation_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
<include file="parts/0474_enable_fixed_deposit_product_maker_checker.xml" relativeToChangelogFile="true"/>
|
<include file="parts/0474_enable_fixed_deposit_product_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0475_enable_admin_user_maker_checker.xml" relativeToChangelogFile="true"/>
|
||||||
|
<include file="parts/0476_add_user_pending_request_and_enable_charge_mc.xml" relativeToChangelogFile="true"/>
|
||||||
</databaseChangeLog>
|
</databaseChangeLog>
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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 id="0475_add_permissions_role_checker_if_not_exists" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'authorisation', 'PERMISSIONS_ROLE_CHECKER', 'ROLE', 'PERMISSIONS_CHECKER', FALSE
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_permission WHERE code = 'PERMISSIONS_ROLE_CHECKER'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0475_enable_admin_user_and_role_maker_checker" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<preConditions onFail="HALT">
|
||||||
|
<sqlCheck expectedResult="9">
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM m_permission
|
||||||
|
WHERE code IN (
|
||||||
|
'CREATE_USER', 'UPDATE_USER', 'DELETE_USER',
|
||||||
|
'CREATE_ROLE', 'UPDATE_ROLE', 'PERMISSIONS_ROLE',
|
||||||
|
'DELETE_ROLE', 'ENABLE_ROLE', 'DISABLE_ROLE'
|
||||||
|
)
|
||||||
|
</sqlCheck>
|
||||||
|
</preConditions>
|
||||||
|
<sql>
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE code IN (
|
||||||
|
'CREATE_USER', 'UPDATE_USER', 'DELETE_USER',
|
||||||
|
'CREATE_ROLE', 'UPDATE_ROLE', 'PERMISSIONS_ROLE',
|
||||||
|
'DELETE_ROLE', 'ENABLE_ROLE', 'DISABLE_ROLE'
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0475_grant_admin_user_and_role_checker_permissions_to_roles" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_USER_CHECKER', 'UPDATE_USER_CHECKER', 'DELETE_USER_CHECKER',
|
||||||
|
'CREATE_ROLE_CHECKER', 'UPDATE_ROLE_CHECKER', 'PERMISSIONS_ROLE_CHECKER',
|
||||||
|
'DELETE_ROLE_CHECKER', 'ENABLE_ROLE_CHECKER', 'DISABLE_ROLE_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN (
|
||||||
|
'CREATE_USER_CHECKER', 'UPDATE_USER_CHECKER', 'DELETE_USER_CHECKER',
|
||||||
|
'CREATE_ROLE_CHECKER', 'UPDATE_ROLE_CHECKER', 'PERMISSIONS_ROLE_CHECKER',
|
||||||
|
'DELETE_ROLE_CHECKER', 'ENABLE_ROLE_CHECKER', 'DISABLE_ROLE_CHECKER'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<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 id="0476_create_m_user_pending_request_table" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
CREATE TABLE IF NOT EXISTS m_user_pending_request (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NULL REFERENCES m_appuser(id) ON DELETE SET NULL,
|
||||||
|
request_type VARCHAR(20) NOT NULL,
|
||||||
|
status VARCHAR(20) NOT NULL,
|
||||||
|
payload_json TEXT NOT NULL,
|
||||||
|
original_json TEXT NULL,
|
||||||
|
password_hash VARCHAR(255) NULL,
|
||||||
|
maker_id BIGINT NOT NULL REFERENCES m_appuser(id),
|
||||||
|
submitted_date TIMESTAMPTZ NOT NULL,
|
||||||
|
checker_id BIGINT NULL REFERENCES m_appuser(id),
|
||||||
|
checked_date TIMESTAMPTZ NULL,
|
||||||
|
reject_reason VARCHAR(500) NULL,
|
||||||
|
pending_lock_key VARCHAR(100) NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pending_req_status ON m_user_pending_request(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pending_req_user_id ON m_user_pending_request(user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_pending_req_maker ON m_user_pending_request(maker_id);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
<changeSet id="0476_configure_user_and_charge_maker_checker_permissions" author="fineract" runOnChange="false">
|
||||||
|
<validCheckSum>ANY</validCheckSum>
|
||||||
|
<sql>
|
||||||
|
-- Disable native MC on CREATE_USER and UPDATE_USER to avoid core password sanitization error
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = FALSE
|
||||||
|
WHERE code IN ('CREATE_USER', 'UPDATE_USER');
|
||||||
|
|
||||||
|
-- Enable native MC on UPDATE_CHARGE and DELETE_CHARGE
|
||||||
|
UPDATE m_permission
|
||||||
|
SET can_maker_checker = TRUE
|
||||||
|
WHERE code IN ('UPDATE_CHARGE', 'DELETE_CHARGE');
|
||||||
|
|
||||||
|
-- Insert bespoke user maker-checker permissions
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'SUBMIT_CREATE_USER', 'USER', 'SUBMIT_CREATE', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SUBMIT_CREATE_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'SUBMIT_UPDATE_USER', 'USER', 'SUBMIT_UPDATE', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'SUBMIT_UPDATE_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'APPROVE_USER', 'USER', 'APPROVE', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'APPROVE_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'REJECT_USER', 'USER', 'REJECT', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'REJECT_USER');
|
||||||
|
|
||||||
|
INSERT INTO m_permission (grouping, code, entity_name, action_name, can_maker_checker)
|
||||||
|
SELECT 'user_administration', 'READ_USER_PENDING_REQUEST', 'USER', 'READ_PENDING', FALSE
|
||||||
|
WHERE NOT EXISTS (SELECT 1 FROM m_permission WHERE code = 'READ_USER_PENDING_REQUEST');
|
||||||
|
|
||||||
|
-- Grant permissions to Super user
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT r.id, p.id
|
||||||
|
FROM m_role r
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE r.name = 'Super user'
|
||||||
|
AND p.code IN (
|
||||||
|
'UPDATE_CHARGE', 'UPDATE_CHARGE_CHECKER', 'DELETE_CHARGE', 'DELETE_CHARGE_CHECKER',
|
||||||
|
'SUBMIT_CREATE_USER', 'SUBMIT_UPDATE_USER', 'APPROVE_USER', 'REJECT_USER', 'READ_USER_PENDING_REQUEST'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = r.id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Grant permissions to bankadmin
|
||||||
|
INSERT INTO m_role_permission (role_id, permission_id)
|
||||||
|
SELECT ur.role_id, p.id
|
||||||
|
FROM m_appuser u
|
||||||
|
JOIN m_appuser_role ur ON ur.appuser_id = u.id
|
||||||
|
CROSS JOIN m_permission p
|
||||||
|
WHERE u.username = 'bankadmin'
|
||||||
|
AND p.code IN (
|
||||||
|
'UPDATE_CHARGE', 'UPDATE_CHARGE_CHECKER', 'DELETE_CHARGE', 'DELETE_CHARGE_CHECKER',
|
||||||
|
'SUBMIT_CREATE_USER', 'SUBMIT_UPDATE_USER', 'APPROVE_USER', 'REJECT_USER', 'READ_USER_PENDING_REQUEST'
|
||||||
|
)
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM m_role_permission rp
|
||||||
|
WHERE rp.role_id = ur.role_id AND rp.permission_id = p.id
|
||||||
|
);
|
||||||
|
</sql>
|
||||||
|
</changeSet>
|
||||||
|
|
||||||
|
</databaseChangeLog>
|
||||||
+133
@@ -484,6 +484,55 @@ public class CommandSourceServiceTest {
|
|||||||
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_FixedDepositProduct_SuperUser_EnforcesMakerChecker() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_FIXEDDEPOSITPRODUCT");
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("FIXEDDEPOSITPRODUCT");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn("CREATE");
|
||||||
|
Mockito.when(commandSource.getId()).thenReturn(114L);
|
||||||
|
Mockito.when(commandSource.getResourceId()).thenReturn(214L);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_FIXEDDEPOSITPRODUCT")).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_FixedDepositProduct_CheckerApproval_MarksAsChecked() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_FIXEDDEPOSITPRODUCT");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_FIXEDDEPOSITPRODUCT")).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testProcessCommand_Products_CheckerApproval_MarksAsChecked() {
|
public void testProcessCommand_Products_CheckerApproval_MarksAsChecked() {
|
||||||
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
@@ -505,4 +554,88 @@ public class CommandSourceServiceTest {
|
|||||||
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_UserOperations_SuperUserBypassBlocked_ThrowsRollbackException() {
|
||||||
|
String[] actions = { "CREATE", "UPDATE", "DELETE" };
|
||||||
|
String[] permissions = { "CREATE_USER", "UPDATE_USER", "DELETE_USER" };
|
||||||
|
|
||||||
|
for (int i = 0; i < actions.length; i++) {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("USER");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn(actions[i]);
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn(permissions[i]);
|
||||||
|
Mockito.when(commandSource.isSanitized()).thenReturn(false);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask(permissions[i])).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_RoleOperations_SuperUserBypassBlocked_ThrowsRollbackException() {
|
||||||
|
String[] actions = { "CREATE", "UPDATE", "PERMISSIONS", "DELETE" };
|
||||||
|
String[] permissions = { "CREATE_ROLE", "UPDATE_ROLE", "PERMISSIONS_ROLE", "DELETE_ROLE" };
|
||||||
|
|
||||||
|
for (int i = 0; i < actions.length; i++) {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser superUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getEntityName()).thenReturn("ROLE");
|
||||||
|
Mockito.when(commandSource.getActionName()).thenReturn(actions[i]);
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn(permissions[i]);
|
||||||
|
Mockito.when(commandSource.isSanitized()).thenReturn(false);
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask(permissions[i])).thenReturn(true);
|
||||||
|
Mockito.when(superUser.isCheckerSuperUser()).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
Assertions.assertThrows(org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException.class, () -> {
|
||||||
|
underTest.processCommand(handler, jsonCommand, commandSource, superUser, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
Mockito.verify(commandSource).markAsAwaitingApproval();
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsChecked(any());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testProcessCommand_UserAndRole_CheckerApproval_MarksAsChecked() {
|
||||||
|
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||||
|
JsonCommand jsonCommand = Mockito.mock(JsonCommand.class);
|
||||||
|
CommandSource commandSource = Mockito.mock(CommandSource.class);
|
||||||
|
AppUser checkerUser = Mockito.mock(AppUser.class);
|
||||||
|
|
||||||
|
Mockito.when(commandSource.getPermissionCode()).thenReturn("CREATE_USER");
|
||||||
|
Mockito.when(configurationDomainService.isMakerCheckerEnabledForTask("CREATE_USER")).thenReturn(true);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult dummyResult = new org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder()
|
||||||
|
.withCommandId(1L).build();
|
||||||
|
Mockito.when(handler.processCommand(jsonCommand)).thenReturn(dummyResult);
|
||||||
|
|
||||||
|
org.apache.fineract.infrastructure.core.data.CommandProcessingResult result = underTest.processCommand(handler, jsonCommand,
|
||||||
|
commandSource, checkerUser, true);
|
||||||
|
|
||||||
|
Assertions.assertNotNull(result);
|
||||||
|
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||||
|
Mockito.verify(commandSource, Mockito.never()).markAsAwaitingApproval();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user