This is an automated email from the ASF dual-hosted git repository. arnold pushed a commit to branch develop in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push: new cce790205 [FINERACT-1678] Bypass LoanAccount Write Protection cce790205 is described below commit cce79020544f34e7bd607a05d27be124a4bfa24a Author: taskain7 <task...@gmail.com> AuthorDate: Mon Oct 17 12:12:48 2022 +0200 [FINERACT-1678] Bypass LoanAccount Write Protection --- .../service/InlineLoanCOBExecutorServiceImpl.java | 8 ++++- .../jobs/filter/LoanCOBApiFilter.java | 8 ++++- .../useradministration/domain/AppUser.java | 4 +++ .../db/changelog/tenant/changelog-tenant.xml | 1 + ...dd_bypass_loan_write_transaction_permission.xml | 34 ++++++++++++++++++++++ .../jobs/filter/LoanCOBApiFilterTest.java | 30 +++++++++++++++++++ 6 files changed, 83 insertions(+), 2 deletions(-) diff --git a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java index b8e10bfaa..2c50ec66a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/cob/service/InlineLoanCOBExecutorServiceImpl.java @@ -42,6 +42,7 @@ import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameter; import org.apache.fineract.infrastructure.jobs.domain.CustomJobParameterRepository; import org.apache.fineract.infrastructure.jobs.exception.JobNotFoundException; import org.apache.fineract.infrastructure.jobs.service.InlineExecutorService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.infrastructure.springbatch.SpringBatchJobConstants; import org.jetbrains.annotations.NotNull; import org.springframework.batch.core.BatchStatus; @@ -77,6 +78,7 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService, private final JobExplorer jobExplorer; private final TransactionTemplate transactionTemplate; private final CustomJobParameterRepository customJobParameterRepository; + private final PlatformSecurityContext context; private Gson gson; @@ -168,10 +170,14 @@ public class InlineLoanCOBExecutorServiceImpl implements InlineExecutorService, } private boolean isLockOverrulable(LoanAccountLock loanAccountLock) { - if (LockOwner.LOAN_COB_PARTITIONING.equals(loanAccountLock.getLockOwner())) { + if (LockOwner.LOAN_COB_PARTITIONING.equals(loanAccountLock.getLockOwner()) || isBypassUser()) { return true; } else { return StringUtils.isNotBlank(loanAccountLock.getError()); } } + + private boolean isBypassUser() { + return context.getAuthenticatedUserIfPresent().isBypassUser(); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java index dea636aa0..08e33e286 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java @@ -35,6 +35,7 @@ import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.cob.service.LoanAccountLockService; import org.apache.fineract.infrastructure.core.data.ApiGlobalErrorResponse; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -49,6 +50,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter { private final GLIMAccountInfoRepository glimAccountInfoRepository; private final LoanAccountLockService loanAccountLockService; + private final PlatformSecurityContext context; private static final List<HttpMethod> HTTP_METHODS = List.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.DELETE); private static final Function<String, Boolean> URL_FUNCTION = s -> s.matches("/loans/\\d+.*") || s.matches("/loans/glimAccount/\\d+.*"); @@ -59,7 +61,7 @@ public class LoanCOBApiFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { - if (!isOnApiList(request)) { + if (!isOnApiList(request) || isBypassUser()) { proceed(filterChain, request, response); } else { Iterable<String> split = Splitter.on('/').split(request.getPathInfo()); @@ -74,6 +76,10 @@ public class LoanCOBApiFilter extends OncePerRequestFilter { } } + private boolean isBypassUser() { + return context.getAuthenticatedUserIfPresent().isBypassUser(); + } + private boolean isLoanLocked(Long loanId, boolean isGlim) { if (!isGlim) { return loanAccountLockService.isLoanHardLocked(loanId); diff --git a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java index 0e9133c83..36c1f77ea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java +++ b/fineract-provider/src/main/java/org/apache/fineract/useradministration/domain/AppUser.java @@ -434,6 +434,10 @@ public class AppUser extends AbstractPersistableCustom implements PlatformUser { return this.enabled; } + public boolean isBypassUser() { + return hasAnyPermission("BYPASS_LOAN_WRITE_PROTECTION"); + } + public String getFirstname() { return this.firstname; } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index f652ebe41..f0f92f568 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -84,4 +84,5 @@ <include file="parts/0062_add_fraud_attribute_to_loan.xml" relativeToChangelogFile="true"/> <include file="parts/0063_add_permissions_for_external_event_configuration.xml" relativeToChangelogFile="true"/> <include file="parts/0064_refactor_loan_transaction_strategy.xml" relativeToChangelogFile="true"/> + <include file="parts/0065_add_bypass_loan_write_transaction_permission.xml" relativeToChangelogFile="true"/> </databaseChangeLog> diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0065_add_bypass_loan_write_transaction_permission.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0065_add_bypass_loan_write_transaction_permission.xml new file mode 100644 index 000000000..c411b5ec4 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0065_add_bypass_loan_write_transaction_permission.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + 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. + +--> +<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + <changeSet author="fineract" id="1"> + <insert tableName="m_permission"> + <column name="grouping" value="transaction_loan"/> + <column name="code" value="BYPASS_LOAN_WRITE_PROTECTION"/> + <column name="entity_name" value="LOAN"/> + <column name="action_name" value="BYPASS"/> + <column name="can_maker_checker" valueBoolean="false"/> + </insert> + </changeSet> +</databaseChangeLog> diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java index de7bf1ce1..216c670a4 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java @@ -31,9 +31,11 @@ import java.util.Collections; import javax.servlet.FilterChain; import javax.servlet.ServletException; import org.apache.fineract.cob.service.LoanAccountLockService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.domain.GLIMAccountInfoRepository; import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonitoringAccount; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.useradministration.domain.AppUser; import org.apache.http.HttpStatus; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -55,14 +57,34 @@ class LoanCOBApiFilterTest { private LoanAccountLockService loanAccountLockService; @Mock private GLIMAccountInfoRepository glimAccountInfoRepository; + @Mock + private PlatformSecurityContext context; @Test void shouldProceedWhenUrlDoesNotMatch() throws ServletException, IOException { MockHttpServletRequest request = mock(MockHttpServletRequest.class); + MockHttpServletResponse response = mock(MockHttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + given(request.getPathInfo()).willReturn("/jobs/2/inline"); given(request.getMethod()).willReturn(HTTPMethods.POST.value()); + + testObj.doFilterInternal(request, response, filterChain); + verify(filterChain, times(1)).doFilter(request, response); + } + + @Test + void shouldProceedWhenUserHasBypassPermission() throws ServletException, IOException { + MockHttpServletRequest request = mock(MockHttpServletRequest.class); MockHttpServletResponse response = mock(MockHttpServletResponse.class); FilterChain filterChain = mock(FilterChain.class); + AppUser appUser = mock(AppUser.class); + + given(request.getPathInfo()).willReturn("/jobs/2/inline"); + given(request.getMethod()).willReturn(HTTPMethods.POST.value()); + given(context.getAuthenticatedUserIfPresent()).willReturn(appUser); + given(appUser.isBypassUser()).willReturn(true); + testObj.doFilterInternal(request, response, filterChain); verify(filterChain, times(1)).doFilter(request, response); } @@ -72,10 +94,12 @@ class LoanCOBApiFilterTest { MockHttpServletRequest request = mock(MockHttpServletRequest.class); MockHttpServletResponse response = mock(MockHttpServletResponse.class); FilterChain filterChain = mock(FilterChain.class); + AppUser appUser = mock(AppUser.class); given(request.getPathInfo()).willReturn("/loans/2/charges"); given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); + given(context.getAuthenticatedUserIfPresent()).willReturn(appUser); testObj.doFilterInternal(request, response, filterChain); verify(filterChain, times(1)).doFilter(request, response); @@ -86,10 +110,12 @@ class LoanCOBApiFilterTest { MockHttpServletRequest request = mock(MockHttpServletRequest.class); MockHttpServletResponse response = mock(MockHttpServletResponse.class); FilterChain filterChain = mock(FilterChain.class); + AppUser appUser = mock(AppUser.class); given(request.getPathInfo()).willReturn("/loans/2/charges"); given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(false); + given(context.getAuthenticatedUserIfPresent()).willReturn(appUser); testObj.doFilterInternal(request, response, filterChain); verify(filterChain, times(1)).doFilter(request, response); @@ -101,11 +127,13 @@ class LoanCOBApiFilterTest { MockHttpServletResponse response = mock(MockHttpServletResponse.class); FilterChain filterChain = mock(FilterChain.class); PrintWriter writer = mock(PrintWriter.class); + AppUser appUser = mock(AppUser.class); given(request.getPathInfo()).willReturn("/loans/2/charges"); given(request.getMethod()).willReturn(HTTPMethods.POST.value()); given(loanAccountLockService.isLoanHardLocked(2L)).willReturn(true); given(response.getWriter()).willReturn(writer); + given(context.getAuthenticatedUserIfPresent()).willReturn(appUser); testObj.doFilterInternal(request, response, filterChain); verify(response, times(1)).setStatus(HttpStatus.SC_CONFLICT); @@ -120,6 +148,7 @@ class LoanCOBApiFilterTest { GroupLoanIndividualMonitoringAccount glimAccount = mock(GroupLoanIndividualMonitoringAccount.class); Loan loan = mock(Loan.class); Long loanId = 2L; + AppUser appUser = mock(AppUser.class); given(request.getPathInfo()).willReturn("/loans/glimAccount/2"); given(request.getMethod()).willReturn(HTTPMethods.POST.value()); @@ -128,6 +157,7 @@ class LoanCOBApiFilterTest { given(loan.getId()).willReturn(loanId); given(loanAccountLockService.isLoanHardLocked(loanId)).willReturn(true); given(response.getWriter()).willReturn(writer); + given(context.getAuthenticatedUserIfPresent()).willReturn(appUser); testObj.doFilterInternal(request, response, filterChain); verify(response, times(1)).setStatus(HttpStatus.SC_CONFLICT);