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);
}
}