diff --git a/exec/pom.xml b/exec/pom.xml index ca80ce3b..ee38ce5d 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -21,7 +21,7 @@ com.google.fhir.gateway fhir-gateway - 0.1.25 + 0.1.26 exec diff --git a/plugins/pom.xml b/plugins/pom.xml index 68819695..ac118be3 100755 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -23,7 +23,7 @@ implementations do not have to do this; they can redeclare those deps. --> com.google.fhir.gateway fhir-gateway - 0.1.25 + 0.1.26 plugins diff --git a/plugins/src/main/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecision.java b/plugins/src/main/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecision.java index f4705809..ee6ea2c0 100755 --- a/plugins/src/main/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecision.java +++ b/plugins/src/main/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecision.java @@ -21,7 +21,9 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import com.google.common.annotations.VisibleForTesting; +import com.google.fhir.gateway.ExceptionUtil; import com.google.fhir.gateway.ProxyConstants; import com.google.fhir.gateway.interfaces.AccessDecision; import com.google.fhir.gateway.interfaces.RequestDetailsReader; @@ -35,8 +37,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import javax.annotation.Nullable; import lombok.Getter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.HttpResponse; @@ -45,6 +50,9 @@ import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.r4.model.Bundle; import org.hl7.fhir.r4.model.ListResource; +import org.hl7.fhir.r4.model.OperationOutcome; +import org.hl7.fhir.r4.model.Resource; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -94,24 +102,24 @@ public boolean canAccess() { public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) { RequestMutation requestMutation = null; - - // TODO: Disable access for a user who adds tags to organisations, locations or care teams that - // they do not have access to - // This does not bar access to anyone who uses their own sync tags to circumvent - // the filter. The aim of this feature based on scoping was to pre-filter the data for the user if (isSyncUrl(requestDetailsReader)) { - // This prevents access to a user who has no location/organisation/team assigned to them - if (locationIds.size() == 0 && careTeamIds.size() == 0 && organizationIds.size() == 0) { - locationIds.add( - "CR1bAeGgaYqIpsNkG0iidfE5WVb5BJV1yltmL4YFp3o6mxj3iJPhKh4k9ROhlyZveFC8298lYzft8SIy8yMNLl5GVWQXNRr1sSeBkP2McfFZjbMYyrxlNFOJgqvtccDKKYSwBiLHq2By5tRupHcmpIIghV7Hp39KgF4iBDNqIGMKhgOIieQwt5BRih5FgnwdHrdlK9ix"); + if (locationIds.isEmpty() && careTeamIds.isEmpty() && organizationIds.isEmpty()) { + + ForbiddenOperationException forbiddenOperationException = + new ForbiddenOperationException( + "User un-authorized to " + + requestDetailsReader.getRequestType() + + " /" + + requestDetailsReader.getRequestPath() + + ". User assignment or sync strategy not configured correctly"); + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, forbiddenOperationException.getMessage(), forbiddenOperationException); } // Skip app-wide global resource requests if (!shouldSkipDataFiltering(requestDetailsReader)) { - List syncFilterParameterValues = - addSyncFilters( - requestDetailsReader, getSyncTags(locationIds, careTeamIds, organizationIds)); + addSyncFilters(getSyncTags(locationIds, careTeamIds, organizationIds)); requestMutation = RequestMutation.builder() .queryParams(Map.of(ProxyConstants.TAG_SEARCH_PARAM, syncFilterParameterValues)) @@ -126,12 +134,10 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea * Adds filters to the {@link RequestDetailsReader} for the _tag property to allow filtering by * specific code-url-values that match specific locations, teams or organisations * - * @param requestDetailsReader * @param syncTags * @return the extra query Parameter values */ - private List addSyncFilters( - RequestDetailsReader requestDetailsReader, Pair> syncTags) { + private List addSyncFilters(Pair> syncTags) { List paramValues = new ArrayList<>(); Collections.addAll( paramValues, @@ -139,68 +145,126 @@ private List addSyncFilters( .getKey() .substring(LENGTH_OF_SEARCH_PARAM_AND_EQUALS) .split(ProxyConstants.PARAM_VALUES_SEPARATOR)); - - String[] prevTagFilters = - requestDetailsReader.getParameters().get(ProxyConstants.TAG_SEARCH_PARAM); - if (prevTagFilters != null && prevTagFilters.length > 0) { - Collections.addAll(paramValues, prevTagFilters); - } - return paramValues; } + /** NOTE: Always return a null whenever you want to skip post-processing */ @Override public String postProcess(RequestDetailsReader request, HttpResponse response) throws IOException { String resultContent = null; - String listMode = request.getHeader(Constants.FHIR_GATEWAY_MODE); + Resource resultContentBundle; + String gatewayMode = request.getHeader(Constants.FHIR_GATEWAY_MODE); + + if (StringUtils.isNotBlank(gatewayMode)) { + + resultContent = new BasicResponseHandler().handleResponse(response); + IBaseResource responseResource = fhirR4JsonParser.parseResource(resultContent); + + switch (gatewayMode) { + case Constants.LIST_ENTRIES: + resultContentBundle = postProcessModeListEntries(responseResource); + break; - switch (listMode) { - case Constants.LIST_ENTRIES: - resultContent = postProcessModeListEntries(response); - default: - break; + default: + String exceptionMessage = + "The FHIR Gateway Mode header is configured with an un-recognized value of \'" + + gatewayMode + + '\''; + OperationOutcome operationOutcome = createOperationOutcome(exceptionMessage); + + resultContentBundle = operationOutcome; + } + + if (resultContentBundle != null) + resultContent = fhirR4JsonParser.encodeResourceToString(resultContentBundle); } + return resultContent; } + @NotNull + private static OperationOutcome createOperationOutcome(String exception) { + OperationOutcome operationOutcome = new OperationOutcome(); + OperationOutcome.OperationOutcomeIssueComponent operationOutcomeIssueComponent = + new OperationOutcome.OperationOutcomeIssueComponent(); + operationOutcomeIssueComponent.setSeverity(OperationOutcome.IssueSeverity.ERROR); + operationOutcomeIssueComponent.setCode(OperationOutcome.IssueType.PROCESSING); + operationOutcomeIssueComponent.setDiagnostics(exception); + operationOutcome.setIssue(Arrays.asList(operationOutcomeIssueComponent)); + return operationOutcome; + } + + @NotNull + private static Bundle processListEntriesGatewayModeByListResource( + ListResource responseListResource) { + Bundle requestBundle = new Bundle(); + requestBundle.setType(Bundle.BundleType.BATCH); + + for (ListResource.ListEntryComponent listEntryComponent : responseListResource.getEntry()) { + requestBundle.addEntry( + createBundleEntryComponent( + Bundle.HTTPVerb.GET, listEntryComponent.getItem().getReference(), null)); + } + return requestBundle; + } + + private Bundle processListEntriesGatewayModeByBundle(IBaseResource responseResource) { + Bundle requestBundle = new Bundle(); + requestBundle.setType(Bundle.BundleType.BATCH); + + List bundleEntryComponentList = + ((Bundle) responseResource) + .getEntry().stream() + .filter(it -> it.getResource() instanceof ListResource) + .flatMap( + bundleEntryComponent -> + ((ListResource) bundleEntryComponent.getResource()).getEntry().stream()) + .map( + listEntryComponent -> + createBundleEntryComponent( + Bundle.HTTPVerb.GET, listEntryComponent.getItem().getReference(), null)) + .collect(Collectors.toList()); + + return requestBundle.setEntry(bundleEntryComponentList); + } + + @NotNull + private static Bundle.BundleEntryComponent createBundleEntryComponent( + Bundle.HTTPVerb method, String requestPath, @Nullable String condition) { + + Bundle.BundleEntryComponent bundleEntryComponent = new Bundle.BundleEntryComponent(); + bundleEntryComponent.setRequest( + new Bundle.BundleEntryRequestComponent() + .setMethod(method) + .setUrl(requestPath) + .setIfMatch(condition)); + + return bundleEntryComponent; + } + /** * Generates a Bundle result from making a batch search request with the contained entries in the * List as parameters * - * @param response HTTPResponse + * @param responseResource FHIR Resource result returned byt the HTTPResponse * @return String content of the result Bundle */ - private String postProcessModeListEntries(HttpResponse response) throws IOException { + private Bundle postProcessModeListEntries(IBaseResource responseResource) { - String resultContent = new BasicResponseHandler().handleResponse(response); - IBaseResource responseResource = fhirR4JsonParser.parseResource(resultContent); + Bundle requestBundle = null; if (responseResource instanceof ListResource && ((ListResource) responseResource).hasEntry()) { - Bundle requestBundle = new Bundle(); - requestBundle.setType(Bundle.BundleType.BATCH); - Bundle.BundleEntryComponent bundleEntryComponent; - - for (ListResource.ListEntryComponent listEntryComponent : - ((ListResource) responseResource).getEntry()) { + requestBundle = processListEntriesGatewayModeByListResource((ListResource) responseResource); - bundleEntryComponent = new Bundle.BundleEntryComponent(); - bundleEntryComponent.setRequest( - new Bundle.BundleEntryRequestComponent() - .setMethod(Bundle.HTTPVerb.GET) - .setUrl(listEntryComponent.getItem().getReference())); - - requestBundle.addEntry(bundleEntryComponent); - } + } else if (responseResource instanceof Bundle) { - Bundle responseBundle = - createFhirClientForR4().transaction().withBundle(requestBundle).execute(); - - resultContent = fhirR4JsonParser.encodeResourceToString(responseBundle); + requestBundle = processListEntriesGatewayModeByBundle(responseResource); } - return resultContent; + + return createFhirClientForR4().transaction().withBundle(requestBundle).execute(); } /** diff --git a/plugins/src/test/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecisionTest.java b/plugins/src/test/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecisionTest.java index 074abad2..9f7a6b0c 100755 --- a/plugins/src/test/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecisionTest.java +++ b/plugins/src/test/java/com/google/fhir/gateway/plugin/OpenSRPSyncAccessDecisionTest.java @@ -42,6 +42,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpResponse; import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ListResource; +import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,8 +64,11 @@ public class OpenSRPSyncAccessDecisionTest { private OpenSRPSyncAccessDecision testInstance; @Test - public void preprocessShouldAddAllFiltersWhenIdsForLocationsOrganisationsAndCareTeamsAreProvided() - throws IOException { + public void + preprocessShouldAddAllFiltersWhenIdsForLocationsOrganisationsAndCareTeamsAreProvided() { + locationIds.addAll(Arrays.asList("my-location-id", "my-location-id2")); + careTeamIds.add("my-careteam-id"); + organisationIds.add("my-organization-id"); testInstance = createOpenSRPSyncAccessDecisionTestInstance(); @@ -266,7 +271,7 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso for (String locationId : organisationIds) { Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - Assert.assertTrue(requestDetails.getParameters().size() == 0); + Assert.assertNull(mutatedRequest); } } @@ -298,7 +303,7 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso RequestMutation mutatedRequest = testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); - Assert.assertNull(requestDetails.getParameters().get(ProxyConstants.TAG_SEARCH_PARAM)); + Assert.assertNull(mutatedRequest); } @Test @@ -339,6 +344,22 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso } } + @Test(expected = RuntimeException.class) + public void preprocessShouldThrowRuntimeExceptionWhenNoSyncStrategyFilterIsProvided() { + testInstance = createOpenSRPSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Patient"); + requestDetails.setRequestPath("Patient"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); + + // Call the method under testing + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + } + @Test public void testPostProcessWithListModeHeaderShouldFetchListEntriesBundle() throws IOException { locationIds.add("Location-1"); @@ -417,6 +438,88 @@ public void testPostProcessWithoutListModeHeaderShouldShouldReturnNull() throws Assert.assertNull(resultContent); } + @Test + public void testPostProcessWithListModeHeaderSearchByTagShouldFetchListEntriesBundle() + throws IOException { + locationIds.add("Location-1"); + testInstance = Mockito.spy(createOpenSRPSyncAccessDecisionTestInstance()); + + FhirContext fhirR4Context = mock(FhirContext.class); + IGenericClient iGenericClient = mock(IGenericClient.class); + ITransaction iTransaction = mock(ITransaction.class); + ITransactionTyped iClientExecutable = mock(ITransactionTyped.class); + + Mockito.when(fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV))) + .thenReturn(iGenericClient); + Mockito.when(iGenericClient.transaction()).thenReturn(iTransaction); + Mockito.when(iTransaction.withBundle(any(Bundle.class))).thenReturn(iClientExecutable); + + Bundle resultBundle = new Bundle(); + resultBundle.setType(Bundle.BundleType.BATCHRESPONSE); + resultBundle.setId("bundle-result-id"); + + Mockito.when(iClientExecutable.execute()).thenReturn(resultBundle); + + ArgumentCaptor bundleArgumentCaptor = ArgumentCaptor.forClass(Bundle.class); + + testInstance.setFhirR4Context(fhirR4Context); + + RequestDetailsReader requestDetailsSpy = Mockito.mock(RequestDetailsReader.class); + + Mockito.when(requestDetailsSpy.getHeader(OpenSRPSyncAccessDecision.Constants.FHIR_GATEWAY_MODE)) + .thenReturn(OpenSRPSyncAccessDecision.Constants.LIST_ENTRIES); + + URL listUrl = Resources.getResource("test_list_resource.json"); + String testListJson = Resources.toString(listUrl, StandardCharsets.UTF_8); + + FhirContext realFhirContext = FhirContext.forR4(); + ListResource listResource = + (ListResource) realFhirContext.newJsonParser().parseResource(testListJson); + + Bundle bundle = new Bundle(); + Bundle.BundleEntryComponent bundleEntryComponent = new Bundle.BundleEntryComponent(); + bundleEntryComponent.setResource(listResource); + bundle.setType(Bundle.BundleType.BATCHRESPONSE); + bundle.setEntry(Arrays.asList(bundleEntryComponent)); + + HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); + + TestUtil.setUpFhirResponseMock( + fhirResponseMock, realFhirContext.newJsonParser().encodeResourceToString(bundle)); + + String resultContent = testInstance.postProcess(requestDetailsSpy, fhirResponseMock); + + Mockito.verify(iTransaction).withBundle(bundleArgumentCaptor.capture()); + Bundle requestBundle = bundleArgumentCaptor.getValue(); + + // Verify modified request to the server + Assert.assertNotNull(requestBundle); + Assert.assertEquals(Bundle.BundleType.BATCH, requestBundle.getType()); + List requestBundleEntries = requestBundle.getEntry(); + Assert.assertEquals(2, requestBundleEntries.size()); + + Assert.assertEquals(Bundle.HTTPVerb.GET, requestBundleEntries.get(0).getRequest().getMethod()); + Assert.assertEquals( + "Group/proxy-list-entry-id-1", requestBundleEntries.get(0).getRequest().getUrl()); + + Assert.assertEquals(Bundle.HTTPVerb.GET, requestBundleEntries.get(1).getRequest().getMethod()); + Assert.assertEquals( + "Group/proxy-list-entry-id-2", requestBundleEntries.get(1).getRequest().getUrl()); + + // Verify returned result content from the server request + Assert.assertNotNull(resultContent); + Assert.assertEquals( + "{\"resourceType\":\"Bundle\",\"id\":\"bundle-result-id\",\"type\":\"batch-response\"}", + resultContent); + } + + @After + public void cleanUp() { + locationIds.clear(); + careTeamIds.clear(); + organisationIds.clear(); + } + private OpenSRPSyncAccessDecision createOpenSRPSyncAccessDecisionTestInstance() { OpenSRPSyncAccessDecision accessDecision = new OpenSRPSyncAccessDecision( diff --git a/pom.xml b/pom.xml index 1637affd..ee2d4993 100755 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ com.google.fhir.gateway fhir-gateway - 0.1.25 + 0.1.26 pom FHIR Information Gateway diff --git a/server/pom.xml b/server/pom.xml index b7ca1c80..9c9dcd77 100755 --- a/server/pom.xml +++ b/server/pom.xml @@ -21,7 +21,7 @@ com.google.fhir.gateway fhir-gateway - 0.1.25 + 0.1.26 server diff --git a/server/src/main/java/com/google/fhir/gateway/ExceptionUtil.java b/server/src/main/java/com/google/fhir/gateway/ExceptionUtil.java index 90778d97..5edb826e 100644 --- a/server/src/main/java/com/google/fhir/gateway/ExceptionUtil.java +++ b/server/src/main/java/com/google/fhir/gateway/ExceptionUtil.java @@ -53,7 +53,7 @@ static void throwRuntimeExceptionAndLog(Logger logger, String errorMessage) { throwRuntimeExceptionAndLog(logger, errorMessage, null, RuntimeException.class); } - static void throwRuntimeExceptionAndLog(Logger logger, String errorMessage, Exception e) { + public static void throwRuntimeExceptionAndLog(Logger logger, String errorMessage, Exception e) { throwRuntimeExceptionAndLog(logger, errorMessage, e, RuntimeException.class); } }