From 2bd3a4d9fe588509633668021502b4f73628eb3b Mon Sep 17 00:00:00 2001 From: damien-git Date: Fri, 29 Mar 2024 12:13:45 -0400 Subject: [PATCH] [MODORDERS-1073] Added a permission to bypass acqunit checks (#481) --- descriptors/ModuleDescriptor-template.json | 23 ++++++- .../invoices/utils/AcqDesiredPermissions.java | 7 ++- .../org/folio/rest/impl/InvoiceHelper.java | 8 +++ .../folio/rest/impl/InvoiceLineHelper.java | 11 ++++ .../org/folio/rest/impl/ProtectionHelper.java | 5 ++ .../org/folio/utils/UserPermissionsUtil.java | 4 ++ .../protection/InvoicesProtectionTest.java | 19 ++++++ .../impl/protection/LinesProtectionTest.java | 61 ++++++++++++++++--- .../protection/ProtectedEntityTestBase.java | 2 +- 9 files changed, 130 insertions(+), 10 deletions(-) diff --git a/descriptors/ModuleDescriptor-template.json b/descriptors/ModuleDescriptor-template.json index c173a9d8b..3cbd67436 100644 --- a/descriptors/ModuleDescriptor-template.json +++ b/descriptors/ModuleDescriptor-template.json @@ -10,6 +10,9 @@ "methods": ["GET"], "pathPattern": "/invoice/invoices", "permissionsRequired": ["invoice.invoices.collection.get"], + "permissionsDesired": [ + "invoices.bypass-acquisition-units" + ], "modulePermissions": [ "invoice-storage.invoices.collection.get", "invoice-storage.invoice-lines.collection.get", @@ -36,6 +39,9 @@ "permissionsRequired": [ "invoice.invoices.item.get" ], + "permissionsDesired": [ + "invoices.bypass-acquisition-units" + ], "modulePermissions": [ "invoice-storage.invoices.item.get", "invoice-storage.invoice-lines.collection.get", @@ -52,7 +58,8 @@ "invoices.fiscal-year.update", "invoice.item.approve", "invoice.item.pay", - "invoice.item.cancel" + "invoice.item.cancel", + "invoices.bypass-acquisition-units" ], "modulePermissions": [ "configuration.entries.collection.get", @@ -109,6 +116,9 @@ "methods": ["GET"], "pathPattern": "/invoice/invoice-lines", "permissionsRequired": ["invoice.invoice-lines.collection.get"], + "permissionsDesired": [ + "invoices.bypass-acquisition-units" + ], "modulePermissions": [ "invoice-storage.invoice-lines.collection.get", "acquisitions-units-storage.units.collection.get", @@ -148,6 +158,9 @@ "methods": ["GET"], "pathPattern": "/invoice/invoice-lines/{id}", "permissionsRequired": ["invoice.invoice-lines.item.get"], + "permissionsDesired": [ + "invoices.bypass-acquisition-units" + ], "modulePermissions": [ "invoice-storage.invoice-lines.item.get", "invoice-storage.invoice-lines.item.put", @@ -163,6 +176,9 @@ "methods": ["PUT"], "pathPattern": "/invoice/invoice-lines/{id}", "permissionsRequired": ["invoice.invoice-lines.item.put"], + "permissionsDesired": [ + "invoices.bypass-acquisition-units" + ], "modulePermissions": [ "invoice-storage.invoice-lines.item.put", "invoice-storage.invoice-lines.item.get", @@ -897,6 +913,11 @@ "batch-voucher.export-configurations.credentials.test" ] }, + { + "permissionName": "invoices.bypass-acquisition-units", + "displayName": "Bypass acquisition units checks", + "description": "Backend internal permission to bypass invoice acquisition units checks" + }, { "permissionName": "invoices.acquisitions-units-assignments.assign", "displayName": "Acquisitions unit assignment - create unit assignment", diff --git a/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java b/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java index 473871068..caa1d25cf 100644 --- a/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java +++ b/src/main/java/org/folio/invoices/utils/AcqDesiredPermissions.java @@ -11,7 +11,8 @@ public enum AcqDesiredPermissions { APPROVE("invoice.item.approve"), PAY("invoice.item.pay"), CANCEL("invoice.item.cancel"), - FISCAL_YEAR_UPDATE("invoices.fiscal-year.update"); + FISCAL_YEAR_UPDATE("invoices.fiscal-year.update"), + BYPASS_ACQ_UNITS("invoices.bypass-acquisition-units"); private String permission; private static final List values; @@ -32,4 +33,8 @@ public String getPermission() { public static List getValues() { return values; } + + public static List getValuesExceptBypass() { + return values.stream().filter(v -> !BYPASS_ACQ_UNITS.getPermission().equals(v)).toList(); + } } diff --git a/src/main/java/org/folio/rest/impl/InvoiceHelper.java b/src/main/java/org/folio/rest/impl/InvoiceHelper.java index d4fe3c561..9d4a943c9 100644 --- a/src/main/java/org/folio/rest/impl/InvoiceHelper.java +++ b/src/main/java/org/folio/rest/impl/InvoiceHelper.java @@ -9,6 +9,7 @@ import static javax.money.Monetary.getDefaultRounding; import static org.apache.commons.lang3.StringUtils.EMPTY; import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.folio.invoices.utils.AcqDesiredPermissions.BYPASS_ACQ_UNITS; import static org.folio.invoices.utils.ErrorCodes.CANNOT_RESET_INVOICE_FISCAL_YEAR; import static org.folio.invoices.utils.ErrorCodes.INVALID_INVOICE_TRANSITION_ON_PAID_STATUS; import static org.folio.invoices.utils.ErrorCodes.MULTIPLE_ADJUSTMENTS_FISCAL_YEARS; @@ -22,6 +23,7 @@ import static org.folio.invoices.utils.ProtectedOperationType.UPDATE; import static org.folio.invoices.utils.ResourcePathResolver.INVOICES; import static org.folio.services.voucher.VoucherCommandService.VOUCHER_NUMBER_PREFIX_CONFIG_QUERY; +import static org.folio.utils.UserPermissionsUtil.userHasDesiredPermission; import static org.folio.utils.UserPermissionsUtil.verifyUserHasAssignPermission; import static org.folio.utils.UserPermissionsUtil.verifyUserHasFiscalYearUpdatePermission; import static org.folio.utils.UserPermissionsUtil.verifyUserHasInvoiceApprovePermission; @@ -262,6 +264,9 @@ public Future getInvoices(int limit, int offset, String query } private Future buildGetInvoicesQuery(String query) { + if (userHasDesiredPermission(BYPASS_ACQ_UNITS, okapiHeaders)) { + return succeededFuture(query); + } return protectionHelper.buildAcqUnitsCqlExprToSearchRecords(INVOICES) .map(acqUnitsCqlExpr -> { if (isEmpty(query)) { @@ -384,6 +389,9 @@ private List filterUpdatedLines(List invoiceLines, Lis * acquisitions units */ private Future validateAcqUnitsOnUpdate(Invoice updatedInvoice, Invoice persistedInvoice) { + if (userHasDesiredPermission(BYPASS_ACQ_UNITS, okapiHeaders)) { + return Future.succeededFuture(); + } List updatedAcqUnitIds = updatedInvoice.getAcqUnitIds(); List currentAcqUnitIds = persistedInvoice.getAcqUnitIds(); verifyUserHasManagePermission(updatedAcqUnitIds, currentAcqUnitIds, okapiHeaders); diff --git a/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java b/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java index dceba9e79..188d680da 100644 --- a/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java +++ b/src/main/java/org/folio/rest/impl/InvoiceLineHelper.java @@ -3,6 +3,7 @@ import static io.vertx.core.Future.succeededFuture; import static java.util.stream.Collectors.toList; import static org.apache.commons.lang3.StringUtils.isEmpty; +import static org.folio.invoices.utils.AcqDesiredPermissions.BYPASS_ACQ_UNITS; import static org.folio.invoices.utils.ErrorCodes.CANNOT_DELETE_INVOICE_LINE; import static org.folio.invoices.utils.ErrorCodes.FAILED_TO_UPDATE_INVOICE_AND_OTHER_LINES; import static org.folio.invoices.utils.ErrorCodes.FAILED_TO_UPDATE_PONUMBERS; @@ -18,6 +19,7 @@ import static org.folio.invoices.utils.ProtectedOperationType.UPDATE; import static org.folio.invoices.utils.ResourcePathResolver.INVOICE_LINES; import static org.folio.invoices.utils.ResourcePathResolver.resourcesPath; +import static org.folio.utils.UserPermissionsUtil.userHasDesiredPermission; import java.util.ArrayList; import java.util.Collections; @@ -86,6 +88,15 @@ public InvoiceLineHelper(Map okapiHeaders, Context ctx) { } public Future getInvoiceLines(int limit, int offset, String query) { + if (userHasDesiredPermission(BYPASS_ACQ_UNITS, okapiHeaders)) { + String endpoint; + if (isEmpty(query)) { + endpoint = resourcesPath(INVOICE_LINES); + } else { + endpoint = String.format(GET_INVOICE_LINES_BY_QUERY, limit, offset, getEndpointWithQuery(query)); + } + return invoiceLineService.getInvoiceLines(endpoint, buildRequestContext()); + } return protectionHelper.buildAcqUnitsCqlExprToSearchRecords(INVOICE_LINES) .compose(acqUnitsCqlExpr -> { String queryParam; diff --git a/src/main/java/org/folio/rest/impl/ProtectionHelper.java b/src/main/java/org/folio/rest/impl/ProtectionHelper.java index f330a36d5..580a9662c 100644 --- a/src/main/java/org/folio/rest/impl/ProtectionHelper.java +++ b/src/main/java/org/folio/rest/impl/ProtectionHelper.java @@ -1,11 +1,13 @@ package org.folio.rest.impl; import static io.vertx.core.Future.succeededFuture; +import static org.folio.invoices.utils.AcqDesiredPermissions.BYPASS_ACQ_UNITS; import static org.folio.invoices.utils.ErrorCodes.ACQ_UNITS_NOT_FOUND; import static org.folio.invoices.utils.ErrorCodes.USER_HAS_NO_PERMISSIONS; import static org.folio.invoices.utils.HelperUtils.ALL_UNITS_CQL; import static org.folio.invoices.utils.HelperUtils.convertIdsToCqlQuery; import static org.folio.services.AcquisitionsUnitsService.ACQUISITIONS_UNIT_ID; +import static org.folio.utils.UserPermissionsUtil.userHasDesiredPermission; import java.util.ArrayList; import java.util.List; @@ -56,6 +58,9 @@ public ProtectionHelper(Map okapiHeaders, Context ctx) { * exist; successfully otherwise */ public Future isOperationRestricted(List unitIds, ProtectedOperationType operation) { + if (userHasDesiredPermission(BYPASS_ACQ_UNITS, okapiHeaders)) { + return Future.succeededFuture(); + } if (CollectionUtils.isNotEmpty(unitIds)) { return getUnitsByIds(unitIds).compose(units -> { if (unitIds.size() == units.size()) { diff --git a/src/main/java/org/folio/utils/UserPermissionsUtil.java b/src/main/java/org/folio/utils/UserPermissionsUtil.java index 5f63328af..90953c5d6 100644 --- a/src/main/java/org/folio/utils/UserPermissionsUtil.java +++ b/src/main/java/org/folio/utils/UserPermissionsUtil.java @@ -40,6 +40,10 @@ public static void verifyUserHasAssignPermission(List acqUnitIds, Map okapiHeaders) { + return getProvidedPermissions(okapiHeaders).contains(acqPerm.getPermission()); + } + public static boolean isUserDoesNotHaveDesiredPermission(AcqDesiredPermissions acqPerm, Map okapiHeaders) { return !getProvidedPermissions(okapiHeaders).contains(acqPerm.getPermission()); } diff --git a/src/test/java/org/folio/rest/impl/protection/InvoicesProtectionTest.java b/src/test/java/org/folio/rest/impl/protection/InvoicesProtectionTest.java index c13d9b2c1..68bb5ce68 100644 --- a/src/test/java/org/folio/rest/impl/protection/InvoicesProtectionTest.java +++ b/src/test/java/org/folio/rest/impl/protection/InvoicesProtectionTest.java @@ -2,6 +2,7 @@ import static io.vertx.core.json.Json.encodePrettily; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.folio.invoices.utils.AcqDesiredPermissions.BYPASS_ACQ_UNITS; import static org.folio.invoices.utils.ErrorCodes.ACQ_UNITS_NOT_FOUND; import static org.folio.invoices.utils.ErrorCodes.USER_HAS_NO_ACQ_PERMISSIONS; import static org.folio.invoices.utils.ErrorCodes.USER_HAS_NO_PERMISSIONS; @@ -10,6 +11,7 @@ import static org.folio.rest.impl.MockServer.addMockEntry; import static org.folio.rest.impl.ProtectionHelper.ACQUISITIONS_UNIT_IDS; import static org.folio.rest.impl.protection.ProtectedOperations.UPDATE; +import static org.folio.utils.UserPermissionsUtil.OKAPI_HEADER_PERMISSIONS; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; @@ -18,6 +20,8 @@ import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.core.IsEqual.equalTo; +import io.restassured.http.Header; +import io.vertx.core.json.JsonArray; import io.vertx.junit5.VertxExtension; import java.util.Arrays; import java.util.Collections; @@ -211,4 +215,19 @@ public void testGetInvoiceWithAssignedSoftDeletedUnit() { assertThat(acquisitionsUnits, hasSize(1)); assertThat(acquisitionsUnits.get(0).getIsDeleted(), is(true)); } + + @ParameterizedTest + @ValueSource(strings = { + "READ" + }) + void testBypassAcqUnitChecks(ProtectedOperations operation) { + Header permissionHeader = new Header(OKAPI_HEADER_PERMISSIONS, + new JsonArray(List.of(BYPASS_ACQ_UNITS.getPermission())).encode()); + Headers headers = new Headers(X_OKAPI_TENANT, permissionHeader, X_OKAPI_USER_WITH_UNITS_NOT_ASSIGNED_TO_RECORD); + Invoice invoice = prepareInvoice(Collections.emptyList()); + operation.process(INVOICE_PATH, encodePrettily(invoice.withAcqUnitIds(PROTECTED_UNITS)), + headers, operation.getContentType(), operation.getCode()); + + validateNumberOfRequests(0, 0); + } } diff --git a/src/test/java/org/folio/rest/impl/protection/LinesProtectionTest.java b/src/test/java/org/folio/rest/impl/protection/LinesProtectionTest.java index 7f21d6f9d..a2a0bf5ce 100644 --- a/src/test/java/org/folio/rest/impl/protection/LinesProtectionTest.java +++ b/src/test/java/org/folio/rest/impl/protection/LinesProtectionTest.java @@ -2,25 +2,33 @@ import static io.vertx.core.json.Json.encodePrettily; import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.folio.invoices.utils.AcqDesiredPermissions.BYPASS_ACQ_UNITS; import static org.folio.invoices.utils.ErrorCodes.ACQ_UNITS_NOT_FOUND; import static org.folio.invoices.utils.ErrorCodes.GENERIC_ERROR_CODE; import static org.folio.invoices.utils.ErrorCodes.USER_HAS_NO_PERMISSIONS; +import static org.folio.invoices.utils.HelperUtils.INVOICE_ID; import static org.folio.rest.impl.InvoiceLinesApiTest.INVOICE_LINES_PATH; +import static org.folio.utils.UserPermissionsUtil.OKAPI_HEADER_PERMISSIONS; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.collection.IsCollectionWithSize.hasSize; import static org.hamcrest.core.IsEqual.equalTo; +import io.restassured.http.Header; +import io.vertx.core.json.JsonArray; import io.vertx.junit5.VertxExtension; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.folio.HttpStatus; import org.folio.rest.jaxrs.model.Errors; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import io.restassured.http.Headers; +import java.util.List; + @ExtendWith(VertxExtension.class) public class LinesProtectionTest extends ProtectedEntityTestBase { @@ -56,11 +64,11 @@ public void testOperationWithNonExistedUnits(ProtectedOperations operation) { }) public void testOperationWithAllowedUnits(ProtectedOperations operation) { logger.info( - "=== Invoice-lines protection: Test corresponding record has units allowed operation - expecting of call only to Units API ==="); + "=== Invoice-lines protection: Test corresponding record has units allowed operation - expecting of call only to Units API ==="); final Headers headers = prepareHeaders(X_OKAPI_TENANT, X_OKAPI_USER_ID); operation.process(INVOICE_LINES_PATH, encodePrettily(prepareInvoiceLine(NOT_PROTECTED_UNITS)), headers, - operation.getContentType(), operation.getCode()); + operation.getContentType(), operation.getCode()); validateNumberOfRequests(1, 0); } @@ -74,11 +82,11 @@ public void testOperationWithAllowedUnits(ProtectedOperations operation) { }) public void testWithRestrictedUnitsAndAllowedUser(ProtectedOperations operation) { logger.info( - "=== Invoice-lines protection: Test corresponding record has units, units protect operation, user is member of order's units - expecting of calls to Units, Memberships APIs and allowance of operation ==="); + "=== Invoice-lines protection: Test corresponding record has units, units protect operation, user is member of order's units - expecting of calls to Units, Memberships APIs and allowance of operation ==="); operation.process(INVOICE_LINES_PATH, encodePrettily(prepareInvoiceLine(PROTECTED_UNITS)), - prepareHeaders(X_OKAPI_TENANT, X_OKAPI_USER_WITH_UNITS_ASSIGNED_TO_RECORD), operation.getContentType(), - operation.getCode()); + prepareHeaders(X_OKAPI_TENANT, X_OKAPI_USER_WITH_UNITS_ASSIGNED_TO_RECORD), operation.getContentType(), + operation.getCode()); validateNumberOfRequests(1, 1); } @@ -111,13 +119,13 @@ public void testWithProtectedUnitsAndForbiddenUser(ProtectedOperations operation }) public void testOperationWithUnprocessableBadUnits(ProtectedOperations operation) { logger.info( - "=== Invoice-lines protection: Test corresponding record contains unprocessable bad units - expecting of call only to Units API ==="); + "=== Invoice-lines protection: Test corresponding record contains unprocessable bad units - expecting of call only to Units API ==="); final Headers headers = prepareHeaders(X_OKAPI_TENANT, X_OKAPI_USER_ID); Errors errors = operation .process(INVOICE_LINES_PATH, encodePrettily(prepareInvoiceLine(BAD_UNITS)), headers, APPLICATION_JSON, - HttpStatus.HTTP_BAD_REQUEST.toInt()) + HttpStatus.HTTP_BAD_REQUEST.toInt()) .as(Errors.class); assertThat(errors.getErrors(), hasSize(1)); @@ -127,4 +135,43 @@ public void testOperationWithUnprocessableBadUnits(ProtectedOperations operation // Verify number of sub-requests validateNumberOfRequests(0, 0); } + + @ParameterizedTest + @ValueSource(strings = { + "UPDATE", + "READ" + }) + void testBypassAcqUnitChecks(ProtectedOperations operation) { + Header permissionHeader = new Header(OKAPI_HEADER_PERMISSIONS, + new JsonArray(List.of(BYPASS_ACQ_UNITS.getPermission())).encode()); + Headers headers = new Headers(X_OKAPI_TENANT, permissionHeader, X_OKAPI_USER_WITH_UNITS_NOT_ASSIGNED_TO_RECORD); + operation.process(INVOICE_LINES_PATH, encodePrettily(prepareInvoiceLine(PROTECTED_UNITS)), + headers, operation.getContentType(), operation.getCode()); + + validateNumberOfRequests(0, 0); + } + + @Test + public void testBypassGetCollectionWithQuery() { + Header permissionHeader = new Header(OKAPI_HEADER_PERMISSIONS, + new JsonArray(List.of(BYPASS_ACQ_UNITS.getPermission())).encode()); + Headers headers = new Headers(X_OKAPI_TENANT, permissionHeader, X_OKAPI_USER_WITH_UNITS_NOT_ASSIGNED_TO_RECORD); + String cql = String.format("%s==%s", INVOICE_ID, APPROVED_INVOICE_ID); + String endpointQuery = String.format("%s?query=%s", INVOICE_LINES_PATH, cql); + + verifyGet(endpointQuery, headers, APPLICATION_JSON, 200); + + validateNumberOfRequests(0, 0); + } + + @Test + public void testBypassGetCollectionWithoutQuery() { + Header permissionHeader = new Header(OKAPI_HEADER_PERMISSIONS, + new JsonArray(List.of(BYPASS_ACQ_UNITS.getPermission())).encode()); + Headers headers = new Headers(X_OKAPI_TENANT, permissionHeader, X_OKAPI_USER_WITH_UNITS_NOT_ASSIGNED_TO_RECORD); + + verifyGet(INVOICE_LINES_PATH, headers, APPLICATION_JSON, 200); + + validateNumberOfRequests(0, 0); + } } diff --git a/src/test/java/org/folio/rest/impl/protection/ProtectedEntityTestBase.java b/src/test/java/org/folio/rest/impl/protection/ProtectedEntityTestBase.java index 52a386b70..b157f08a8 100644 --- a/src/test/java/org/folio/rest/impl/protection/ProtectedEntityTestBase.java +++ b/src/test/java/org/folio/rest/impl/protection/ProtectedEntityTestBase.java @@ -45,7 +45,7 @@ public abstract class ProtectedEntityTestBase extends ApiTestBase { private static final String USER_IS_MEMBER_OF_ACQ_UNITS = "6b4be232-5ad9-47a6-80b1-8c1acabd6212"; static final Header X_OKAPI_USER_WITH_UNITS_ASSIGNED_TO_RECORD = new Header(OKAPI_USERID_HEADER, USER_IS_MEMBER_OF_ACQ_UNITS); protected static final Header ALL_DESIRED_PERMISSIONS_HEADER = new Header(OKAPI_HEADER_PERMISSIONS, - new JsonArray(AcqDesiredPermissions.getValues()).encode()); + new JsonArray(AcqDesiredPermissions.getValuesExceptBypass()).encode()); static void validateNumberOfRequests(int numOfUnitRqs, int numOfMembershipRqs) { assertThat(MockServer.getAcqUnitsSearches(), getMatcher(numOfUnitRqs));