FINERACT-1977: 4-eye-principle, maker-checker permission
This commit is contained in:
+42
-54
@@ -52,7 +52,6 @@ import org.apache.fineract.batch.exception.BatchReferenceInvalidException;
|
||||
import org.apache.fineract.batch.exception.ErrorInfo;
|
||||
import org.apache.fineract.batch.service.ResolutionHelper.BatchRequestNode;
|
||||
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
|
||||
import org.apache.fineract.infrastructure.core.exception.AbstractIdempotentCommandException;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.infrastructure.core.filters.BatchCallHandler;
|
||||
import org.apache.fineract.infrastructure.core.filters.BatchFilter;
|
||||
@@ -62,10 +61,8 @@ import org.springframework.dao.ConcurrencyFailureException;
|
||||
import org.springframework.dao.NonTransientDataAccessException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
import org.springframework.transaction.TransactionException;
|
||||
import org.springframework.transaction.TransactionExecution;
|
||||
import org.springframework.transaction.TransactionStatus;
|
||||
import org.springframework.transaction.TransactionSystemException;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
@@ -104,8 +101,7 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
*/
|
||||
@Override
|
||||
public List<BatchResponse> handleBatchRequestsWithoutEnclosingTransaction(final List<BatchRequest> requestList, UriInfo uriInfo) {
|
||||
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
|
||||
return handleBatchRequests(false, requestList, uriInfo);
|
||||
return handleBatchRequests(requestList, uriInfo, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +113,18 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
*/
|
||||
@Override
|
||||
public List<BatchResponse> handleBatchRequestsWithEnclosingTransaction(final List<BatchRequest> requestList, final UriInfo uriInfo) {
|
||||
return callInTransaction(Function.identity()::apply, () -> handleBatchRequests(true, requestList, uriInfo));
|
||||
return handleBatchRequests(requestList, uriInfo, true);
|
||||
}
|
||||
|
||||
private List<BatchResponse> handleBatchRequests(final List<BatchRequest> requestList, final UriInfo uriInfo,
|
||||
boolean enclosingTransaction) {
|
||||
BatchRequestContextHolder.setIsEnclosingTransaction(enclosingTransaction);
|
||||
try {
|
||||
return enclosingTransaction ? callInTransaction(Function.identity()::apply, () -> handleRequestNodes(requestList, uriInfo))
|
||||
: handleRequestNodes(requestList, uriInfo);
|
||||
} finally {
|
||||
BatchRequestContextHolder.resetIsEnclosingTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,18 +143,15 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
|
||||
transactionConfigurator.accept(transactionTemplate);
|
||||
return transactionTemplate.execute(status -> {
|
||||
BatchRequestContextHolder.setEnclosingTransaction(Optional.of(status));
|
||||
BatchRequestContextHolder.setEnclosingTransaction(status);
|
||||
try {
|
||||
responseList.addAll(request.get());
|
||||
return responseList;
|
||||
} catch (BatchExecutionException ex) {
|
||||
status.setRollbackOnly();
|
||||
return List.of(buildErrorResponse(ex.getCause(), ex.getRequest()));
|
||||
} catch (RuntimeException ex) {
|
||||
status.setRollbackOnly();
|
||||
return buildErrorResponses(ex, responseList);
|
||||
responseList.add(buildErrorResponse(ex.getCause(), ex.getRequest()));
|
||||
return responseList;
|
||||
} finally {
|
||||
BatchRequestContextHolder.setEnclosingTransaction(Optional.empty());
|
||||
BatchRequestContextHolder.resetTransaction();
|
||||
}
|
||||
});
|
||||
} catch (TransactionException | NonTransientDataAccessException ex) {
|
||||
@@ -163,24 +167,17 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
* @param uriInfo
|
||||
* @return {@code List<BatchResponse>}
|
||||
*/
|
||||
private List<BatchResponse> handleBatchRequests(boolean enclosingTransaction, final List<BatchRequest> requestList,
|
||||
final UriInfo uriInfo) {
|
||||
private List<BatchResponse> handleRequestNodes(final List<BatchRequest> requestList, final UriInfo uriInfo) {
|
||||
final List<BatchRequestNode> rootNodes;
|
||||
try {
|
||||
rootNodes = this.resolutionHelper.buildNodesTree(requestList);
|
||||
} catch (BatchReferenceInvalidException e) {
|
||||
return List.of(buildErrorResponse(e));
|
||||
return List.of(buildOrThrowErrorResponse(e, null));
|
||||
}
|
||||
|
||||
final ArrayList<BatchResponse> responseList = new ArrayList<>(requestList.size());
|
||||
for (BatchRequestNode rootNode : rootNodes) {
|
||||
if (enclosingTransaction) {
|
||||
this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo, enclosingTransaction);
|
||||
} else {
|
||||
ArrayList<BatchResponse> localResponseList = new ArrayList<>();
|
||||
this.callRequestRecursive(rootNode.getRequest(), rootNode, localResponseList, uriInfo, enclosingTransaction);
|
||||
responseList.addAll(localResponseList);
|
||||
}
|
||||
this.callRequestRecursive(rootNode.getRequest(), rootNode, responseList, uriInfo);
|
||||
}
|
||||
responseList.sort(Comparator.comparing(BatchResponse::getRequestId));
|
||||
return responseList;
|
||||
@@ -197,18 +194,10 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
* the collected responses
|
||||
* @return {@code BatchResponse}
|
||||
*/
|
||||
private void callRequestRecursive(BatchRequest request, BatchRequestNode requestNode, List<BatchResponse> responseList, UriInfo uriInfo,
|
||||
boolean enclosingTransaction) {
|
||||
private void callRequestRecursive(BatchRequest request, BatchRequestNode requestNode, List<BatchResponse> responseList,
|
||||
UriInfo uriInfo) {
|
||||
// run current node
|
||||
BatchResponse response;
|
||||
if (enclosingTransaction) {
|
||||
response = executeRequest(request, uriInfo);
|
||||
} else {
|
||||
List<BatchResponse> transactionResponse = callInTransaction(
|
||||
transactionTemplate -> transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
|
||||
() -> List.of(executeRequest(request, uriInfo)));
|
||||
response = transactionResponse.get(0);
|
||||
}
|
||||
BatchResponse response = executeRequest(request, uriInfo);
|
||||
responseList.add(response);
|
||||
if (response.getStatusCode() != null && response.getStatusCode() == SC_OK) {
|
||||
// run child nodes
|
||||
@@ -217,13 +206,10 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
BatchRequest resolvedChildRequest;
|
||||
try {
|
||||
resolvedChildRequest = this.resolutionHelper.resolveRequest(childRequest, response);
|
||||
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo);
|
||||
} catch (JsonPathException jpex) {
|
||||
responseList.add(buildErrorResponse(jpex, childRequest));
|
||||
return;
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BatchExecutionException(childRequest, ex);
|
||||
responseList.add(buildOrThrowErrorResponse(jpex, childRequest));
|
||||
}
|
||||
callRequestRecursive(resolvedChildRequest, childNode, responseList, uriInfo, enclosingTransaction);
|
||||
});
|
||||
} else {
|
||||
responseList.addAll(parentRequestFailedRecursive(request, requestNode, response, null));
|
||||
@@ -245,7 +231,7 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
log.debug("Batch request: method [{}], relative url [{}]", request.getMethod(), request.getRelativeUrl());
|
||||
Either<RuntimeException, BatchRequest> preprocessorResult = runPreprocessors(request);
|
||||
if (preprocessorResult.isLeft()) {
|
||||
throw new BatchExecutionException(request, preprocessorResult.getLeft());
|
||||
return buildOrThrowErrorResponse(preprocessorResult.getLeft(), request);
|
||||
} else {
|
||||
request = preprocessorResult.get();
|
||||
}
|
||||
@@ -253,19 +239,16 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
BatchRequestContextHolder.setRequestAttributes(new HashMap<>(Optional.ofNullable(request.getHeaders())
|
||||
.map(list -> list.stream().collect(Collectors.toMap(Header::getName, Header::getValue)))
|
||||
.orElse(Collections.emptyMap())));
|
||||
BatchCallHandler callHandler = new BatchCallHandler(this.batchFilters, commandStrategy::execute);
|
||||
Optional<TransactionStatus> transaction = BatchRequestContextHolder.getEnclosingTransaction();
|
||||
if (transaction.isPresent()) {
|
||||
if (BatchRequestContextHolder.isEnclosingTransaction()) {
|
||||
entityManager.flush();
|
||||
}
|
||||
BatchCallHandler callHandler = new BatchCallHandler(this.batchFilters, commandStrategy::execute);
|
||||
final BatchResponse rootResponse = callHandler.serviceCall(request, uriInfo);
|
||||
log.debug("Batch response: status code [{}], method [{}], relative url [{}]", rootResponse.getStatusCode(), request.getMethod(),
|
||||
request.getRelativeUrl());
|
||||
return rootResponse;
|
||||
} catch (AbstractIdempotentCommandException idempotentException) {
|
||||
return buildErrorResponse(idempotentException, request);
|
||||
} catch (RuntimeException ex) {
|
||||
throw new BatchExecutionException(request, ex);
|
||||
return buildOrThrowErrorResponse(ex, request);
|
||||
} finally {
|
||||
BatchRequestContextHolder.resetRequestAttributes();
|
||||
}
|
||||
@@ -313,11 +296,6 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
return responseList;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private BatchResponse buildErrorResponse(@NotNull Throwable ex) {
|
||||
return buildErrorResponse(ex, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the response when any exception raised
|
||||
*
|
||||
@@ -346,6 +324,15 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
return buildErrorResponse(requestId, statusCode, body, headers);
|
||||
}
|
||||
|
||||
private BatchResponse buildOrThrowErrorResponse(RuntimeException ex, BatchRequest request) {
|
||||
BatchResponse response = buildErrorResponse(ex, request);
|
||||
if (response.getStatusCode() != SC_OK && BatchRequestContextHolder.isEnclosingTransaction()) {
|
||||
BatchRequestContextHolder.getTransaction().ifPresent(TransactionExecution::setRollbackOnly);
|
||||
throw new BatchExecutionException(request, ex);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private List<BatchResponse> buildErrorResponses(Throwable ex, @NotNull List<BatchResponse> responseList) {
|
||||
BatchResponse response = responseList.isEmpty() ? null
|
||||
@@ -368,9 +355,10 @@ public class BatchApiServiceImpl implements BatchApiService {
|
||||
}
|
||||
if (response != null) {
|
||||
requestId = response.getRequestId();
|
||||
if (response.getStatusCode() == null || response.getStatusCode() != SC_OK) {
|
||||
if (response.getStatusCode() != null) {
|
||||
statusCode = response.getStatusCode();
|
||||
Integer responseCode = response.getStatusCode();
|
||||
if (responseCode == null || responseCode != SC_OK) {
|
||||
if (responseCode != null) {
|
||||
statusCode = responseCode;
|
||||
}
|
||||
body = "Transaction is being rolled back. First erroneous request: \n" + new Gson().toJson(response);
|
||||
}
|
||||
|
||||
@@ -202,6 +202,14 @@ public class CommandSource extends AbstractPersistableCustom {
|
||||
this.commandAsJson = json;
|
||||
}
|
||||
|
||||
public AppUser getMaker() {
|
||||
return maker;
|
||||
}
|
||||
|
||||
public AppUser getChecker() {
|
||||
return checker;
|
||||
}
|
||||
|
||||
public String getActionName() {
|
||||
return this.actionName;
|
||||
}
|
||||
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* 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.commands.exception;
|
||||
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
|
||||
public class RollbackTransactionAsCommandIsNotApprovedByCheckerException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* When maker-checker is configured globally and also for the current transaction.
|
||||
*
|
||||
* An initial save determines if there are any integrity rule or data problems.
|
||||
*
|
||||
* If there isn't... and the transaction is from a maker... then this roll back is issued and the
|
||||
* commandSourceResult is used to write the audit entry.
|
||||
*/
|
||||
private final CommandSource commandSourceResult;
|
||||
|
||||
public RollbackTransactionAsCommandIsNotApprovedByCheckerException(final CommandSource commandSourceResult) {
|
||||
this.commandSourceResult = commandSourceResult;
|
||||
}
|
||||
|
||||
public CommandSource getCommandSourceResult() {
|
||||
return this.commandSourceResult;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 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.commands.exception;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder;
|
||||
|
||||
/**
|
||||
* When maker-checker is configured globally and also for the current transaction. An initial save determines if there
|
||||
* are any integrity rule or data problems. If there isn't... and the transaction is from a maker... then this roll back
|
||||
* is issued and the commandSourceResult is used to write the audit entry.
|
||||
*/
|
||||
public class RollbackTransactionNotApprovedException extends RuntimeException {
|
||||
|
||||
private final CommandProcessingResult result;
|
||||
|
||||
public RollbackTransactionNotApprovedException(Long commandId, Long entityId) {
|
||||
this.result = new CommandProcessingResultBuilder().withCommandId(commandId).withEntityId(entityId).setRollbackTransaction(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
public CommandProcessingResult getResult() {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+5
@@ -30,6 +30,11 @@ public class UnsupportedCommandException extends RuntimeException {
|
||||
this.unsupportedCommandName = unsupportedCommandName;
|
||||
}
|
||||
|
||||
public UnsupportedCommandException(final String unsupportedCommandName, String message) {
|
||||
super(message);
|
||||
this.unsupportedCommandName = unsupportedCommandName;
|
||||
}
|
||||
|
||||
public String getUnsupportedCommandName() {
|
||||
return this.unsupportedCommandName;
|
||||
}
|
||||
|
||||
+1
-4
@@ -18,7 +18,6 @@
|
||||
*/
|
||||
package org.apache.fineract.commands.service;
|
||||
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
@@ -28,8 +27,6 @@ public interface CommandProcessingService {
|
||||
|
||||
CommandProcessingResult executeCommand(CommandWrapper wrapper, JsonCommand command, boolean isApprovedByChecker);
|
||||
|
||||
CommandProcessingResult logCommand(CommandSource commandSourceResult);
|
||||
|
||||
boolean validateCommand(CommandWrapper commandWrapper, AppUser user);
|
||||
boolean validateRollbackCommand(CommandWrapper commandWrapper, AppUser user);
|
||||
|
||||
}
|
||||
|
||||
+17
-9
@@ -26,7 +26,10 @@ import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.exception.CommandNotFoundException;
|
||||
import org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -95,19 +98,24 @@ public class CommandSourceService {
|
||||
idempotencyKey);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRED)
|
||||
public CommandSource getInitialCommandSource(CommandWrapper wrapper, JsonCommand jsonCommand, AppUser maker, String idempotencyKey) {
|
||||
CommandSource commandSourceResult;
|
||||
if (jsonCommand.commandId() != null) {
|
||||
commandSourceResult = commandSourceRepository.findById(jsonCommand.commandId())
|
||||
.orElseThrow(() -> new CommandNotFoundException(jsonCommand.commandId()));
|
||||
commandSourceResult.markAsChecked(maker);
|
||||
} else {
|
||||
commandSourceResult = CommandSource.fullEntryFrom(wrapper, jsonCommand, maker, idempotencyKey, UNDER_PROCESSING.getValue());
|
||||
}
|
||||
CommandSource commandSourceResult = CommandSource.fullEntryFrom(wrapper, jsonCommand, maker, idempotencyKey,
|
||||
UNDER_PROCESSING.getValue());
|
||||
if (commandSourceResult.getCommandJson() == null) {
|
||||
commandSourceResult.setCommandJson("{}");
|
||||
}
|
||||
return commandSourceResult;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public CommandProcessingResult processCommand(NewCommandSourceHandler handler, JsonCommand command, CommandSource commandSource,
|
||||
AppUser user, boolean isApprovedByChecker, boolean isMakerChecker) {
|
||||
final CommandProcessingResult result = handler.processCommand(command);
|
||||
boolean isRollback = !isApprovedByChecker && !user.isCheckerSuperUser() && (isMakerChecker || result.isRollbackTransaction());
|
||||
if (isRollback) {
|
||||
commandSource.markAsAwaitingApproval();
|
||||
throw new RollbackTransactionNotApprovedException(commandSource.getId(), commandSource.getResourceId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-9
@@ -19,6 +19,7 @@
|
||||
package org.apache.fineract.commands.service;
|
||||
|
||||
import com.google.gson.JsonElement;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
@@ -26,6 +27,8 @@ import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.exception.CommandNotAwaitingApprovalException;
|
||||
import org.apache.fineract.commands.exception.CommandNotFoundException;
|
||||
import org.apache.fineract.commands.exception.UnsupportedCommandException;
|
||||
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.serialization.FromJsonHelper;
|
||||
@@ -45,10 +48,10 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
||||
private final FromJsonHelper fromApiJsonHelper;
|
||||
private final CommandProcessingService processAndLogCommandService;
|
||||
private final SchedulerJobRunnerReadService schedulerJobRunnerReadService;
|
||||
private final ConfigurationDomainService configurationService;
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) {
|
||||
|
||||
boolean isApprovedByChecker = false;
|
||||
|
||||
// check if is update of own account details
|
||||
@@ -77,7 +80,6 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
||||
|
||||
@Override
|
||||
public CommandProcessingResult approveEntry(final Long makerCheckerId) {
|
||||
|
||||
final CommandSource commandSourceInput = validateMakerCheckerTransaction(makerCheckerId);
|
||||
validateIsUpdateAllowed();
|
||||
|
||||
@@ -111,16 +113,24 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
|
||||
}
|
||||
|
||||
private CommandSource validateMakerCheckerTransaction(final Long makerCheckerId) {
|
||||
|
||||
final CommandSource commandSourceInput = this.commandSourceRepository.findById(makerCheckerId)
|
||||
final CommandSource commandSource = this.commandSourceRepository.findById(makerCheckerId)
|
||||
.orElseThrow(() -> new CommandNotFoundException(makerCheckerId));
|
||||
if (!commandSourceInput.isMarkedAsAwaitingApproval()) {
|
||||
if (!commandSource.isMarkedAsAwaitingApproval()) {
|
||||
throw new CommandNotAwaitingApprovalException(makerCheckerId);
|
||||
}
|
||||
|
||||
this.context.authenticatedUser().validateHasCheckerPermissionTo(commandSourceInput.getPermissionCode());
|
||||
|
||||
return commandSourceInput;
|
||||
AppUser appUser = this.context.authenticatedUser();
|
||||
String permissionCode = commandSource.getPermissionCode();
|
||||
appUser.validateHasCheckerPermissionTo(permissionCode);
|
||||
if (!configurationService.isSameMakerCheckerEnabled() && !appUser.isCheckerSuperUser()) {
|
||||
AppUser maker = commandSource.getMaker();
|
||||
if (maker == null) {
|
||||
throw new UnsupportedCommandException(permissionCode, "Maker user is missing.");
|
||||
}
|
||||
if (Objects.equals(appUser.getId(), maker.getId())) {
|
||||
throw new UnsupportedCommandException(permissionCode, "Can not be checked by the same user.");
|
||||
}
|
||||
}
|
||||
return commandSource;
|
||||
}
|
||||
|
||||
private void validateIsUpdateAllowed() {
|
||||
|
||||
+31
-57
@@ -35,14 +35,12 @@ import org.apache.fineract.batch.exception.ErrorInfo;
|
||||
import org.apache.fineract.commands.domain.CommandProcessingResultType;
|
||||
import org.apache.fineract.commands.domain.CommandSource;
|
||||
import org.apache.fineract.commands.domain.CommandWrapper;
|
||||
import org.apache.fineract.commands.exception.RollbackTransactionAsCommandIsNotApprovedByCheckerException;
|
||||
import org.apache.fineract.commands.exception.UnsupportedCommandException;
|
||||
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||
import org.apache.fineract.commands.provider.CommandHandlerProvider;
|
||||
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.domain.BatchRequestContextHolder;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
|
||||
import org.apache.fineract.infrastructure.core.exception.ErrorHandler;
|
||||
@@ -58,7 +56,6 @@ import org.apache.fineract.infrastructure.security.service.PlatformSecurityConte
|
||||
import org.apache.fineract.useradministration.domain.AppUser;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -76,9 +73,7 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
private final ConfigurationDomainService configurationDomainService;
|
||||
private final CommandHandlerProvider commandHandlerProvider;
|
||||
private final IdempotencyKeyResolver idempotencyKeyResolver;
|
||||
private final IdempotencyKeyGenerator idempotencyKeyGenerator;
|
||||
private final CommandSourceService commandSourceService;
|
||||
private final ErrorHandler errorHandler;
|
||||
|
||||
private final FineractRequestContextHolder fineractRequestContextHolder;
|
||||
private final Gson gson = GoogleGsonSerializerHelper.createSimpleGson();
|
||||
@@ -92,73 +87,68 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
|
||||
Long commandId = (Long) fineractRequestContextHolder.getAttribute(COMMAND_SOURCE_ID, null);
|
||||
boolean isRetry = commandId != null;
|
||||
boolean isEnclosingTransaction = BatchRequestContextHolder.isEnclosingTransaction();
|
||||
|
||||
CommandSource commandSource = null;
|
||||
String idempotencyKey;
|
||||
if (isRetry) {
|
||||
commandSource = commandSourceService.getCommandSource(commandId);
|
||||
idempotencyKey = commandSource.getIdempotencyKey();
|
||||
} else if ((commandId = command.commandId()) != null) { // action on the command itself
|
||||
commandSource = commandSourceService.getCommandSource(commandId);
|
||||
idempotencyKey = commandSource.getIdempotencyKey();
|
||||
} else {
|
||||
idempotencyKey = idempotencyKeyResolver.resolve(wrapper);
|
||||
}
|
||||
exceptionWhenTheRequestAlreadyProcessed(wrapper, idempotencyKey, isRetry);
|
||||
|
||||
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
|
||||
AppUser user = context.authenticatedUser(wrapper);
|
||||
if (commandSource == null) {
|
||||
AppUser user = context.authenticatedUser(wrapper);
|
||||
if (sameTransaction) {
|
||||
if (isEnclosingTransaction) {
|
||||
commandSource = commandSourceService.getInitialCommandSource(wrapper, command, user, idempotencyKey);
|
||||
} else {
|
||||
commandSource = commandSourceService.saveInitialNewTransaction(wrapper, command, user, idempotencyKey);
|
||||
storeCommandIdInContext(commandSource); // Store command id as a request attribute
|
||||
commandId = commandSource.getId();
|
||||
}
|
||||
}
|
||||
if (commandId != null) {
|
||||
storeCommandIdInContext(commandSource); // Store command id as a request attribute
|
||||
}
|
||||
|
||||
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName());
|
||||
if (isApprovedByChecker || (isMakerChecker && user.isCheckerSuperUser())) {
|
||||
commandSource.markAsChecked(user);
|
||||
}
|
||||
setIdempotencyKeyStoreFlag(true);
|
||||
|
||||
final CommandProcessingResult result;
|
||||
try {
|
||||
result = findCommandHandler(wrapper).processCommand(command);
|
||||
result = commandSourceService.processCommand(findCommandHandler(wrapper), command, commandSource, user, isApprovedByChecker,
|
||||
isMakerChecker);
|
||||
} catch (Throwable t) { // NOSONAR
|
||||
RuntimeException mappable = ErrorHandler.getMappable(t);
|
||||
ErrorInfo errorInfo = commandSourceService.generateErrorInfo(mappable);
|
||||
commandSource.setResultStatusCode(errorInfo.getStatusCode());
|
||||
Integer statusCode = errorInfo.getStatusCode();
|
||||
commandSource.setResultStatusCode(statusCode);
|
||||
commandSource.setResult(errorInfo.getMessage());
|
||||
commandSource.setStatus(ERROR);
|
||||
if (!sameTransaction) { // TODO: temporary solution
|
||||
if (statusCode != SC_OK) {
|
||||
commandSource.setStatus(ERROR);
|
||||
}
|
||||
if (!isEnclosingTransaction) { // TODO: temporary solution
|
||||
commandSource = commandSourceService.saveResultNewTransaction(commandSource);
|
||||
}
|
||||
publishHookErrorEvent(wrapper, command, errorInfo); // TODO must be performed in a new transaction
|
||||
// must not throw any exception; must persist in new transaction as the current transaction was already
|
||||
// marked as rollback
|
||||
publishHookErrorEvent(wrapper, command, errorInfo);
|
||||
throw mappable;
|
||||
}
|
||||
|
||||
commandSource.updateForAudit(result);
|
||||
commandSource.setResultStatusCode(SC_OK);
|
||||
commandSource.updateForAudit(result);
|
||||
commandSource.setResult(toApiJsonSerializer.serializeResult(result));
|
||||
commandSource.setStatus(PROCESSED);
|
||||
|
||||
boolean isRollback = !isApprovedByChecker && (result.isRollbackTransaction()
|
||||
|| configurationDomainService.isMakerCheckerEnabledForTask(wrapper.taskPermissionName()));
|
||||
// TODO: this should be removed, can not override audit information (and maker-checker does not work)
|
||||
if (!isRollback && result.hasChanges()) {
|
||||
commandSource.setCommandJson(toApiJsonSerializer.serializeResult(result.getChanges()));
|
||||
}
|
||||
|
||||
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
|
||||
if (sameTransaction) {
|
||||
storeCommandIdInContext(commandSource); // Store command id as a request attribute
|
||||
}
|
||||
|
||||
if (isRollback) {
|
||||
/*
|
||||
* JournalEntry will generate a new transactionId every time. Updating the transactionId with old
|
||||
* transactionId, because as there are no entries are created with new transactionId, will throw an error
|
||||
* when checker approves the transaction
|
||||
*/
|
||||
commandSource.setTransactionId(command.getTransactionId());
|
||||
// TODO: this should be removed together with lines 147-149
|
||||
commandSource.setCommandJson(command.json()); // Set back CommandSource json data
|
||||
throw new RollbackTransactionAsCommandIsNotApprovedByCheckerException(commandSource);
|
||||
}
|
||||
storeCommandIdInContext(commandSource); // Store command id as a request attribute
|
||||
|
||||
result.setRollbackTransaction(null);
|
||||
publishHookEvent(wrapper.entityName(), wrapper.actionName(), command, result); // TODO must be performed in a
|
||||
@@ -201,24 +191,8 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
fineractRequestContextHolder.setAttribute(IDEMPOTENCY_KEY_STORE_FLAG, flag);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Override
|
||||
public CommandProcessingResult logCommand(CommandSource commandSource) {
|
||||
commandSource.markAsAwaitingApproval();
|
||||
if (commandSource.getIdempotencyKey() == null) {
|
||||
commandSource.setIdempotencyKey(idempotencyKeyGenerator.create());
|
||||
}
|
||||
commandSource = commandSourceService.saveResultSameTransaction(commandSource);
|
||||
|
||||
return new CommandProcessingResultBuilder().withCommandId(commandSource.getId()).withEntityId(commandSource.getResourceId())
|
||||
.build();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public CommandProcessingResult fallbackExecuteCommand(Exception e) {
|
||||
if (e instanceof RollbackTransactionAsCommandIsNotApprovedByCheckerException ex) {
|
||||
return logCommand(ex.getCommandSourceResult());
|
||||
}
|
||||
throw ErrorHandler.getMappable(e);
|
||||
}
|
||||
|
||||
@@ -281,10 +255,10 @@ public class SynchronousCommandProcessingService implements CommandProcessingSer
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean validateCommand(final CommandWrapper commandWrapper, final AppUser user) {
|
||||
boolean rollbackTransaction = configurationDomainService.isMakerCheckerEnabledForTask(commandWrapper.taskPermissionName());
|
||||
public boolean validateRollbackCommand(final CommandWrapper commandWrapper, final AppUser user) {
|
||||
user.validateHasPermissionTo(commandWrapper.getTaskPermissionName());
|
||||
return rollbackTransaction;
|
||||
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(commandWrapper.taskPermissionName());
|
||||
return isMakerChecker && !user.isCheckerSuperUser();
|
||||
}
|
||||
|
||||
private void publishHookEvent(final String entityName, final String actionName, JsonCommand command, final Object result) {
|
||||
|
||||
+2
@@ -25,6 +25,8 @@ public interface ConfigurationDomainService {
|
||||
|
||||
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
|
||||
|
||||
boolean isSameMakerCheckerEnabled();
|
||||
|
||||
boolean isAmazonS3Enabled();
|
||||
|
||||
boolean isRescheduleFutureRepaymentsEnabled();
|
||||
|
||||
+65
-18
@@ -27,15 +27,16 @@ public final class BatchRequestContextHolder {
|
||||
|
||||
private BatchRequestContextHolder() {}
|
||||
|
||||
private static final ThreadLocal<Map<String, Object>> batchAttributes = new NamedThreadLocal<>("BatchAttributesForProcessing");
|
||||
private static final ThreadLocal<Map<String, Object>> batchAttributes = new NamedThreadLocal<>("batchAttributes");
|
||||
|
||||
private static final ThreadLocal<Optional<TransactionStatus>> enclosingTransaction = new NamedThreadLocal<>("EnclosingTransaction") {
|
||||
private static final ThreadLocal<Optional<TransactionStatus>> batchTransaction = new NamedThreadLocal<>("batchTransaction") {
|
||||
|
||||
@Override
|
||||
protected Optional<TransactionStatus> initialValue() {
|
||||
return Optional.empty();
|
||||
}
|
||||
};
|
||||
private static final ThreadLocal<Boolean> isEnclosingTransaction = new NamedThreadLocal<>("isEnclosingTransaction");
|
||||
|
||||
/**
|
||||
* True if the batch attributes are set
|
||||
@@ -47,12 +48,12 @@ public final class BatchRequestContextHolder {
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the batch attributes are set and the enclosing transaction is set to true
|
||||
* Returns the batch attributes for the current thread.
|
||||
*
|
||||
* @return
|
||||
* @return the batch attributes for the current thread, cna be null
|
||||
*/
|
||||
public static Optional<TransactionStatus> getEnclosingTransaction() {
|
||||
return enclosingTransaction.get();
|
||||
public static Map<String, Object> getRequestAttributes() {
|
||||
return batchAttributes.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,15 +66,6 @@ public final class BatchRequestContextHolder {
|
||||
batchAttributes.set(requestAttributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the batch attributes for the current thread.
|
||||
*
|
||||
* @return the batch attributes for the current thread, cna be null
|
||||
*/
|
||||
public static Map<String, Object> getRequestAttributes() {
|
||||
return batchAttributes.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the batch attributes for the current thread.
|
||||
*/
|
||||
@@ -82,11 +74,66 @@ public final class BatchRequestContextHolder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the enclosing transaction flag for the current thread.
|
||||
* True if the batch attributes are set and the enclosing transaction is set to true
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static boolean isEnclosingTransaction() {
|
||||
return Boolean.TRUE.equals(isEnclosingTransaction.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the isEnclosingTransaction flag for the current thread.
|
||||
*
|
||||
* @param isEnclosingTransaction
|
||||
*/
|
||||
public static void setIsEnclosingTransaction(boolean isEnclosingTransaction) {
|
||||
BatchRequestContextHolder.isEnclosingTransaction.set(isEnclosingTransaction);
|
||||
}
|
||||
|
||||
public static void resetIsEnclosingTransaction() {
|
||||
isEnclosingTransaction.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the transaction
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static Optional<TransactionStatus> getTransaction() {
|
||||
return batchTransaction.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the enclosing transaction
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static Optional<TransactionStatus> getEnclosingTransaction() {
|
||||
return isEnclosingTransaction() ? getTransaction() : Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the transaction for the current thread.
|
||||
*
|
||||
* @param enclosingTransaction
|
||||
*/
|
||||
public static void setEnclosingTransaction(Optional<TransactionStatus> enclosingTransaction) {
|
||||
BatchRequestContextHolder.enclosingTransaction.set(enclosingTransaction);
|
||||
public static void setTransaction(TransactionStatus enclosingTransaction) {
|
||||
batchTransaction.set(Optional.ofNullable(enclosingTransaction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the enclosing transaction for the current thread.
|
||||
*
|
||||
* @param enclosingTransaction
|
||||
*/
|
||||
public static void setEnclosingTransaction(TransactionStatus enclosingTransaction) {
|
||||
if (isEnclosingTransaction()) {
|
||||
setTransaction(enclosingTransaction);
|
||||
}
|
||||
}
|
||||
|
||||
public static void resetTransaction() {
|
||||
batchTransaction.set(Optional.empty());
|
||||
}
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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.infrastructure.core.exceptionmapper;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import jakarta.ws.rs.ext.ExceptionMapper;
|
||||
import jakarta.ws.rs.ext.Provider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.commands.exception.RollbackTransactionNotApprovedException;
|
||||
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* An {@link ExceptionMapper} to map {@link PlatformApiDataValidationException} thrown by platform into a HTTP API
|
||||
* friendly format.
|
||||
*
|
||||
* The {@link PlatformApiDataValidationException} is typically thrown in data validation of the parameters passed in
|
||||
* with an api request.
|
||||
*/
|
||||
@Provider
|
||||
@Component
|
||||
@Scope("singleton")
|
||||
@Slf4j
|
||||
public class RollbackTransactionNotApprovedExceptionMapper
|
||||
implements FineractExceptionMapper, ExceptionMapper<RollbackTransactionNotApprovedException> {
|
||||
|
||||
@Override
|
||||
public Response toResponse(final RollbackTransactionNotApprovedException exception) {
|
||||
log.warn("Exception: {}", exception.getClass().getName());
|
||||
return Response.ok().entity(new Gson().toJson(exception.getResult())).type(MediaType.APPLICATION_JSON).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int errorCode() {
|
||||
return 2000;
|
||||
}
|
||||
}
|
||||
+4
@@ -47,8 +47,12 @@ public class UnsupportedCommandExceptionMapper implements ExceptionMapper<Unsupp
|
||||
final List<ApiParameterError> errors = new ArrayList<>();
|
||||
|
||||
final StringBuilder validationErrorCode = new StringBuilder("error.msg.command.unsupported");
|
||||
String message = exception.getMessage();
|
||||
final StringBuilder defaultEnglishMessage = new StringBuilder("The command ").append(exception.getUnsupportedCommandName())
|
||||
.append(" is not supported.");
|
||||
if (message != null) {
|
||||
defaultEnglishMessage.append(" ").append(message);
|
||||
}
|
||||
log.warn("Exception: {}, Message: {}", exception.getClass().getName(), defaultEnglishMessage);
|
||||
final ApiParameterError error = ApiParameterError.parameterError(validationErrorCode.toString(), defaultEnglishMessage.toString(),
|
||||
exception.getUnsupportedCommandName(), exception.getUnsupportedCommandName());
|
||||
|
||||
+1
-4
@@ -25,7 +25,6 @@ import lombok.RequiredArgsConstructor;
|
||||
import org.apache.fineract.commands.domain.CommandSourceRepository;
|
||||
import org.apache.fineract.commands.service.CommandSourceService;
|
||||
import org.apache.fineract.commands.service.SynchronousCommandProcessingService;
|
||||
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -39,11 +38,9 @@ public class IdempotencyStoreHelper {
|
||||
|
||||
public void storeCommandResult(Integer response, String body, Long commandId) {
|
||||
commandSourceRepository.findById(commandId).ifPresent(commandSource -> {
|
||||
boolean sameTransaction = BatchRequestContextHolder.getEnclosingTransaction().isPresent();
|
||||
commandSource.setResultStatusCode(response);
|
||||
commandSource.setResult(body);
|
||||
commandSource = sameTransaction ? commandSourceService.saveResultSameTransaction(commandSource)
|
||||
: commandSourceService.saveResultNewTransaction(commandSource);
|
||||
commandSourceService.saveResultSameTransaction(commandSource);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -657,6 +657,10 @@ public class AppUser extends AbstractPersistableCustom implements PlatformUser {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isCheckerSuperUser() {
|
||||
return hasPermissionTo("CHECKER_SUPER_USER");
|
||||
}
|
||||
|
||||
public void validateHasDatatableReadPermission(final String datatable) {
|
||||
if (hasNotPermissionForDatatable(datatable, "READ")) {
|
||||
throw new NoAuthorizationException("Not authorised to read datatable: " + datatable);
|
||||
|
||||
+6
@@ -34,6 +34,7 @@ import org.apache.fineract.infrastructure.core.domain.ActionContext;
|
||||
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
|
||||
import org.apache.fineract.infrastructure.core.exceptionmapper.IdempotentCommandExceptionMapper;
|
||||
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -48,6 +49,11 @@ public class IdempotencyCommandProcessFailedExceptionTest {
|
||||
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
public void tearDown() {
|
||||
ThreadLocalContextUtil.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInconsistentStatus() {
|
||||
IdempotentCommandExceptionMapper mapper = new IdempotentCommandExceptionMapper();
|
||||
|
||||
Reference in New Issue
Block a user