Skip to content

Commit

Permalink
Merge pull request #108 from onaio/cherry-picked-enhancements
Browse files Browse the repository at this point in the history
FHIR Gateway Extensions enhancements
pld authored Jan 22, 2025
2 parents 11173d1 + 5bac55f commit ad55c04
Showing 8 changed files with 288 additions and 77 deletions.
4 changes: 2 additions & 2 deletions exec/pom.xml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
<parent>
<groupId>org.smartregister</groupId>
<artifactId>opensrp-gateway-plugin</artifactId>
<version>2.2.3</version>
<version>2.2.4</version>
</parent>

<artifactId>exec</artifactId>
@@ -70,7 +70,7 @@
<dependency>
<groupId>org.smartregister</groupId>
<artifactId>plugins</artifactId>
<version>2.2.3</version>
<version>2.2.4</version>
</dependency>

<dependency>
2 changes: 1 addition & 1 deletion plugins/pom.xml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
<parent>
<groupId>org.smartregister</groupId>
<artifactId>opensrp-gateway-plugin</artifactId>
<version>2.2.3</version>
<version>2.2.4</version>
</parent>

<artifactId>plugins</artifactId>
Original file line number Diff line number Diff line change
@@ -181,6 +181,7 @@ public List<Location> getDescendants(
allLocations.add(parentLocation);
}
if (childLocationBundle != null) {
Utils.fetchAllBundlePagesAndInject(r4FHIRClient, childLocationBundle);
childLocationBundle.getEntry().parallelStream()
.forEach(
childLocation -> {
@@ -193,24 +194,6 @@ public List<Location> getDescendants(
null,
adminLevels));
});

while (childLocationBundle.getLink(Bundle.LINK_NEXT) != null) {
childLocationBundle =
getFhirClientForR4().loadPage().next(childLocationBundle).execute();

childLocationBundle.getEntry().parallelStream()
.forEach(
childLocation -> {
Location childLocationEntity =
(Location) childLocation.getResource();
allLocations.add(childLocationEntity);
allLocations.addAll(
getDescendants(
childLocationEntity.getIdElement().getIdPart(),
null,
adminLevels));
});
}
}

return allLocations;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.smartregister.fhir.gateway.plugins;

import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -11,7 +12,6 @@
import javax.inject.Named;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CareTeam;
@@ -103,20 +103,22 @@ public AccessDecision checkAccess(RequestDetailsReader requestDetails) {
private void initSyncAccessDecision(RequestDetailsReader requestDetailsReader) {
Map<String, List<String>> syncStrategyIds;

Composition composition = fetchComposition();
String syncStrategy = readSyncStrategyFromComposition(composition);

if (CacheHelper.INSTANCE.skipCache()) {
syncStrategyIds =
getSyncStrategyIds(
jwt.getSubject(), applicationId, fhirContext, requestDetailsReader);
getSyncStrategyIds(jwt.getSubject(), syncStrategy, requestDetailsReader);
} else {
syncStrategyIds =
CacheHelper.INSTANCE.cache.get(
jwt.getSubject(),
userId ->
generateSyncStrategyIdsCacheKey(
jwt.getSubject(),
syncStrategy,
requestDetailsReader.getParameters()),
key ->
getSyncStrategyIds(
userId,
applicationId,
fhirContext,
requestDetailsReader));
jwt.getSubject(), syncStrategy, requestDetailsReader));
}

this.syncAccessDecision =
@@ -129,6 +131,38 @@ private void initSyncAccessDecision(RequestDetailsReader requestDetailsReader) {
userRoles);
}

@VisibleForTesting
protected static String generateSyncStrategyIdsCacheKey(
String userId, String syncStrategy, Map<String, String[]> parameters) {

String key = null;
switch (syncStrategy) {
case Constants.SyncStrategy.RELATED_ENTITY_LOCATION:
try {

String[] syncLocations =
parameters.getOrDefault(
Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {});

if (syncLocations.length == 0) {
key = userId;
} else {
key = Utils.generateHash(Utils.getSortedInput(syncLocations[0], ","));
}

} catch (NoSuchAlgorithmException exception) {
logger.error(exception.getMessage());
}

break;

default:
key = userId;
}

return key;
}

