Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
package org.apache.fineract.commands.domain;

import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.jpa.repository.Modifying;
Expand All @@ -29,6 +30,28 @@ public interface CommandSourceRepository extends JpaRepository<CommandSource, Lo

CommandSource findByActionNameAndEntityNameAndIdempotencyKey(String actionName, String entityName, String idempotencyKey);

@Query(value = """
select distinct c.* from m_portfolio_command_source c
where upper(c.action_name) = upper(?1)
and upper(c.entity_name) = upper(?2)
and (
c.resource_id = ?3
or c.subresource_id = ?3
or c.client_id = ?3
or c.loan_id = ?3
or c.savings_account_id = ?3
or c.group_id = ?3
or c.office_id = ?3
or c.product_id = ?3
or c.creditbureau_id = ?3
or c.organisation_creditbureau_id = ?3
)
and c.status = ?4
order by c.made_on_date_utc desc
""", nativeQuery = true)
List<CommandSource> findPendingByActionAndEntityAndResource(@Param("actionName") String actionName,
@Param("entityName") String entityName, @Param("resourceId") Long resourceId, @Param("status") Integer status);

@Modifying(flushAutomatically = true)
@Query("delete from CommandSource c where c.status = :status and c.madeOnDate is not null and c.madeOnDate <= :dateForPurgeCriteria")
void deleteOlderEventsWithStatus(@Param("status") Integer status, @Param("dateForPurgeCriteria") OffsetDateTime dateForPurgeCriteria);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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.exception.AbstractPlatformDomainRuleException;

/**
* Thrown when a checker-only user (has _CHECKER permission but not the base permission) attempts to initiate an action
* directly, with no pending maker submission to approve.
*/
public class MakerCheckerCheckerOnlyInitiationException extends AbstractPlatformDomainRuleException {

public MakerCheckerCheckerOnlyInitiationException(final String permissionCode) {
super("error.msg.maker.checker.checker.only.cannot.initiate",
"You have checker-only permission for this action. You cannot initiate it. Use the maker-checker approval flow to approve a pending submission.",
permissionCode);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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.exception.AbstractPlatformDomainRuleException;

/**
* Thrown when a maker attempts to submit an action that already has a pending checker approval entry for the same
* action, entity, and resource.
*/
public class MakerCheckerDuplicatePendingSubmissionException extends AbstractPlatformDomainRuleException {

public MakerCheckerDuplicatePendingSubmissionException(final String actionName, final String entityName) {
super("error.msg.maker.checker.duplicate.pending.submission",
"This action is already pending checker approval. Please wait for it to be approved or rejected before resubmitting.",
actionName, entityName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ private void validateMakerChecker(CommandSource commandSource, AppUser user, boo
String permission = commandSource.getPermissionCode();
boolean isMakerChecker = configurationDomainService.isMakerCheckerEnabledForTask(permission);
if (isMakerChecker || result.isRollbackTransaction()) {
if (isApprovedByChecker || user.isCheckerSuperUser()) {
boolean userHasCheckerPermission = !user.hasNotPermissionForAnyOf("CHECKER_SUPER_USER", permission + "_CHECKER");
if (isApprovedByChecker || userHasCheckerPermission) {
commandSource.markAsChecked(user);
} else {
if (commandSource.isSanitized()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,14 @@
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.commands.domain.CommandProcessingResultType;
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.CommandNotAwaitingApprovalException;
import org.apache.fineract.commands.exception.CommandNotFoundException;
import org.apache.fineract.commands.exception.MakerCheckerCheckerOnlyInitiationException;
import org.apache.fineract.commands.exception.MakerCheckerDuplicatePendingSubmissionException;
import org.apache.fineract.commands.exception.UnsupportedCommandException;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
Expand Down Expand Up @@ -56,18 +59,45 @@ public class PortfolioCommandSourceWritePlatformServiceImpl implements Portfolio
@Override
public CommandProcessingResult logCommandSource(final CommandWrapper wrapper) {
boolean isApprovedByChecker = false;
final AppUser currentUser = this.context.authenticatedUser(wrapper);

// check if is update of own account details
if (wrapper.isChangeOfOwnUserDetails(this.context.authenticatedUser(wrapper).getId())) {
if (wrapper.isChangeOfOwnUserDetails(currentUser.getId())) {
// then allow this operation to proceed.
// maker checker doesnt mean anything here.
isApprovedByChecker = true; // set to true in case permissions have
// been maker-checker enabled by
// accident.
} else {
// if not user changing their own details - check user has
// permission to perform specific task.
this.context.authenticatedUser(wrapper).validateHasPermissionTo(wrapper.getTaskPermissionName());
final String taskPermission = wrapper.getTaskPermissionName();
final boolean hasBasePermission = !currentUser.hasNotPermissionForAnyOf(taskPermission);
final boolean hasCheckerPermission = !currentUser.hasNotPermissionForAnyOf("CHECKER_SUPER_USER", taskPermission + "_CHECKER");

if (!hasBasePermission && hasCheckerPermission) {
// Checker-only user: find and approve the pending entry for this action+entity+resource
final Long resourceId = resolveResourceId(wrapper);
final List<CommandSource> pendingCommands = findPendingCommandsByResource(wrapper, resourceId);
if (!pendingCommands.isEmpty()) {
final CommandSource pendingCommand = pendingCommands.get(0);
log.debug("Checker-only user {} auto-approving pending command id={} for {}/{}", currentUser.getUsername(),
pendingCommand.getId(), wrapper.entityName(), wrapper.actionName());
return approveEntry(pendingCommand.getId());
} else {
throw new MakerCheckerCheckerOnlyInitiationException(taskPermission);
}
} else {
currentUser.validateHasPermissionTo(taskPermission);

if (!hasCheckerPermission && configurationService.isMakerCheckerEnabledForTask(taskPermission)) {
final Long resourceId = resolveResourceId(wrapper);
final List<CommandSource> pendingCommands = findPendingCommandsByResource(wrapper, resourceId);
if (!pendingCommands.isEmpty()) {
log.warn("Maker {} attempted duplicate submission for {}/{} - pending id={}", currentUser.getUsername(),
wrapper.entityName(), wrapper.actionName(), pendingCommands.get(0).getId());
throw new MakerCheckerDuplicatePendingSubmissionException(wrapper.actionName(), wrapper.entityName());
}
}
}
}
validateIsUpdateAllowed();

Expand Down Expand Up @@ -142,6 +172,33 @@ private void validateIsUpdateAllowed() {
this.schedulerJobRunnerReadService.isUpdatesAllowed();
}

private List<CommandSource> findPendingCommandsByResource(final CommandWrapper wrapper, final Long resourceId) {
if (resourceId == null) {
return List.of();
}
return this.commandSourceRepository.findPendingByActionAndEntityAndResource(wrapper.actionName(), wrapper.entityName(), resourceId,
CommandProcessingResultType.AWAITING_APPROVAL.getValue());
}

private Long resolveResourceId(final CommandWrapper wrapper) {
if (wrapper.getEntityId() != null) {
return wrapper.getEntityId();
}
if (wrapper.getLoanId() != null) {
return wrapper.getLoanId();
}
if (wrapper.getSavingsId() != null) {
return wrapper.getSavingsId();
}
if (wrapper.getClientId() != null) {
return wrapper.getClientId();
}
if (wrapper.getGroupId() != null) {
return wrapper.getGroupId();
}
return null;
}

@Override
public Long rejectEntry(final Long makerCheckerId) {
final CommandSource commandSourceInput = validateMakerCheckerTransaction(makerCheckerId);
Expand Down
Loading