FINERACT-1977: 4-eye-principle, maker-checker permission

This commit is contained in:
jmarta
2024-01-11 11:46:32 +01:00
committed by Adam Saghy
parent 2fe1b0e7d3
commit 16eac933d6
69 changed files with 887 additions and 359 deletions
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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);
}
@@ -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,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() {
@@ -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) {
@@ -25,6 +25,8 @@ public interface ConfigurationDomainService {
boolean isMakerCheckerEnabledForTask(String taskPermissionCode);
boolean isSameMakerCheckerEnabled();
boolean isAmazonS3Enabled();
boolean isRescheduleFutureRepaymentsEnabled();
@@ -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());
}
}
@@ -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;
}
}
@@ -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());
@@ -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);
@@ -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();