diff --git a/plugins/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java b/plugins/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java index ef2a4fd3..3cba9a0a 100644 --- a/plugins/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java +++ b/plugins/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java @@ -76,7 +76,8 @@ public boolean canAccess() { } @Override - public String postProcess(HttpResponse response) throws IOException { + public String postProcess(RequestDetailsReader request, HttpResponse response) + throws IOException { Preconditions.checkState(HttpUtil.isResponseValid(response)); String content = CharStreams.toString(HttpUtil.readerFromEntity(response.getEntity())); IParser parser = fhirContext.newJsonParser(); 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 f8b84b22..5f373f82 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 @@ -15,7 +15,12 @@ */ package com.google.fhir.gateway.plugin; +import static com.google.fhir.gateway.plugin.PermissionAccessChecker.Factory.PROXY_TO_ENV; + +import ca.uhn.fhir.context.FhirContext; +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.servlet.ServletRequestDetails; import com.google.common.annotations.VisibleForTesting; import com.google.fhir.gateway.ProxyConstants; @@ -36,7 +41,11 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.HttpResponse; +import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.util.TextUtils; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ListResource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -56,6 +65,10 @@ public class OpenSRPSyncAccessDecision implements AccessDecision { private final List organizationIds; private IgnoredResourcesConfig config; + private Gson gson = new Gson(); + + private FhirContext fhirR4Context = FhirContext.forR4(); + private IParser fhirR4JsonParser = fhirR4Context.newJsonParser(); public OpenSRPSyncAccessDecision( String applicationId, @@ -133,8 +146,58 @@ private void addSyncFilters( } @Override - public String postProcess(HttpResponse response) throws IOException { - return null; + public String postProcess(RequestDetailsReader request, HttpResponse response) + throws IOException { + + String resultContent = null; + String listMode = request.getHeader(Constants.FHIR_GATEWAY_MODE); + + switch (listMode) { + case Constants.LIST_ENTRIES: + resultContent = postProcessModeListEntries(response); + default: + break; + } + return resultContent; + } + + /** + * Generates a Bundle result from making a batch search request with the contained entries in the + * List as parameters + * + * @param response HTTPResponse + * @return String content of the result Bundle + */ + private String postProcessModeListEntries(HttpResponse response) throws IOException { + + String resultContent = null; + IBaseResource responseResource = + fhirR4JsonParser.parseResource((new BasicResponseHandler().handleResponse(response))); + + 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()) { + + bundleEntryComponent = new Bundle.BundleEntryComponent(); + bundleEntryComponent.setRequest( + new Bundle.BundleEntryRequestComponent() + .setMethod(Bundle.HTTPVerb.GET) + .setUrl(listEntryComponent.getItem().getReference())); + + requestBundle.addEntry(bundleEntryComponent); + } + + Bundle responseBundle = + createFhirClientForR4().transaction().withBundle(requestBundle).execute(); + + resultContent = fhirR4JsonParser.encodeResourceToString(responseBundle); + } + return resultContent; } /** @@ -214,7 +277,6 @@ private boolean isResourceTypeRequest(String requestPath) { protected IgnoredResourcesConfig getIgnoredResourcesConfigFileConfiguration(String configFile) { if (configFile != null && !configFile.isEmpty()) { try { - Gson gson = new Gson(); config = gson.fromJson(new FileReader(configFile), IgnoredResourcesConfig.class); if (config == null || config.entries == null) { throw new IllegalArgumentException("A map with a single `entries` array expected!"); @@ -286,6 +348,10 @@ private boolean shouldSkipDataFiltering(ServletRequestDetails servletRequestDeta return false; } + private IGenericClient createFhirClientForR4() { + return fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + } + @VisibleForTesting protected void setSkippedResourcesConfig(IgnoredResourcesConfig config) { this.config = config; @@ -308,4 +374,19 @@ public String toString() { + '}'; } } + + @VisibleForTesting + protected void setFhirR4Context(FhirContext fhirR4Context) { + this.fhirR4Context = fhirR4Context; + } + + @VisibleForTesting + protected void setFhirR4JsonParser(IParser fhirR4JsonParser) { + this.fhirR4JsonParser = fhirR4JsonParser; + } + + public static final class Constants { + public static final String FHIR_GATEWAY_MODE = "fhir-gateway-mode"; + public static final String LIST_ENTRIES = "list-entries"; + } } diff --git a/plugins/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java b/plugins/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java index e42498d0..0959f730 100644 --- a/plugins/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java +++ b/plugins/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java @@ -18,6 +18,7 @@ import ca.uhn.fhir.context.FhirContext; import com.google.common.io.Resources; import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; import java.io.IOException; import java.net.URL; import java.nio.charset.StandardCharsets; @@ -39,6 +40,9 @@ public class AccessGrantedAndUpdateListTest { @Mock(answer = Answers.RETURNS_DEEP_STUBS) private HttpResponse responseMock; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private RequestDetailsReader requestDetailsMock; + private static final FhirContext fhirContext = FhirContext.forR4(); private AccessGrantedAndUpdateList testInstance; @@ -55,7 +59,7 @@ public void postProcessNewPatientPut() throws IOException { testInstance = AccessGrantedAndUpdateList.forPatientResource( TEST_LIST_ID, httpFhirClientMock, fhirContext); - testInstance.postProcess(responseMock); + testInstance.postProcess(requestDetailsMock, responseMock); } @Test @@ -63,6 +67,6 @@ public void postProcessNewPatientPost() throws IOException { testInstance = AccessGrantedAndUpdateList.forPatientResource( TEST_LIST_ID, httpFhirClientMock, fhirContext); - testInstance.postProcess(responseMock); + testInstance.postProcess(requestDetailsMock, responseMock); } } 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 3e2ffc3f..1ef5da66 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 @@ -15,22 +15,39 @@ */ package com.google.fhir.gateway.plugin; +import static com.google.fhir.gateway.plugin.PermissionAccessChecker.Factory.PROXY_TO_ENV; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; + +import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ITransaction; +import ca.uhn.fhir.rest.gclient.ITransactionTyped; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.google.common.collect.Maps; import com.google.common.io.Resources; +import com.google.fhir.gateway.HttpFhirClient; import com.google.fhir.gateway.ProxyConstants; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; import java.io.IOException; import java.net.URL; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.hl7.fhir.r4.model.Bundle; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) @@ -44,6 +61,8 @@ public class OpenSRPSyncAccessDecisionTest { private OpenSRPSyncAccessDecision testInstance; + @Mock private HttpFhirClient httpFhirClientMock; + @Test public void preprocessShouldAddAllFiltersWhenIdsForLocationsOrganisationsAndCareTeamsAreProvided() throws IOException { @@ -300,6 +319,84 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso } } + @Test + public void testPostProcessWithListModeHeaderShouldFetchListEntriesBundle() 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); + + HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); + + TestUtil.setUpFhirResponseMock(fhirResponseMock, testListJson); + + 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); + } + + @Test + public void testPostProcessWithoutListModeHeaderShouldShouldReturnNull() throws IOException { + testInstance = createOpenSRPSyncAccessDecisionTestInstance(); + + RequestDetailsReader requestDetailsSpy = Mockito.mock(RequestDetailsReader.class); + Mockito.when(requestDetailsSpy.getHeader(OpenSRPSyncAccessDecision.Constants.FHIR_GATEWAY_MODE)) + .thenReturn(""); + + String resultContent = + testInstance.postProcess(requestDetailsSpy, Mockito.mock(HttpResponse.class)); + + // Verify no special Post-Processing happened + Assert.assertNull(resultContent); + } + private OpenSRPSyncAccessDecision createOpenSRPSyncAccessDecisionTestInstance() { OpenSRPSyncAccessDecision accessDecision = new OpenSRPSyncAccessDecision( diff --git a/plugins/src/test/resources/test_list_resource.json b/plugins/src/test/resources/test_list_resource.json new file mode 100644 index 00000000..6d384d29 --- /dev/null +++ b/plugins/src/test/resources/test_list_resource.json @@ -0,0 +1,35 @@ +{ + "resourceType": "List", + "id": "proxy-test-list-id", + "identifier": [ + { + "use": "official", + "value": "proxy-test-list-id" + } + ], + "status": "current", + "mode": "working", + "title": "Proxy Test List", + "code": { + "coding": [ + { + "system": "http://ona.io", + "code": "supply-chain", + "display": "Proxy Test List" + } + ], + "text": "My Proxy Test List" + }, + "entry": [ + { + "item": { + "reference": "Group/proxy-list-entry-id-1" + } + }, + { + "item": { + "reference": "Group/proxy-list-entry-id-2" + } + } + ] +} diff --git a/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java b/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java index e07f607e..bb5f869e 100755 --- a/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java +++ b/server/src/main/java/com/google/fhir/gateway/BearerAuthorizationInterceptor.java @@ -290,7 +290,7 @@ public boolean authorizeRequest(RequestDetails requestDetails) { if (HttpUtil.isResponseValid(response)) { try { // For post-processing rationale/example see b/207589782#comment3. - content = outcome.postProcess(response); + content = outcome.postProcess(new RequestDetailsToReader(requestDetails), response); } catch (Exception e) { // Note this is after a successful fetch/update of the FHIR store. That success must be // passed to the client even if the access related post-processing fails. diff --git a/server/src/main/java/com/google/fhir/gateway/CapabilityPostProcessor.java b/server/src/main/java/com/google/fhir/gateway/CapabilityPostProcessor.java index 718a66e9..1cefc1de 100644 --- a/server/src/main/java/com/google/fhir/gateway/CapabilityPostProcessor.java +++ b/server/src/main/java/com/google/fhir/gateway/CapabilityPostProcessor.java @@ -66,7 +66,8 @@ public boolean canAccess() { } @Override - public String postProcess(HttpResponse response) throws IOException { + public String postProcess(RequestDetailsReader requestDetails, HttpResponse response) + throws IOException { Preconditions.checkState(HttpUtil.isResponseValid(response)); String content = CharStreams.toString(HttpUtil.readerFromEntity(response.getEntity())); IParser parser = fhirContext.newJsonParser(); diff --git a/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java b/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java index a9c9e748..708a99ee 100644 --- a/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java +++ b/server/src/main/java/com/google/fhir/gateway/interfaces/AccessDecision.java @@ -56,5 +56,6 @@ public interface AccessDecision { * reads the response; otherwise null. Note that we should try to avoid reading the whole * content in memory whenever it is not needed for post-processing. */ - String postProcess(HttpResponse response) throws IOException; + String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response) + throws IOException; } diff --git a/server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java b/server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java index 135e1059..90e9f42a 100644 --- a/server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java +++ b/server/src/main/java/com/google/fhir/gateway/interfaces/NoOpAccessDecision.java @@ -37,7 +37,7 @@ public boolean canAccess() { } @Override - public String postProcess(HttpResponse response) { + public String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response) { return null; } diff --git a/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java b/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java index 8327a159..f6a4ea70 100644 --- a/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java +++ b/server/src/test/java/com/google/fhir/gateway/BearerAuthorizationInterceptorTest.java @@ -381,7 +381,9 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea return RequestMutation.builder().queryParams(paramMutations).build(); } - public String postProcess(HttpResponse response) throws IOException { + @Override + public String postProcess( + RequestDetailsReader requestDetailsReader, HttpResponse response) throws IOException { return null; } };