Skip to content

Commit

Permalink
[MODORDERS-1073] Added a permission to bypass acqunit checks (#481)
Browse files Browse the repository at this point in the history
  • Loading branch information
damien-git authored Mar 29, 2024
1 parent 6a4c059 commit 2bd3a4d
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 10 deletions.
23 changes: 22 additions & 1 deletion descriptors/ModuleDescriptor-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> values;
Expand All @@ -32,4 +33,8 @@ public String getPermission() {
public static List<String> getValues() {
return values;
}

public static List<String> getValuesExceptBypass() {
return values.stream().filter(v -> !BYPASS_ACQ_UNITS.getPermission().equals(v)).toList();
}
}
8 changes: 8 additions & 0 deletions src/main/java/org/folio/rest/impl/InvoiceHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -262,6 +264,9 @@ public Future<InvoiceCollection> getInvoices(int limit, int offset, String query
}

private Future<String> buildGetInvoicesQuery(String query) {
if (userHasDesiredPermission(BYPASS_ACQ_UNITS, okapiHeaders)) {
return succeededFuture(query);
}
return protectionHelper.buildAcqUnitsCqlExprToSearchRecords(INVOICES)
.map(acqUnitsCqlExpr -> {
if (isEmpty(query)) {
Expand Down Expand Up @@ -384,6 +389,9 @@ private List<InvoiceLine> filterUpdatedLines(List<InvoiceLine> invoiceLines, Lis
* acquisitions units
*/
private Future<Void> validateAcqUnitsOnUpdate(Invoice updatedInvoice, Invoice persistedInvoice) {
if (userHasDesiredPermission(BYPASS_ACQ_UNITS, okapiHeaders)) {
return Future.succeededFuture();
}
List<String> updatedAcqUnitIds = updatedInvoice.getAcqUnitIds();
List<String> currentAcqUnitIds = persistedInvoice.getAcqUnitIds();
verifyUserHasManagePermission(updatedAcqUnitIds, currentAcqUnitIds, okapiHeaders);
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/folio/rest/impl/InvoiceLineHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -86,6 +88,15 @@ public InvoiceLineHelper(Map<String, String> okapiHeaders, Context ctx) {
}

public Future<InvoiceLineCollection> 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;
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/org/folio/rest/impl/ProtectionHelper.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -56,6 +58,9 @@ public ProtectionHelper(Map<String, String> okapiHeaders, Context ctx) {
* exist; successfully otherwise
*/
public Future<Void> isOperationRestricted(List<String> 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()) {
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/folio/utils/UserPermissionsUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ public static void verifyUserHasAssignPermission(List<String> acqUnitIds, Map<St
}
}

public static boolean userHasDesiredPermission(AcqDesiredPermissions acqPerm, Map<String, String> okapiHeaders) {
return getProvidedPermissions(okapiHeaders).contains(acqPerm.getPermission());
}

public static boolean isUserDoesNotHaveDesiredPermission(AcqDesiredPermissions acqPerm, Map<String, String> okapiHeaders) {
return !getProvidedPermissions(okapiHeaders).contains(acqPerm.getPermission());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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));
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down

0 comments on commit 2bd3a4d

Please sign in to comment.