diff --git a/exec/pom.xml b/exec/pom.xml index d4cfed8..8141e2c 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -4,7 +4,7 @@ org.smartregister opensrp-gateway-plugin - 2.2.3 + 2.2.4 exec @@ -70,7 +70,7 @@ org.smartregister plugins - 2.2.3 + 2.2.4 diff --git a/plugins/pom.xml b/plugins/pom.xml index 5968a84..6e0fcec 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -4,7 +4,7 @@ org.smartregister opensrp-gateway-plugin - 2.2.3 + 2.2.4 plugins diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java index 6d6383f..94038c3 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java @@ -181,6 +181,7 @@ public List getDescendants( allLocations.add(parentLocation); } if (childLocationBundle != null) { + Utils.fetchAllBundlePagesAndInject(r4FHIRClient, childLocationBundle); childLocationBundle.getEntry().parallelStream() .forEach( childLocation -> { @@ -193,24 +194,6 @@ public List 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; diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java index 4f71f91..e4ee255 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -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> 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 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 fetchCompositionAndPractitionerDetails( - String subject, String applicationId, FhirContext fhirContext) { + PractitionerDetails fetchPractitionerDetails(String subject) { fhirContext.registerCustomType(PractitionerDetails.class); IGenericClient client = Utils.createFhirClientForR4(fhirContext); @@ -227,44 +260,33 @@ Pair 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 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 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> getSyncStrategyIds( - String subjectId, - String applicationId, - FhirContext fhirContext, - RequestDetailsReader requestDetailsReader) { - Pair 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); } @@ -272,8 +294,9 @@ private Map> getSyncStrategyIds( private List getLocationUuids(String[] syncLocations) { List 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,16 +376,9 @@ private Map> 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<>(); } @@ -370,15 +386,7 @@ private Map> collateSyncStrategyIds( 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( diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java index 3e4ff94..c0c4ca3 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java @@ -1,19 +1,28 @@ 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; @@ -21,6 +30,7 @@ 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 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; + } } diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java index 284c2df..2c688e2 100755 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java @@ -451,4 +451,16 @@ public void testAccessDeniedWhenSingleRoleMissingForTypeBundleResources() throws assertThat(canAccess, equalTo(false)); } + + @Test + public void testGenerateSyncStrategyIdsCacheKey() { + String testUserId = "my-test-user-id"; + Map 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); + } } diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java index 4e126dd..98e0ba0 100644 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java @@ -1,8 +1,13 @@ 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; @@ -10,11 +15,13 @@ 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; @@ -22,18 +29,23 @@ 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 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 parameters = new HashMap<>(); + parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"location1"}); + + MockedStatic 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 parameters = new HashMap<>(); + parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"location1"}); + + String result = + PermissionAccessChecker.generateSyncStrategyIdsCacheKey( + userId, syncStrategy, parameters); + + Assert.assertEquals(userId, result); + } } diff --git a/pom.xml b/pom.xml index 57c55c4..69134af 100755 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ org.smartregister opensrp-gateway-plugin - 2.2.3 + 2.2.4 pom