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:
+5
@@ -61,6 +61,11 @@ public class ConfigurationDomainServiceJpa implements ConfigurationDomainService
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMakerCheckerEnabled() {
|
||||
return getGlobalConfigurationPropertyData(GlobalConfigurationConstants.MAKER_CHECKER).isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAllowedLoanStatusesForExternalAssetTransfer() {
|
||||
final GlobalConfigurationPropertyData property = getGlobalConfigurationPropertyData(
|
||||
|
||||
+67
@@ -88,6 +88,11 @@ public class UsersApiResource {
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
private final BulkImportWorkbookPopulatorService bulkImportWorkbookPopulatorService;
|
||||
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
|
||||
@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 })
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
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() //
|
||||
.createUser() //
|
||||
@@ -179,6 +190,12 @@ public class UsersApiResource {
|
||||
@Produces({ MediaType.APPLICATION_JSON })
|
||||
public String update(@PathParam("userId") @Parameter(description = "userId") final Long userId,
|
||||
@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() //
|
||||
.updateUser(userId) //
|
||||
@@ -190,6 +207,56 @@ public class UsersApiResource {
|
||||
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
|
||||
@Path("{userId}/pwd")
|
||||
@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/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/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>
|
||||
|
||||
+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());
|
||||
}
|
||||
|
||||
@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
|
||||
public void testProcessCommand_Products_CheckerApproval_MarksAsChecked() {
|
||||
NewCommandSourceHandler handler = Mockito.mock(NewCommandSourceHandler.class);
|
||||
@@ -505,4 +554,88 @@ public class CommandSourceServiceTest {
|
||||
Mockito.verify(commandSource).markAsChecked(checkerUser);
|
||||
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