private boolean checkUserHasRole(String resourceName, String requestType) {
return StringUtils.isNotBlank(resourceName)
&& (checkIfRoleExists(getAdminRoleName(resourceName), this.userRoles)
@@ -216,8 +250,7 @@ private Composition readCompositionResource(String applicationId, FhirContext fh
return compositionEntry != null ? (Composition) compositionEntry.getResource() : null;
}

Pair<Composition, PractitionerDetails> fetchCompositionAndPractitionerDetails(
String subject, String applicationId, FhirContext fhirContext) {
PractitionerDetails fetchPractitionerDetails(String subject) {
fhirContext.registerCustomType(PractitionerDetails.class);

IGenericClient client = Utils.createFhirClientForR4(fhirContext);
@@ -227,53 +260,43 @@ Pair<Composition, PractitionerDetails> fetchCompositionAndPractitionerDetails(
PractitionerDetails practitionerDetails =
practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(subject);

Composition composition = readCompositionResource(applicationId, fhirContext);

if (composition == null)
throw new IllegalStateException(
"No Composition resource found for application id '" + applicationId + "'");

if (practitionerDetails == null)
throw new IllegalStateException(
"No PractitionerDetail resource found for user with id '" + subject + "'");

return Pair.of(composition, practitionerDetails);
return practitionerDetails;
}

Pair<String, PractitionerDetails> fetchSyncStrategyDetails(
String subject, String applicationId, FhirContext fhirContext) {
private Composition fetchComposition() {
Composition composition = readCompositionResource(applicationId, fhirContext);
if (composition == null)
throw new IllegalStateException(
"No Composition resource found for application id '" + applicationId + "'");

Pair<Composition, PractitionerDetails> compositionPractitionerDetailsPair =
fetchCompositionAndPractitionerDetails(subject, applicationId, fhirContext);
Composition composition = compositionPractitionerDetailsPair.getLeft();
PractitionerDetails practitionerDetails = compositionPractitionerDetailsPair.getRight();
return composition;
}

private String readSyncStrategyFromComposition(Composition composition) {
String binaryResourceReference = Utils.getBinaryResourceReference(composition);
Binary binary =
Utils.readApplicationConfigBinaryResource(binaryResourceReference, fhirContext);

return Pair.of(Utils.findSyncStrategy(binary), practitionerDetails);
return Utils.findSyncStrategy(binary);
}

private Map<String, List<String>> getSyncStrategyIds(
String subjectId,
String applicationId,
FhirContext fhirContext,
RequestDetailsReader requestDetailsReader) {
Pair<String, PractitionerDetails> syncStrategyDetails =
fetchSyncStrategyDetails(subjectId, applicationId, fhirContext);
String subjectId, String syncStrategy, RequestDetailsReader requestDetailsReader) {

String syncStrategy = syncStrategyDetails.getLeft();
PractitionerDetails practitionerDetails = syncStrategyDetails.getRight();
PractitionerDetails practitionerDetails = fetchPractitionerDetails(subjectId);

return collateSyncStrategyIds(syncStrategy, practitionerDetails, requestDetailsReader);
}

private List<String> getLocationUuids(String[] syncLocations) {
List<String> locationUuids = new ArrayList<>();
String syncLocationParam;
for (int i = 0; i < syncLocations.length; i++) {
syncLocationParam = syncLocations[i];

for (String syncLocation : syncLocations) {
syncLocationParam = syncLocation;
if (!syncLocationParam.isEmpty())
locationUuids.addAll(
Set.of(syncLocationParam.split(Constants.PARAM_VALUES_SEPARATOR)));
@@ -353,32 +376,17 @@ private Map<String, List<String>> collateSyncStrategyIds(
&& practitionerDetails.getFhirPractitionerDetails()
!= null
? PractitionerDetailsEndpointHelper.getAttributedLocations(
PractitionerDetailsEndpointHelper.getLocationsHierarchy(
practitionerDetails
.getFhirPractitionerDetails()
.getLocations()
.stream()
.map(
location ->
location.getIdElement()
.getIdPart())
.collect(Collectors.toList())))
practitionerDetails
.getFhirPractitionerDetails()
.getLocationHierarchyList())
: new HashSet<>();
}

} else
throw new IllegalStateException(
"'" + syncStrategy + "' sync strategy NOT supported!!");

resultMap =
!syncStrategyIds.isEmpty()
? Map.of(syncStrategy, new ArrayList<>(syncStrategyIds))
: null;

if (resultMap == null) {
throw new IllegalStateException(
"No Sync strategy ids found for selected sync strategy " + syncStrategy);
}
resultMap = Map.of(syncStrategy, new ArrayList<>(syncStrategyIds));

} else
throw new IllegalStateException(
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
package org.smartregister.fhir.gateway.plugins;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.codec.binary.Hex;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UriType;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.impl.GenericClient;

public class Utils {

@@ -193,4 +203,84 @@ public static String findSyncStrategy(byte[] binaryDataBytes) {

return syncStrategy;
}

public static String generateHash(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes());
return Hex.encodeHexString(hashBytes);
}

public static String getSortedInput(String input, String separator) {
return getSortedInput(Arrays.stream(input.split(separator)), separator);
}

public static String getSortedInput(Stream<String> inputStream, String separator) {
return inputStream.sorted(Comparator.naturalOrder()).collect(Collectors.joining(separator));
}

/**
* This is a recursive function which updates the result bundle with results of all pages
* whenever there's an entry for Bundle.LINK_NEXT
*
* @param fhirClient the Generic FHIR Client instance
* @param resultBundle the result bundle from the first request
*/
public static void fetchAllBundlePagesAndInject(
IGenericClient fhirClient, Bundle resultBundle) {

if (resultBundle.getLink(Bundle.LINK_NEXT) != null) {

cleanUpBundlePaginationNextLinkServerBaseUrl((GenericClient) fhirClient, resultBundle);

Bundle pageResultBundle = fhirClient.loadPage().next(resultBundle).execute();

resultBundle.getEntry().addAll(pageResultBundle.getEntry());
resultBundle.setLink(pageResultBundle.getLink());

fetchAllBundlePagesAndInject(fhirClient, resultBundle);
}

resultBundle.setLink(
resultBundle.getLink().stream()
.filter(
bundleLinkComponent ->
!Bundle.LINK_NEXT.equals(bundleLinkComponent.getRelation()))
.collect(Collectors.toList()));
resultBundle.getMeta().setLastUpdated(resultBundle.getMeta().getLastUpdated());
}

public static void cleanUpBundlePaginationNextLinkServerBaseUrl(
GenericClient fhirClient, Bundle resultBundle) {
String cleanUrl =
cleanHapiPaginationLinkBaseUrl(
resultBundle.getLink(Bundle.LINK_NEXT).getUrl(), fhirClient.getUrlBase());
resultBundle
.getLink()
.replaceAll(
bundleLinkComponent ->
Bundle.LINK_NEXT.equals(bundleLinkComponent.getRelation())
? new Bundle.BundleLinkComponent(
new StringType(Bundle.LINK_NEXT),
new UriType(cleanUrl))
: bundleLinkComponent);
}

public static String cleanBaseUrl(String originalUrl, String fhirServerBaseUrl) {
int hostStartIndex = originalUrl.indexOf("://") + 3;
int pathStartIndex = originalUrl.indexOf("/", hostStartIndex);

// If the URL has no path, assume it ends right after the host
if (pathStartIndex == -1) {
pathStartIndex = originalUrl.length();
}

return fhirServerBaseUrl + originalUrl.substring(pathStartIndex);
}

public static String cleanHapiPaginationLinkBaseUrl(
String originalUrl, String fhirServerBaseUrl) {
return originalUrl.indexOf('?') > -1
? fhirServerBaseUrl + originalUrl.substring(originalUrl.indexOf('?'))
: fhirServerBaseUrl;
}
}
Original file line number Diff line number Diff line change
@@ -451,4 +451,16 @@ public void testAccessDeniedWhenSingleRoleMissingForTypeBundleResources() throws

assertThat(canAccess, equalTo(false));
}

@Test
public void testGenerateSyncStrategyIdsCacheKey() {
String testUserId = "my-test-user-id";
Map<String, String[]> strategyIdMap =
Map.of(Constants.SyncStrategy.CARE_TEAM, new String[] {"id-1, id-2,id-3"});
String cacheKey =
PermissionAccessChecker.generateSyncStrategyIdsCacheKey(
testUserId, Constants.SyncStrategy.CARE_TEAM, strategyIdMap);

Assert.assertEquals(testUserId, cacheKey);
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
package org.smartregister.fhir.gateway.plugins;

import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Base64BinaryType;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Reference;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.MockedStatic;
import org.mockito.Mockito;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.impl.GenericClient;
import ca.uhn.fhir.rest.gclient.ICriterion;
import ca.uhn.fhir.rest.gclient.IGetPage;
import ca.uhn.fhir.rest.gclient.IGetPageTyped;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.gclient.IUntypedQuery;

public class UtilsTest {

private FhirContext fhirContextMock;
private GenericClient genericClientMock;

@Before
public void setUp() {
fhirContextMock = Mockito.mock(FhirContext.class);
IGenericClient clientMock = Mockito.mock(IGenericClient.class);
genericClientMock = Mockito.mock(GenericClient.class);

Mockito.when(fhirContextMock.newRestfulGenericClient(Mockito.anyString()))
.thenReturn(clientMock);
@@ -230,4 +242,110 @@ public void testReadApplicationConfigBinaryResourceReturnsBinary() {

Assert.assertEquals("{\"appId\":\"test-app\",\"appTitle\":\"Test App\"}", decodedJson);
}

@Test
public void testGenerateHashConsistency() throws NoSuchAlgorithmException {
String input = "consistentTest";
String hash1 = Utils.generateHash(input);
String hash2 = Utils.generateHash(input);
Assert.assertEquals(hash1, hash2);
}

@Test
public void testGenerateHashDifferentInputs() throws NoSuchAlgorithmException {
String input1 = "inputOne";
String input2 = "inputTwo";
String hash1 = Utils.generateHash(input1);
String hash2 = Utils.generateHash(input2);
Assert.assertNotEquals(hash1, hash2);
}

@Test
public void testFetchAllBundlePagesAndInject() {
Bundle firstPageBundle = new Bundle();
firstPageBundle.setMeta(new Meta().setLastUpdated(new Date()));
firstPageBundle.addLink().setRelation(Bundle.LINK_NEXT).setUrl("nextPageUrl");

Bundle secondPageBundle = new Bundle();
secondPageBundle.setMeta(new Meta().setLastUpdated(new Date()));
secondPageBundle.addEntry(new Bundle.BundleEntryComponent());

IGetPage loadPageMock = Mockito.mock(IGetPage.class);
IGetPageTyped iGetPageTypedMock = Mockito.mock(IGetPageTyped.class);
Mockito.doReturn(loadPageMock).when(genericClientMock).loadPage();
Mockito.doReturn(iGetPageTypedMock).when(loadPageMock).next(firstPageBundle);
Mockito.doReturn(secondPageBundle).when(iGetPageTypedMock).execute();
Utils.fetchAllBundlePagesAndInject(genericClientMock, firstPageBundle);

Assert.assertEquals(1, firstPageBundle.getEntry().size());
Assert.assertNull(firstPageBundle.getLink(Bundle.LINK_NEXT));
Assert.assertNotNull(firstPageBundle.getMeta().getLastUpdated());
Mockito.verify(genericClientMock.loadPage(), Mockito.times(1)).next(firstPageBundle);
}

@Test
public void testCleanUpBundleLinksServerBaseUrlMultiplePaginationNextLink() {
Bundle resultBundle = new Bundle();
resultBundle
.addLink()
.setRelation(Bundle.LINK_NEXT)
.setUrl(
"http://old-base-url:8080/fhir?_getpages=c380a770-4ecc-45fa-b9c4-003c5b37e1f4&_getpagesoffset=2&_count=1&_pretty=true&_bundletype=searchset");

Mockito.when(genericClientMock.getUrlBase()).thenReturn("http://new-base-url");

Utils.cleanUpBundlePaginationNextLinkServerBaseUrl(genericClientMock, resultBundle);

List<Bundle.BundleLinkComponent> links = resultBundle.getLink();

Bundle.BundleLinkComponent nextLink =
links.stream()
.filter(link -> Bundle.LINK_NEXT.equals(link.getRelation()))
.findFirst()
.orElse(null);
Assert.assertNotNull(nextLink);
Assert.assertEquals(
"http://new-base-url?_getpages=c380a770-4ecc-45fa-b9c4-003c5b37e1f4&_getpagesoffset=2&_count=1&_pretty=true&_bundletype=searchset",
nextLink.getUrl());
}

@Test
public void testGenericCleanBaseUrl() {
String cleanHostUrl =
Utils.cleanBaseUrl(
"http://old-base-url/nextPage?param=value", "http://new-base-url");
Assert.assertEquals("http://new-base-url/nextPage?param=value", cleanHostUrl);
}

@Test
public void testGenerateSyncStrategyIdsCacheKeyWithSyncLocations() {
String userId = "user123";
String syncStrategy = Constants.SyncStrategy.RELATED_ENTITY_LOCATION;
Map<String, String[]> parameters = new HashMap<>();
parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"location1"});

MockedStatic<Utils> mockUtils = Mockito.mockStatic(Utils.class);
mockUtils.when(() -> Utils.generateHash("location1")).thenReturn("hashedLocation1");
mockUtils.when(() -> Utils.getSortedInput("location1", ",")).thenReturn("location1");

String result =
PermissionAccessChecker.generateSyncStrategyIdsCacheKey(
userId, syncStrategy, parameters);
Assert.assertEquals("hashedLocation1", result);
mockUtils.close();
}

@Test
public void testGenerateSyncStrategyIdsCacheKeyDefaultStrategy() {
String userId = "user123";
String syncStrategy = "someOtherStrategy";
Map<String, String[]> parameters = new HashMap<>();
parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"location1"});

String result =
PermissionAccessChecker.generateSyncStrategyIdsCacheKey(
userId, syncStrategy, parameters);

Assert.assertEquals(userId, result);
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@

<groupId>org.smartregister</groupId>
<artifactId>opensrp-gateway-plugin</artifactId>
<version>2.2.3</version>
<version>2.2.4</version>
<packaging>pom</packaging>

<modules>

0 comments on commit ad55c04

Please sign in to comment.