From f02522f255f78c8150c8ccbc243cd0cf9d2512e5 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 23 Aug 2023 15:04:00 +0500 Subject: [PATCH 01/22] Move plugins to own repo --- .gitignore | 0 README.md | 19 +- pom.xml | 136 +---- .../plugin/AccessGrantedAndUpdateList.java | 35 +- .../gateway/plugin/ListAccessChecker.java | 384 ++++++++++++ .../gateway/plugin/PatientAccessChecker.java | 315 ++++++++++ .../plugin/PermissionAccessChecker.java | 116 ++-- .../PractitionerDetailsEndpointHelper.java | 548 +++++++++++++++++ .../fhir/gateway/plugin/SmartFhirScope.java | 164 +++++ .../gateway/plugin/SmartScopeChecker.java | 50 ++ .../gateway/plugin/SyncAccessDecision.java | 478 +++++++++++++++ .../plugin/OpenSRPSyncAccessDecision.java | 192 ------ .../plugin/AccessCheckerTestBase.java | 37 +- .../AccessGrantedAndUpdateListTest.java | 14 +- .../gateway/plugin/ListAccessCheckerTest.java | 328 ++++++++++ .../plugin/PatientAccessCheckerTest.java | 219 +++++++ .../plugin/PermissionAccessCheckerTest.java | 10 +- .../gateway/plugin/SmartScopeCheckerTest.java | 106 ++++ .../plugin/SyncAccessDecisionTest.java | 565 ++++++++++++++++++ .../plugin/TestRequestDetailsToReader.java | 106 ++++ .../google/fhir/gateway}/plugin/TestUtil.java | 4 +- .../plugin/OpenSRPSyncAccessDecisionTest.java | 186 ------ ...bundle_transaction_delete_non_patient.json | 12 + .../bundle_transaction_delete_patient.json | 34 ++ ...nsaction_delete_patient_unauthorized.json} | 2 +- ...ransaction_get_non_patient_authorized.json | 12 + .../bundle_transaction_patch_authorized.json | 30 + ...le_transaction_put_authorized_patient.json | 34 ++ .../hapi_sync_filter_ignored_queries.json | 29 + .../resources/test_bundle_transaction.json | 61 ++ src/test/resources/test_list_resource.json | 35 ++ 31 files changed, 3663 insertions(+), 598 deletions(-) mode change 100644 => 100755 .gitignore mode change 100644 => 100755 pom.xml rename src/main/java/{org/smartregister/fhir/proxy => com/google/fhir/gateway}/plugin/AccessGrantedAndUpdateList.java (86%) create mode 100644 src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java create mode 100644 src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java rename src/main/java/{org/smartregister/fhir/proxy => com/google/fhir/gateway}/plugin/PermissionAccessChecker.java (78%) mode change 100644 => 100755 create mode 100644 src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java create mode 100644 src/main/java/com/google/fhir/gateway/plugin/SmartFhirScope.java create mode 100644 src/main/java/com/google/fhir/gateway/plugin/SmartScopeChecker.java create mode 100755 src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java delete mode 100644 src/main/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java rename src/test/java/{org/smartregister/fhir/proxy => com/google/fhir/gateway}/plugin/AccessCheckerTestBase.java (91%) rename src/test/java/{org/smartregister/fhir/proxy => com/google/fhir/gateway}/plugin/AccessGrantedAndUpdateListTest.java (82%) create mode 100644 src/test/java/com/google/fhir/gateway/plugin/ListAccessCheckerTest.java create mode 100644 src/test/java/com/google/fhir/gateway/plugin/PatientAccessCheckerTest.java rename src/test/java/{org/smartregister/fhir/proxy => com/google/fhir/gateway}/plugin/PermissionAccessCheckerTest.java (98%) mode change 100644 => 100755 create mode 100644 src/test/java/com/google/fhir/gateway/plugin/SmartScopeCheckerTest.java create mode 100755 src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java create mode 100644 src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java rename src/test/java/{org/smartregister/fhir/proxy => com/google/fhir/gateway}/plugin/TestUtil.java (93%) delete mode 100644 src/test/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java create mode 100644 src/test/resources/bundle_transaction_delete_non_patient.json create mode 100644 src/test/resources/bundle_transaction_delete_patient.json rename src/test/resources/{bundle_transaction_delete.json => bundle_transaction_delete_patient_unauthorized.json} (92%) create mode 100644 src/test/resources/bundle_transaction_get_non_patient_authorized.json create mode 100644 src/test/resources/bundle_transaction_patch_authorized.json create mode 100644 src/test/resources/bundle_transaction_put_authorized_patient.json create mode 100644 src/test/resources/hapi_sync_filter_ignored_queries.json create mode 100644 src/test/resources/test_bundle_transaction.json create mode 100644 src/test/resources/test_list_resource.json diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/README.md b/README.md index 84dcec33..5e2a5168 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,18 @@ -# fhir-gateway-plugin +# AccessChecker plugins -This repo holds the OpenSRP permissions checker and data access checker +To implement an access-checker plugin, the +[AccessCheckerFactory interface](../server/src/main/java/com/google/fhir/gateway/interfaces/AccessCheckerFactory.java) +must be implemented, and it must be annotated by a `@Named(value = "KEY")` +annotation. `KEY` is the name of the access-checker that can be used when +running the proxy server (by setting `ACCESS_CHECKER` environment variable). + +Example access-checker plugins in this module are +[ListAccessChecker](src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java) +and +[PatientAccessChecker](src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java). + +Beside doing basic validation of the access-token, the server also provides some +query parameters and resource parsing functionality which are wrapped inside +[PatientFinder](../server/src/main/java/com/google/fhir/gateway/interfaces/PatientFinder.java). + + diff --git a/pom.xml b/pom.xml old mode 100644 new mode 100755 index 131614bf..4b0fb992 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ - com.google.fhir.proxy - fhir-proxy - 0.1.0-Beta + com.google.fhir.gateway + fhir-gateway + 0.2.0 org.smartregister @@ -35,18 +35,19 @@ GitHub Issues https://github.com/opensrp/fhir-access-proxy-plugin - - - nexus-releases - https://oss.sonatype.org/service/local/staging/deploy/maven2 - - - false - nexus-snapshots - Nexus Snapshots Repository - https://oss.sonatype.org/content/repositories/snapshots - - + + + + + + + + + + + + + UTF-8 @@ -54,112 +55,33 @@ 11 ${project.basedir} 6.0.1 - 0.1.0-Beta + 0.2.0 4.1 - - com.google.fhir.proxy + ${project.parent.groupId} server - ${fhir.proxy-plugin.version} + 0.1.32 + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided - ca.uhn.hapi.fhir hapi-fhir-client - ${hapifhir.version} + ${hapifhir_version} org.smartregister fhir-common-utils - 0.0.3-SNAPSHOT + 0.0.9-SNAPSHOT compile - - - standalone-app - - - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.10.1 - - UTF-8 - true - 1.8 - 1.8 - - - - org.springframework.boot - spring-boot-maven-plugin - - - repackage - - repackage - - - exec - com.google.fhir.proxy.plugin.MainApp - - - - - - com.mycila - license-maven-plugin - ${license.maven.plugin.version} - -
${project.basedir}/license-header.txt
- - .venv/** - **/*.txt - -
- - - com.mycila - license-maven-plugin-git - ${license.maven.plugin.version} - - - - - first - - format - - process-sources - - -
-
-
-
-
- diff --git a/src/main/java/org/smartregister/fhir/proxy/plugin/AccessGrantedAndUpdateList.java b/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java similarity index 86% rename from src/main/java/org/smartregister/fhir/proxy/plugin/AccessGrantedAndUpdateList.java rename to src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java index da13b471..82ec025a 100644 --- a/src/main/java/org/smartregister/fhir/proxy/plugin/AccessGrantedAndUpdateList.java +++ b/src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright ${license.git.copyrightYears} Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,20 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.fhir.proxy.plugin; +package com.google.fhir.gateway.plugin; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.google.common.base.Preconditions; import com.google.common.collect.Sets; import com.google.common.escape.Escaper; import com.google.common.io.CharStreams; import com.google.common.net.UrlEscapers; -import com.google.fhir.proxy.FhirUtil; -import com.google.fhir.proxy.HttpFhirClient; -import com.google.fhir.proxy.HttpUtil; -import com.google.fhir.proxy.interfaces.AccessDecision; +import com.google.fhir.gateway.FhirUtil; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.HttpUtil; +import com.google.fhir.gateway.interfaces.AccessDecision; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import com.google.fhir.gateway.interfaces.RequestMutation; import java.io.IOException; import java.util.Set; import org.apache.http.HttpResponse; @@ -63,13 +64,19 @@ private AccessGrantedAndUpdateList( this.resourceTypeExpected = resourceTypeExpected; } + @Override + public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) { + return null; + } + @Override public boolean canAccess() { return true; } @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(); @@ -90,7 +97,8 @@ public String postProcess(HttpResponse response) throws IOException { } if (FhirUtil.isSameResourceType(resource.fhirType(), ResourceType.Bundle)) { - // TODO Response potentially too large to be loaded into memory b/215786247 + // TODO Response potentially too large to be loaded into memory; see: + // https://github.com/google/fhir-gateway/issues/64 Bundle bundle = (Bundle) parser.parseResource(content); Set patientIdsInResponse = Sets.newHashSet(); @@ -112,7 +120,8 @@ public String postProcess(HttpResponse response) throws IOException { private void addPatientToList(String newPatient) throws IOException { Preconditions.checkNotNull(newPatient); - // TODO create this with HAPI client instead of handcrafting (b/211231483)! + // TODO create this with HAPI client instead of handcrafting; see: + // https://github.com/google/fhir-gateway/issues/65 String jsonPatch = String.format( "[{" @@ -127,7 +136,8 @@ private void addPatientToList(String newPatient) throws IOException { + "}]", newPatient); logger.info("Updating access list {} with patch {}", patientListId, jsonPatch); - // TODO decide how to handle failures in access list updates (b/211243404). + // TODO decide how to handle failures in access list updates; see: + // https://github.com/google/fhir-gateway/issues/66 httpFhirClient.patchResource( String.format("List/%s", PARAM_ESCAPER.escape(patientListId)), jsonPatch); } @@ -146,7 +156,4 @@ public static AccessGrantedAndUpdateList forBundle( return new AccessGrantedAndUpdateList( patientListId, httpFhirClient, fhirContext, existPutPatients, ResourceType.Bundle); } - - @Override - public void preProcess(ServletRequestDetails servletRequestDetails) {} } diff --git a/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java b/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java new file mode 100644 index 00000000..61ed7427 --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java @@ -0,0 +1,384 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import com.google.common.escape.Escaper; +import com.google.common.net.UrlEscapers; +import com.google.fhir.gateway.*; +import com.google.fhir.gateway.BundlePatients.BundlePatientsBuilder; +import com.google.fhir.gateway.interfaces.*; +import java.io.IOException; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.inject.Named; +import org.apache.http.HttpResponse; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.ResourceType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This access-checker uses the `patient_list` ID in the access token to fetch the "List" of patient + * IDs that the given user has access to. + */ +public class ListAccessChecker implements AccessChecker { + + private static final Logger logger = LoggerFactory.getLogger(ListAccessChecker.class); + private final FhirContext fhirContext; + private final HttpFhirClient httpFhirClient; + private final String patientListId; + private final PatientFinder patientFinder; + private final Escaper PARAM_ESCAPER = UrlEscapers.urlFormParameterEscaper(); + + private ListAccessChecker( + HttpFhirClient httpFhirClient, + String patientListId, + FhirContext fhirContext, + PatientFinder patientFinder) { + this.fhirContext = fhirContext; + this.httpFhirClient = httpFhirClient; + this.patientListId = patientListId; + this.patientFinder = patientFinder; + } + + /** + * Sends query to backend with user supplied parameters + * + * @param itemsParam resources to search for in the list. Must start with "item=". It is assumed + * that this is properly escaped to be added to the URL string. + * @return the outcome of access checking. Returns false if no parameter is provided, or if the + * parameter does not start with "item=", or if the query does not return exactly one match. + */ + private boolean listIncludesItems(String itemsParam) { + Preconditions.checkArgument(itemsParam.startsWith("item=")); + if (itemsParam.equals("item=")) { + return false; + } + // We cannot use `_summary` parameter because it is not implemented on GCP yet; so to prevent a + // potentially huge list to be fetched each time, we add `_elements=id`. + String searchQuery = + String.format( + "/List?_id=%s&_elements=id&%s", PARAM_ESCAPER.escape(this.patientListId), itemsParam); + logger.debug("Search query for patient access authorization check is: {}", searchQuery); + try { + HttpResponse httpResponse = httpFhirClient.getResource(searchQuery); + HttpUtil.validateResponseEntityOrFail(httpResponse, searchQuery); + Bundle bundle = FhirUtil.parseResponseToBundle(fhirContext, httpResponse); + // We expect exactly one result which is `patientListId`. + return bundle.getTotal() == 1; + } catch (IOException e) { + logger.error("Exception while accessing " + searchQuery, e); + } + return false; + } + // Note this returns true iff at least one of the patient IDs is found in the associated list. + // The rationale is that a user should have access to a resource iff they are authorized to access + // at least one of the patients referenced in that resource. This is a subjective decision, so we + // may want to revisit it in the future. + // Note patientIds are expected NOT to include the `Patient/` prefix (pure IDs only). + private boolean serverListIncludesAnyPatient(Set patientIds) { + if (patientIds == null) { + return false; + } + // TODO consider using the HAPI FHIR client instead; see: + // https://github.com/google/fhir-gateway/issues/65. + String patientParam = + queryBuilder(patientIds, PARAM_ESCAPER.escape("Patient/"), PARAM_ESCAPER.escape(",")); + return listIncludesItems("item=" + patientParam); + } + + // Returns true iff all the patient IDs are found in the associated list. + // Note patientIds are expected to include the `Patient/` prefix. + // TODO fix the above inconsistency with `serverListIncludesAnyPatient`. + private boolean serverListIncludesAllPatients(Set patientIds) { + if (patientIds == null) { + return false; + } + String patientParam = queryBuilder(patientIds, "item=", "&"); + return listIncludesItems(patientParam); + } + + private boolean patientsExist(String patientId) throws IOException { + // TODO consider using the HAPI FHIR client instead; see: + // https://github.com/google/fhir-gateway/issues/65 + String searchQuery = + String.format("/Patient?_id=%s&_elements=id", PARAM_ESCAPER.escape(patientId)); + HttpResponse response = httpFhirClient.getResource(searchQuery); + Bundle bundle = FhirUtil.parseResponseToBundle(fhirContext, response); + if (bundle.getTotal() > 1) { + logger.error( + String.format( + "%s patients with the same ID of one of ths %s returned from the FHIR store.", + bundle.getTotal(), patientId)); + } + return (bundle.getTotal() > 0); + } + + /** + * Inspects the given request to make sure that it is for a FHIR resource of a patient that the + * current user has access too; i.e., the patient is in the patient-list associated to the user. + * + * @param requestDetails the original request sent to the proxy. + * @return true iff patient is in the patient-list associated to the current user. + */ + @Override + public AccessDecision checkAccess(RequestDetailsReader requestDetails) { + try { + // For a Bundle requestDetails.getResourceName() returns null + if (requestDetails.getRequestType() == RequestTypeEnum.POST + && requestDetails.getResourceName() == null) { + return processBundle(requestDetails); + } + // Process non-Bundle requests + switch (requestDetails.getRequestType()) { + case GET: + return processGet(requestDetails); + case POST: + return processPost(requestDetails); + case PUT: + return processPut(requestDetails); + case PATCH: + return processPatch(requestDetails); + case DELETE: + return processDelete(requestDetails); + default: + return NoOpAccessDecision.accessDenied(); + } + } catch (IOException e) { + logger.error("Exception while checking patient existence; denying access! ", e); + return NoOpAccessDecision.accessDenied(); + } + } + + private AccessDecision processGet(RequestDetailsReader requestDetails) { + // There should be a patient id in search params; the param name is based on the resource. + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.List)) { + if (patientListId.equals(FhirUtil.getIdOrNull(requestDetails))) { + return NoOpAccessDecision.accessGranted(); + } + return NoOpAccessDecision.accessDenied(); + } + String patientId = patientFinder.findPatientFromParams(requestDetails); + return new NoOpAccessDecision(serverListIncludesAnyPatient(Sets.newHashSet(patientId))); + } + + private AccessDecision processPost(RequestDetailsReader requestDetails) { + // We have decided to let clients add new patients while understanding its security risks. + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.Patient)) { + return AccessGrantedAndUpdateList.forPatientResource( + patientListId, httpFhirClient, fhirContext); + } + Set patientIds = patientFinder.findPatientsInResource(requestDetails); + return new NoOpAccessDecision(serverListIncludesAnyPatient(patientIds)); + } + + private AccessDecision processPut(RequestDetailsReader requestDetails) throws IOException { + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.Patient)) { + AccessDecision accessDecision = checkPatientAccessInUpdate(requestDetails); + if (accessDecision == null) { + return AccessGrantedAndUpdateList.forPatientResource( + patientListId, httpFhirClient, fhirContext); + } + return accessDecision; + } + return checkNonPatientAccessInUpdate(requestDetails, RequestTypeEnum.PUT); + } + + private AccessDecision processPatch(RequestDetailsReader requestDetails) throws IOException { + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.Patient)) { + AccessDecision accessDecision = checkPatientAccessInUpdate(requestDetails); + if (accessDecision == null) { + logger.error("Creating a new Patient via PATCH is not allowed"); + return NoOpAccessDecision.accessDenied(); + } + return accessDecision; + } + return checkNonPatientAccessInUpdate(requestDetails, RequestTypeEnum.PATCH); + } + + private AccessDecision processDelete(RequestDetailsReader requestDetails) { + // We don't support deletion of List resource used as an access list for a user. + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.List) + && patientListId.equals(FhirUtil.getIdOrNull(requestDetails))) { + return NoOpAccessDecision.accessDenied(); + } + + // TODO(https://github.com/google/fhir-gateway/issues/63):Support direct resource deletion. + + // There should be a patient id in search params; the param name is based on the resource. + String patientId = patientFinder.findPatientFromParams(requestDetails); + return new NoOpAccessDecision(serverListIncludesAnyPatient(Sets.newHashSet(patientId))); + } + + private AccessDecision checkNonPatientAccessInUpdate( + RequestDetailsReader requestDetails, RequestTypeEnum updateMethod) { + Preconditions.checkArgument( + (updateMethod == RequestTypeEnum.PATCH) || (updateMethod == RequestTypeEnum.PUT), + "Expected either PATCH or PUT!"); + + // We do not allow direct resource PUT/PATCH, so Patient ID must be returned + String patientId = patientFinder.findPatientFromParams(requestDetails); + Set patientQueries = Sets.newHashSet(); + // Escaping is not needed here as the set elements will be escaped later. + patientQueries.add(String.format("Patient/%s", patientId)); + + Set patientSet = Sets.newHashSet(); + if (updateMethod == RequestTypeEnum.PATCH) { + patientSet = + patientFinder.findPatientsInPatch(requestDetails, requestDetails.getResourceName()); + } + if (updateMethod == RequestTypeEnum.PUT) { + patientSet = patientFinder.findPatientsInResource(requestDetails); + // One patient referenced in PUT needs to be accessible by client. + if (patientSet.isEmpty()) { + logger.error("No Patient ID referenced in PUT body; denying access!"); + return NoOpAccessDecision.accessDenied(); + } + } + patientQueries.add(queryBuilder(patientSet, "Patient/", ",")); + return new NoOpAccessDecision(serverListIncludesAllPatients(patientQueries)); + } + + @Nullable + private AccessDecision checkPatientAccessInUpdate(RequestDetailsReader requestDetails) + throws IOException { + String patientId = FhirUtil.getIdOrNull(requestDetails); + if (patientId == null || !FhirUtil.isValidId(patientId)) { + // This is an invalid PUT/PATCH request; note we are not supporting "conditional updates" for + // Patient resources. + logger.error("The provided Patient resource has no ID or it is invalid; denying access!"); + return NoOpAccessDecision.accessDenied(); + } + if (patientsExist(patientId)) { + logger.info("Updating existing patient {}, so no need to update access list.", patientId); + return new NoOpAccessDecision(serverListIncludesAnyPatient(Sets.newHashSet(patientId))); + } + // Reaching here means a new Patient being created. + return null; + } + + private AccessDecision processBundle(RequestDetailsReader requestDetails) throws IOException { + BundlePatients patientRequestsInBundle = createBundlePatients(requestDetails); + + if (patientRequestsInBundle == null) { + return NoOpAccessDecision.accessDenied(); + } + + boolean createPatients = patientRequestsInBundle.areTherePatientToCreate(); + Set putPatientIds = patientRequestsInBundle.getUpdatedPatients(); + + if (!createPatients && putPatientIds.isEmpty()) { + return NoOpAccessDecision.accessGranted(); + } + + if (putPatientIds.isEmpty()) { + return AccessGrantedAndUpdateList.forBundle( + patientListId, httpFhirClient, fhirContext, Sets.newHashSet()); + } else { + return AccessGrantedAndUpdateList.forBundle( + patientListId, httpFhirClient, fhirContext, putPatientIds); + } + } + + @Nullable + private BundlePatients createBundlePatients(RequestDetailsReader requestDetails) + throws IOException { + Bundle requestBundle = FhirUtil.parseRequestToBundle(fhirContext, requestDetails); + BundlePatients patientsInBundleUnfiltered = patientFinder.findPatientsInBundle(requestBundle); + + if (patientsInBundleUnfiltered == null) { + return null; + } + + BundlePatientsBuilder builder = new BundlePatientsBuilder(); + builder.setPatientCreationFlag(patientsInBundleUnfiltered.areTherePatientToCreate()); + + Set patientsToCreate = Sets.newHashSet(); + Set patientsToUpdate = Sets.newHashSet(); + + for (String patientId : patientsInBundleUnfiltered.getUpdatedPatients()) { + if (!patientsExist(patientId)) { + patientsToCreate.add(patientId); + } else { + patientsToUpdate.add(patientId); + } + } + + if (!patientsToCreate.isEmpty()) { + builder.setPatientCreationFlag(true); + } + + Set patientQueries = Sets.newHashSet(); + for (Set patientRefSet : patientsInBundleUnfiltered.getReferencedPatients()) { + if (Collections.disjoint(patientRefSet, patientsToCreate)) { + String orQuery = queryBuilder(patientRefSet, "Patient/", ","); + patientQueries.add(orQuery); + } + } + + if (!patientsToUpdate.isEmpty()) { + for (String eachPatient : patientsToUpdate) { + String andQuery = String.format("Patient/%s", eachPatient); + patientQueries.add(andQuery); + } + } + + if (!patientQueries.isEmpty() && !serverListIncludesAllPatients(patientQueries)) { + logger.error("Reference Patients not in List!"); + return null; + } + + return builder.addUpdatePatients(patientsToUpdate).build(); + } + + private String queryBuilder(Set patientSet, String prefix, String delimiter) { + return patientSet.stream() + .filter(Objects::nonNull) + .filter(Predicate.not(String::isEmpty)) + .map(p -> prefix + PARAM_ESCAPER.escape(p)) + .collect(Collectors.joining(delimiter)); + } + + @Named(value = "list") + public static class Factory implements AccessCheckerFactory { + + @VisibleForTesting static final String PATIENT_LIST_CLAIM = "patient_list"; + + private String getListId(DecodedJWT jwt) { + return FhirUtil.checkIdOrFail(JwtUtil.getClaimOrDie(jwt, PATIENT_LIST_CLAIM)); + } + + @Override + public AccessChecker create( + DecodedJWT jwt, + HttpFhirClient httpFhirClient, + FhirContext fhirContext, + PatientFinder patientFinder) { + String patientListId = getListId(jwt); + return new ListAccessChecker(httpFhirClient, patientListId, fhirContext, patientFinder); + } + } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java b/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java new file mode 100644 index 00000000..e5bc8d1e --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java @@ -0,0 +1,315 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Sets; +import com.google.fhir.gateway.BundlePatients; +import com.google.fhir.gateway.FhirUtil; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.JwtUtil; +import com.google.fhir.gateway.interfaces.*; +import com.google.fhir.gateway.plugin.SmartFhirScope.Permission; +import com.google.fhir.gateway.plugin.SmartFhirScope.Principal; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Set; +import javax.inject.Named; +import org.hl7.fhir.instance.model.api.IIdType; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.r4.model.Bundle.BundleEntryRequestComponent; +import org.hl7.fhir.r4.model.Reference; +import org.hl7.fhir.r4.model.ResourceType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * This access-checker uses the `patient_id` and `scope` claims in the access token to decide + * whether access to a request should be granted or not. The `scope` claims are expected to tbe + * SMART-on-FHIR compliant. + */ +public class PatientAccessChecker implements AccessChecker { + private static final Logger logger = LoggerFactory.getLogger(PatientAccessChecker.class); + + private final String authorizedPatientId; + private final PatientFinder patientFinder; + + private final FhirContext fhirContext; + + private final SmartScopeChecker smartScopeChecker; + + private PatientAccessChecker( + FhirContext fhirContext, + String authorizedPatientId, + PatientFinder patientFinder, + SmartScopeChecker smartScopeChecker) { + Preconditions.checkNotNull(authorizedPatientId); + Preconditions.checkNotNull(patientFinder); + Preconditions.checkNotNull(smartScopeChecker); + Preconditions.checkNotNull(fhirContext); + this.authorizedPatientId = authorizedPatientId; + this.patientFinder = patientFinder; + this.fhirContext = fhirContext; + this.smartScopeChecker = smartScopeChecker; + } + + @Override + public AccessDecision checkAccess(RequestDetailsReader requestDetails) { + // For a Bundle requestDetails.getResourceName() returns null + if (requestDetails.getRequestType() == RequestTypeEnum.POST + && requestDetails.getResourceName() == null) { + return processBundle(requestDetails); + } + + switch (requestDetails.getRequestType()) { + case GET: + return processGet(requestDetails); + case POST: + return processPost(requestDetails); + case PUT: + // TODO(https://github.com/google/fhir-gateway/issues/88): Support update as create + // operation + case PATCH: + return processUpdate(requestDetails); + case DELETE: + return processDelete(requestDetails); + default: + return NoOpAccessDecision.accessDenied(); + } + } + + private AccessDecision processGet(RequestDetailsReader requestDetails) { + if (requestDetails.getResourceName() == null) { + return NoOpAccessDecision.accessDenied(); + } + // This operation corresponds to the read/vread/history operations on instance + if (requestDetails.getId() != null) { + return processRead(requestDetails); + } + return processSearch(requestDetails); + } + + private AccessDecision processPost(RequestDetailsReader requestDetails) { + // TODO(https://github.com/google/fhir-gateway/issues/87): Add support for search in post + return processCreate(requestDetails); + } + + private AccessDecision processRead(RequestDetailsReader requestDetails) { + String patientId = patientFinder.findPatientFromParams(requestDetails); + return new NoOpAccessDecision( + authorizedPatientId.equals(patientId) + && smartScopeChecker.hasPermission(requestDetails.getResourceName(), Permission.READ)); + } + + private AccessDecision processSearch(RequestDetailsReader requestDetails) { + String patientId = patientFinder.findPatientFromParams(requestDetails); + return new NoOpAccessDecision( + authorizedPatientId.equals(patientId) + && smartScopeChecker.hasPermission( + requestDetails.getResourceName(), Permission.SEARCH)); + } + + private AccessDecision processCreate(RequestDetailsReader requestDetails) { + // This AccessChecker does not accept new patients. + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.Patient)) { + return NoOpAccessDecision.accessDenied(); + } + Set patientIds = patientFinder.findPatientsInResource(requestDetails); + return new NoOpAccessDecision( + patientIds.contains(authorizedPatientId) + && smartScopeChecker.hasPermission( + requestDetails.getResourceName(), Permission.CREATE)); + } + + private AccessDecision processUpdate(RequestDetailsReader requestDetails) { + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.Patient)) { + return checkPatientAccessInUpdate(requestDetails); + } + return checkNonPatientAccessInUpdate(requestDetails, requestDetails.getRequestType()); + } + + private AccessDecision processDelete(RequestDetailsReader requestDetails) { + // This AccessChecker does not allow deletion of Patient resource + if (FhirUtil.isSameResourceType(requestDetails.getResourceName(), ResourceType.Patient)) { + return NoOpAccessDecision.accessDenied(); + } + // TODO(https://github.com/google/fhir-gateway/issues/63):Support direct resource deletion. + String patientId = patientFinder.findPatientFromParams(requestDetails); + return new NoOpAccessDecision( + authorizedPatientId.equals(patientId) + && smartScopeChecker.hasPermission( + requestDetails.getResourceName(), Permission.DELETE)); + } + + private AccessDecision checkNonPatientAccessInUpdate( + RequestDetailsReader requestDetails, RequestTypeEnum updateMethod) { + // We do not allow direct resource PUT/PATCH, so Patient ID must be returned + String patientId = patientFinder.findPatientFromParams(requestDetails); + if (!patientId.equals(authorizedPatientId)) { + return NoOpAccessDecision.accessDenied(); + } + + Set patientIds = Sets.newHashSet(); + if (updateMethod == RequestTypeEnum.PATCH) { + patientIds = + patientFinder.findPatientsInPatch(requestDetails, requestDetails.getResourceName()); + if (patientIds.isEmpty()) { + return NoOpAccessDecision.accessGranted(); + } + } + if (updateMethod == RequestTypeEnum.PUT) { + patientIds = patientFinder.findPatientsInResource(requestDetails); + } + return new NoOpAccessDecision( + patientIds.contains(authorizedPatientId) + && smartScopeChecker.hasPermission( + requestDetails.getResourceName(), Permission.UPDATE)); + } + + private AccessDecision checkPatientAccessInUpdate(RequestDetailsReader requestDetails) { + String patientId = FhirUtil.getIdOrNull(requestDetails); + if (patientId == null) { + // This is an invalid PUT request; note we are not supporting "conditional updates". + logger.error("The provided Patient resource has no ID; denying access!"); + return NoOpAccessDecision.accessDenied(); + } + return new NoOpAccessDecision( + authorizedPatientId.equals(patientId) + && smartScopeChecker.hasPermission(ResourceType.Patient.name(), Permission.UPDATE)); + } + + private AccessDecision processBundle(RequestDetailsReader requestDetails) { + Bundle requestBundle = FhirUtil.parseRequestToBundle(fhirContext, requestDetails); + BundlePatients patientsInBundle = patientFinder.findPatientsInBundle(requestBundle); + + if (patientsInBundle == null + || patientsInBundle.areTherePatientToCreate() + || !patientsInBundle.getDeletedPatients().isEmpty()) { + return NoOpAccessDecision.accessDenied(); + } + + if (!patientsInBundle.getUpdatedPatients().isEmpty() + && !patientsInBundle.getUpdatedPatients().equals(ImmutableSet.of(authorizedPatientId))) { + return NoOpAccessDecision.accessDenied(); + } + + for (Set refSet : patientsInBundle.getReferencedPatients()) { + if (!refSet.contains(authorizedPatientId)) { + return NoOpAccessDecision.accessDenied(); + } + } + + for (BundleEntryComponent entryComponent : requestBundle.getEntry()) { + if (!doesBundleElementHavePermission(entryComponent)) { + return NoOpAccessDecision.accessDenied(); + } + } + return NoOpAccessDecision.accessGranted(); + } + + private boolean doesReferenceElementHavePermission( + IIdType referenceElement, Permission permission) { + if (referenceElement.getResourceType() != null && referenceElement.hasIdPart()) { + return smartScopeChecker.hasPermission(referenceElement.getResourceType(), permission); + } else { + return smartScopeChecker.hasPermission(referenceElement.getValue(), permission); + } + } + + private boolean doesBundleElementHavePermission(BundleEntryComponent bundleEntry) { + BundleEntryRequestComponent bundleEntryRequest = bundleEntry.getRequest(); + try { + switch (bundleEntryRequest.getMethod()) { + case GET: + if (bundleEntryRequest.getUrl() != null) { + URI resourceUri = new URI(bundleEntryRequest.getUrl()); + IIdType referenceElement = new Reference(resourceUri.getPath()).getReferenceElement(); + return doesReferenceElementHavePermission(referenceElement, Permission.READ); + } + break; + case POST: + if (bundleEntry.getResource().getResourceType() != null) { + return smartScopeChecker.hasPermission( + bundleEntry.getResource().getResourceType().name(), Permission.CREATE); + } + // TODO(https://github.com/google/fhir-gateway/issues/87): Add support for search in post + break; + case PUT: + if (bundleEntryRequest.getUrl() != null) { + URI resourceUri = new URI(bundleEntryRequest.getUrl()); + IIdType referenceElement = new Reference(resourceUri.getPath()).getReferenceElement(); + return doesReferenceElementHavePermission(referenceElement, Permission.UPDATE); + } + break; + case PATCH: + if (bundleEntryRequest.getUrl() != null) { + URI resourceUri = new URI(bundleEntryRequest.getUrl()); + IIdType referenceElement = new Reference(resourceUri.getPath()).getReferenceElement(); + return doesReferenceElementHavePermission(referenceElement, Permission.UPDATE); + } + case DELETE: + if (bundleEntryRequest.getUrl() != null) { + URI resourceUri = new URI(bundleEntryRequest.getUrl()); + IIdType referenceElement = new Reference(resourceUri.getPath()).getReferenceElement(); + return doesReferenceElementHavePermission(referenceElement, Permission.DELETE); + } + break; + default: + return false; + } + } catch (URISyntaxException e) { + logger.error( + String.format("Error in parsing bundle request url %s", bundleEntryRequest.getUrl())); + } + return false; + } + + @Named(value = "patient") + static class Factory implements AccessCheckerFactory { + + @VisibleForTesting static final String PATIENT_CLAIM = "patient_id"; + + @VisibleForTesting static final String SCOPES_CLAIM = "scope"; + + private String getPatientId(DecodedJWT jwt) { + return FhirUtil.checkIdOrFail(JwtUtil.getClaimOrDie(jwt, PATIENT_CLAIM)); + } + + private SmartScopeChecker getSmartFhirPermissionChecker(DecodedJWT jwt) { + String scopesClaim = JwtUtil.getClaimOrDie(jwt, SCOPES_CLAIM); + String[] scopes = scopesClaim.strip().split("\\s+"); + return new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens(Arrays.asList(scopes)), + Principal.PATIENT); + } + + public AccessChecker create( + DecodedJWT jwt, + HttpFhirClient httpFhirClient, + FhirContext fhirContext, + PatientFinder patientFinder) { + return new PatientAccessChecker( + fhirContext, getPatientId(jwt), patientFinder, getSmartFhirPermissionChecker(jwt)); + } + } +} diff --git a/src/main/java/org/smartregister/fhir/proxy/plugin/PermissionAccessChecker.java b/src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java old mode 100644 new mode 100755 similarity index 78% rename from src/main/java/org/smartregister/fhir/proxy/plugin/PermissionAccessChecker.java rename to src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java index f40c872a..395c2d3a --- a/src/main/java/org/smartregister/fhir/proxy/plugin/PermissionAccessChecker.java +++ b/src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright ${license.git.copyrightYears} Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,70 +13,50 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.fhir.proxy.plugin; +package com.google.fhir.gateway.plugin; -import static com.google.fhir.proxy.ProxyConstants.SYNC_STRATEGY; -import static org.hl7.fhir.r4.model.Claim.CARE_TEAM; -import static org.smartregister.utils.Constants.LOCATION; -import static org.smartregister.utils.Constants.ORGANIZATION; +import static com.google.fhir.gateway.ProxyConstants.SYNC_STRATEGY; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.param.SpecialParam; import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.fhir.proxy.BundleResources; -import com.google.fhir.proxy.FhirProxyServer; -import com.google.fhir.proxy.HttpFhirClient; -import com.google.fhir.proxy.JwtUtil; -import com.google.fhir.proxy.ResourceFinderImp; -import com.google.fhir.proxy.interfaces.AccessChecker; -import com.google.fhir.proxy.interfaces.AccessCheckerFactory; -import com.google.fhir.proxy.interfaces.AccessDecision; -import com.google.fhir.proxy.interfaces.NoOpAccessDecision; -import com.google.fhir.proxy.interfaces.PatientFinder; -import com.google.fhir.proxy.interfaces.RequestDetailsReader; -import com.google.fhir.proxy.interfaces.ResourceFinder; +import com.google.fhir.gateway.*; +import com.google.fhir.gateway.interfaces.*; import com.google.gson.Gson; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import java.util.*; import java.util.stream.Collectors; import javax.inject.Named; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.model.practitioner.PractitionerDetails; +import org.smartregister.utils.Constants; public class PermissionAccessChecker implements AccessChecker { private static final Logger logger = LoggerFactory.getLogger(PermissionAccessChecker.class); private final ResourceFinder resourceFinder; private final List userRoles; - private final String applicationId; - - private final List careTeamIds; - - private final List locationIds; - - private final List organizationIds; - - private final List syncStrategy; + private SyncAccessDecision syncAccessDecision; private PermissionAccessChecker( + String keycloakUUID, List userRoles, - ResourceFinder resourceFinder, + ResourceFinderImp resourceFinder, String applicationId, List careTeamIds, List locationIds, List organizationIds, - List syncStrategy) { + String syncStrategy) { Preconditions.checkNotNull(userRoles); Preconditions.checkNotNull(resourceFinder); Preconditions.checkNotNull(applicationId); @@ -86,11 +66,16 @@ private PermissionAccessChecker( Preconditions.checkNotNull(syncStrategy); this.resourceFinder = resourceFinder; this.userRoles = userRoles; - this.applicationId = applicationId; - this.careTeamIds = careTeamIds; - this.organizationIds = organizationIds; - this.locationIds = locationIds; - this.syncStrategy = syncStrategy; + this.syncAccessDecision = + new SyncAccessDecision( + keycloakUUID, + applicationId, + true, + locationIds, + careTeamIds, + organizationIds, + syncStrategy, + userRoles); } @Override @@ -138,10 +123,7 @@ private AccessDecision processDelete(boolean userHasRole) { } private AccessDecision getAccessDecision(boolean userHasRole) { - return userHasRole - ? new OpenSRPSyncAccessDecision( - applicationId, true, locationIds, careTeamIds, organizationIds, syncStrategy) - : NoOpAccessDecision.accessDenied(); + return userHasRole ? syncAccessDecision : NoOpAccessDecision.accessDenied(); } private AccessDecision processPost(boolean userHasRole) { @@ -268,21 +250,18 @@ private Binary findApplicationConfigBinaryResource(String binaryResourceId) { return binary; } - private List findSyncStrategy(Binary binary) { + private String findSyncStrategy(Binary binary) { byte[] bytes = binary != null && binary.getDataElement() != null ? Base64.getDecoder().decode(binary.getDataElement().getValueAsString()) : null; - List syncStrategy = new ArrayList<>(); + String syncStrategy = Constants.EMPTY_STRING; if (bytes != null) { String json = new String(bytes); JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class); JsonArray jsonArray = jsonObject.getAsJsonArray(SYNC_STRATEGY); - if (jsonArray != null) { - for (JsonElement jsonElement : jsonArray) { - syncStrategy.add(jsonElement.getAsString()); - } - } + if (jsonArray != null && !jsonArray.isEmpty()) + syncStrategy = jsonArray.get(0).getAsString(); } return syncStrategy; } @@ -318,13 +297,6 @@ public Map> getMapForWhere(String keycloakUUID lst.add(tokenParam); hmOut.put(PractitionerDetails.SP_KEYCLOAK_UUID, lst); - // Adding isAuthProvided - SpecialParam isAuthProvided = new SpecialParam(); - isAuthProvided.setValue("false"); - List l = new ArrayList(); - l.add(isAuthProvided); - hmOut.put(PractitionerDetails.SP_IS_AUTH_PROVIDED, l); - return hmOut; } @@ -340,55 +312,47 @@ public AccessChecker create( Composition composition = readCompositionResource(applicationId); String binaryResourceReference = getBinaryResourceReference(composition); Binary binary = findApplicationConfigBinaryResource(binaryResourceReference); - List syncStrategy = findSyncStrategy(binary); + String syncStrategy = findSyncStrategy(binary); PractitionerDetails practitionerDetails = readPractitionerDetails(jwt.getSubject()); List careTeams; List organizations; - List locations; List careTeamIds = new ArrayList<>(); List organizationIds = new ArrayList<>(); List locationIds = new ArrayList<>(); - if (syncStrategy.size() > 0) { - if (syncStrategy.contains(CARE_TEAM)) { + if (StringUtils.isNotBlank(syncStrategy)) { + if (syncStrategy.equals(Constants.CARE_TEAM)) { careTeams = practitionerDetails != null && practitionerDetails.getFhirPractitionerDetails() != null ? practitionerDetails.getFhirPractitionerDetails().getCareTeams() : Collections.singletonList(new CareTeam()); for (CareTeam careTeam : careTeams) { - if (careTeam.getIdElement() != null - && careTeam.getIdElement().getIdPartAsLong() != null) { - careTeamIds.add(careTeam.getIdElement().getIdPartAsLong().toString()); + if (careTeam.getIdElement() != null) { + careTeamIds.add(careTeam.getIdElement().getIdPart()); } - careTeamIds.add(careTeam.getId()); } - } else if (syncStrategy.contains(ORGANIZATION)) { + } else if (syncStrategy.equals(Constants.ORGANIZATION)) { organizations = practitionerDetails != null && practitionerDetails.getFhirPractitionerDetails() != null ? practitionerDetails.getFhirPractitionerDetails().getOrganizations() : Collections.singletonList(new Organization()); for (Organization organization : organizations) { - if (organization.getIdElement() != null - && organization.getIdElement().getIdPartAsLong() != null) { - organizationIds.add(organization.getIdElement().getIdPartAsLong().toString()); + if (organization.getIdElement() != null) { + organizationIds.add(organization.getIdElement().getIdPart()); } } - } else if (syncStrategy.contains(LOCATION)) { - locations = + } else if (syncStrategy.equals(Constants.LOCATION)) { + locationIds = practitionerDetails != null && practitionerDetails.getFhirPractitionerDetails() != null - ? practitionerDetails.getFhirPractitionerDetails().getLocations() - : Collections.singletonList(new Location()); - for (Location location : locations) { - if (location.getIdElement() != null - && location.getIdElement().getIdPartAsLong() != null) { - locationIds.add(location.getIdElement().getIdPartAsLong().toString()); - } - } + ? PractitionerDetailsEndpointHelper.getAttributedLocations( + practitionerDetails.getFhirPractitionerDetails().getLocationHierarchyList()) + : locationIds; } } return new PermissionAccessChecker( + jwt.getSubject(), userRoles, ResourceFinderImp.getInstance(fhirContext), applicationId, diff --git a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java b/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java new file mode 100644 index 00000000..f35217b7 --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java @@ -0,0 +1,548 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import static org.smartregister.utils.Constants.EMPTY_STRING; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import com.google.fhir.gateway.ProxyConstants; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.BaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.CareTeam; +import org.hl7.fhir.r4.model.Enumerations; +import org.hl7.fhir.r4.model.Group; +import org.hl7.fhir.r4.model.Identifier; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.OrganizationAffiliation; +import org.hl7.fhir.r4.model.Practitioner; +import org.hl7.fhir.r4.model.PractitionerRole; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.ParentChildrenMap; +import org.smartregister.model.practitioner.FhirPractitionerDetails; +import org.smartregister.model.practitioner.PractitionerDetails; +import org.smartregister.utils.Constants; +import org.springframework.lang.Nullable; + +public class PractitionerDetailsEndpointHelper { + private static final Logger logger = + LoggerFactory.getLogger(PractitionerDetailsEndpointHelper.class); + public static final String PRACTITIONER_GROUP_CODE = "405623001"; + public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; + public static final Bundle EMPTY_BUNDLE = new Bundle(); + private IGenericClient r4FhirClient; + + public PractitionerDetailsEndpointHelper(IGenericClient fhirClient) { + this.r4FhirClient = fhirClient; + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } + + public PractitionerDetails getPractitionerDetailsByKeycloakId(String keycloakUuid) { + PractitionerDetails practitionerDetails = new PractitionerDetails(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); + } + + return practitionerDetails; + } + + public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { + Bundle bundle = new Bundle(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + } + + return bundle; + } + + private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { + Bundle responseBundle = new Bundle(); + List attributedPractitioners = new ArrayList<>(); + PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); + // Get other guys. + + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamList); + List supervisorCareTeamOrganizationLocationIds = + getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); + List officialLocationIds = + getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); + List locationHierarchies = + getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); + List attributedLocationsList = getAttributedLocations(locationHierarchies); + List attributedOrganizationIds = + getOrganizationIdsByLocationIds(attributedLocationsList); + + // Get care teams by organization Ids + List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); + + for (CareTeam careTeam : careTeamList) { + attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); + } + + careTeamList.addAll(attributedCareTeams); + + for (CareTeam careTeam : careTeamList) { + // Add current supervisor practitioners + attributedPractitioners.addAll( + careTeam.getParticipant().stream() + .filter( + it -> + it.hasMember() + && it.getMember() + .getReference() + .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) + .map( + it -> + getPractitionerByIdentifier( + getReferenceIDPart(it.getMember().getReference()))) + .collect(Collectors.toList())); + } + + List bundleEntryComponentList = new ArrayList<>(); + + for (Practitioner attributedPractitioner : attributedPractitioners) { + bundleEntryComponentList.add( + new Bundle.BundleEntryComponent() + .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); + } + + responseBundle.setEntry(bundleEntryComponentList); + responseBundle.setTotal(bundleEntryComponentList.size()); + return responseBundle; + } + + @NotNull + public static List getAttributedLocations(List locationHierarchies) { + List parentChildrenList = + locationHierarchies.stream() + .flatMap( + locationHierarchy -> + locationHierarchy + .getLocationHierarchyTree() + .getLocationsHierarchy() + .getParentChildren() + .stream()) + .collect(Collectors.toList()); + List attributedLocationsList = + parentChildrenList.stream() + .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) + .map(it -> getReferenceIDPart(it.toString())) + .collect(Collectors.toList()); + return attributedLocationsList; + } + + private List getOrganizationIdsByLocationIds(List attributedLocationsList) { + if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { + return new ArrayList<>(); + } + + Bundle organizationAffiliationsBundle = + getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) + .returnBundle(Bundle.class) + .execute(); + + return organizationAffiliationsBundle.getEntry().stream() + .map( + bundleEntryComponent -> + getReferenceIDPart( + ((OrganizationAffiliation) bundleEntryComponent.getResource()) + .getOrganization() + .getReference())) + .distinct() + .collect(Collectors.toList()); + } + + private String getPractitionerIdentifier(Practitioner practitioner) { + String practitionerId = EMPTY_STRING; + if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { + practitionerId = practitioner.getIdElement().getIdPart(); + } + return practitionerId; + } + + private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); + String practitionerId = getPractitionerIdentifier(practitioner); + + logger.info("Searching for care teams for practitioner with id: " + practitioner); + Bundle careTeams = getCareTeams(practitionerId); + List careTeamsList = mapBundleToCareTeams(careTeams); + fhirPractitionerDetails.setCareTeams(careTeamsList); + fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); + + logger.info("Searching for Organizations tied with CareTeams: "); + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamsList); + + Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); + logger.info("Managing Organization are fetched"); + + List managingOrganizationTeams = + mapBundleToOrganizations(careTeamManagingOrganizations); + + logger.info("Searching for organizations of practitioner with id: " + practitioner); + + List practitionerRoleList = + getPractitionerRolesByPractitionerId(practitionerId); + logger.info("Practitioner Roles are fetched"); + + List practitionerOrganizationIds = + getOrganizationIdsByPractitionerRoles(practitionerRoleList); + + Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); + + List teams = mapBundleToOrganizations(practitionerOrganizations); + // TODO Fix Distinct + List bothOrganizations = + Stream.concat(managingOrganizationTeams.stream(), teams.stream()) + .distinct() + .collect(Collectors.toList()); + + fhirPractitionerDetails.setOrganizations(bothOrganizations); + fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); + + Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); + logger.info("Groups are fetched"); + + List groupsList = mapBundleToGroups(groupsBundle); + fhirPractitionerDetails.setGroups(groupsList); + fhirPractitionerDetails.setId(practitionerId); + + logger.info("Searching for locations by organizations"); + + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle( + Stream.concat( + careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) + .distinct() + .collect(Collectors.toList())); + + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + + fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); + + List locationIds = + getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + + List locationsIdentifiers = + getOfficialLocationIdentifiersByLocationIds( + locationIds); // TODO Investigate why the Location ID and official identifiers are + // different + + logger.info("Searching for location hierarchy list by locations identifiers"); + List locationHierarchyList = + getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); + fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); + + logger.info("Searching for locations by ids"); + List locationsList = getLocationsByIds(locationIds); + fhirPractitionerDetails.setLocations(locationsList); + + practitionerDetails.setId(practitionerId); + practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); + + return practitionerDetails; + } + + private List mapBundleToOrganizations(Bundle organizationBundle) { + return organizationBundle.getEntry().stream() + .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private Bundle getGroupsAssignedToPractitioner(String practitionerId) { + return getFhirClientForR4() + .search() + .forResource(Group.class) + .where(Group.MEMBER.hasId(practitionerId)) + .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) + .returnBundle(Bundle.class) + .execute(); + } + + public static Predicate distinctByKey(Function keyExtractor) { + Set seen = ConcurrentHashMap.newKeySet(); + return t -> seen.add(keyExtractor.apply(t)); + } + + private List getPractitionerRolesByPractitionerId(String practitionerId) { + Bundle practitionerRoles = getPractitionerRoles(practitionerId); + return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); + } + + private List getOrganizationIdsByPractitionerRoles( + List practitionerRoles) { + return practitionerRoles.stream() + .filter(practitionerRole -> practitionerRole.hasOrganization()) + .map(it -> getReferenceIDPart(it.getOrganization().getReference())) + .collect(Collectors.toList()); + } + + private Practitioner getPractitionerByIdentifier(String identifier) { + Bundle resultBundle = + getFhirClientForR4() + .search() + .forResource(Practitioner.class) + .where(Practitioner.IDENTIFIER.exactly().identifier(identifier)) + .returnBundle(Bundle.class) + .execute(); + + return resultBundle != null + ? (Practitioner) resultBundle.getEntryFirstRep().getResource() + : null; + } + + private List getCareTeamsByOrganizationIds(List organizationIds) { + if (organizationIds.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasAnyOfIds( + organizationIds.stream() + .map( + it -> + Enumerations.ResourceType.ORGANIZATION.toCode() + + Constants.FORWARD_SLASH + + it) + .collect(Collectors.toList()))) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) + .map(it -> ((CareTeam) it.getResource())) + .collect(Collectors.toList()); + } + + private Bundle getCareTeams(String practitionerId) { + logger.info("Searching for Care Teams with practitioner id :" + practitionerId); + + return getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasId( + Enumerations.ResourceType.PRACTITIONER.toCode() + + Constants.FORWARD_SLASH + + practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private Bundle getPractitionerRoles(String practitionerId) { + logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); + return getFhirClientForR4() + .search() + .forResource(PractitionerRole.class) + .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private static String getReferenceIDPart(String reference) { + return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); + } + + private Bundle getOrganizationsById(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(Organization.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private @Nullable List getLocationsByIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) + .returnBundle(Bundle.class) + .execute(); + + return locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + } + + private @Nullable List getOfficialLocationIdentifiersByLocationIds( + List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + List locations = getLocationsByIds(locationIds); + + return locations.stream() + .map( + it -> + it.getIdentifier().stream() + .filter( + id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) + .map(it2 -> it2.getValue()) + .collect(Collectors.toList())) + .flatMap(it3 -> it3.stream()) + .collect(Collectors.toList()); + } + + private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { + if (organizationIds == null || organizationIds.isEmpty()) { + return new ArrayList<>(); + } + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + } + + private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private List getLocationIdentifiersByOrganizationAffiliations( + List organizationAffiliations) { + + return organizationAffiliations.stream() + .map( + organizationAffiliation -> + getReferenceIDPart( + organizationAffiliation.getLocation().stream() + .findFirst() + .get() + .getReference())) + .collect(Collectors.toList()); + } + + private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { + logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); + return careTeamsList.stream() + .filter(careTeam -> careTeam.hasManagingOrganization()) + .flatMap(it -> it.getManagingOrganization().stream()) + .map(it -> getReferenceIDPart(it.getReference())) + .collect(Collectors.toList()); + } + + private List mapBundleToCareTeams(Bundle careTeams) { + return careTeams.getEntry().stream() + .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToPractitionerRolesWithOrganization( + Bundle practitionerRoles) { + return practitionerRoles.getEntry().stream() + .map(it -> (PractitionerRole) it.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToGroups(Bundle groupsBundle) { + return groupsBundle.getEntry().stream() + .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToOrganizationAffiliation( + Bundle organizationAffiliationBundle) { + return organizationAffiliationBundle.getEntry().stream() + .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List getLocationsHierarchyByOfficialLocationIdentifiers( + List officialLocationIdentifiers) { + if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(LocationHierarchy.class) + .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .map(it -> ((LocationHierarchy) it.getResource())) + .collect(Collectors.toList()); + } + + public static String createSearchTagValues(Map.Entry entry) { + return entry.getKey() + + ProxyConstants.CODE_URL_VALUE_SEPARATOR + + StringUtils.join( + entry.getValue(), + ProxyConstants.PARAM_VALUES_SEPARATOR + + entry.getKey() + + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/SmartFhirScope.java b/src/main/java/com/google/fhir/gateway/plugin/SmartFhirScope.java new file mode 100644 index 00000000..f6e9a519 --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/SmartFhirScope.java @@ -0,0 +1,164 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import com.google.common.base.Preconditions; +import com.google.fhir.gateway.FhirUtil; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; +import lombok.Getter; + +/** + * This class models the SMART-on-FHIR permission scopes that are meant ot be used for accessing + * clinical data. The constraints in this class are according to the official guidelines here: + * https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html#scopes-for-requesting-clinical-data + */ +@Getter +public class SmartFhirScope { + private static final Pattern VALID_SCOPE_PATTERN = + Pattern.compile( + "(\\buser|patient|system\\b)(\\/((\\*)|([a-zA-Z]+)))(\\.((\\*)|([cruds]+)|(\\bread|write\\b)))"); + static final String ALL_RESOURCE_TYPES_WILDCARD = "*"; + private static final String ALL_RESOURCE_PERMISSIONS_WILDCARD = "*"; + private static final String SMART_V1_READ_RESOURCE_PERMISSIONS = "read"; + private static final String SMART_V1_WRITE_RESOURCE_PERMISSIONS = "write"; + + private final Principal principal; + private final String resourceType; + private final Set permissions; + + private SmartFhirScope( + Principal principal, String resourceType, Set resourcePermissions) { + this.principal = principal; + this.permissions = resourcePermissions; + this.resourceType = resourceType; + } + + static List extractSmartFhirScopesFromTokens(List tokens) { + List scopes = new ArrayList<>(); + for (String scope : tokens) { + if (VALID_SCOPE_PATTERN.matcher(scope).matches()) { + scopes.add(createSmartScope(scope)); + } + } + return scopes; + } + + private static SmartFhirScope createSmartScope(String scope) { + String[] split = scope.split("/"); + Preconditions.checkArgument(split.length == 2); + Principal principal = Principal.getPrincipal(split[0]); + String[] permissionSplit = split[1].split("\\."); + Preconditions.checkArgument(permissionSplit.length == 2); + if (!isValidResourceType(permissionSplit[0])) { + throw new IllegalArgumentException( + String.format("Invalid resource type %s", permissionSplit[0])); + } + String resourceType = permissionSplit[0]; + Set permissions = extractPermissions(permissionSplit[1]); + return new SmartFhirScope(principal, resourceType, permissions); + } + + private static boolean isValidResourceType(String resourceType) { + return ALL_RESOURCE_TYPES_WILDCARD.equals(resourceType) + || FhirUtil.isValidFhirResourceType(resourceType); + } + + private static Set extractPermissions(String permissionString) { + Set permissions = new HashSet<>(); + if (ALL_RESOURCE_PERMISSIONS_WILDCARD.equals(permissionString)) { + permissions.addAll(List.of(Permission.values())); + return permissions; + } + // We will support both v1 and v2 versions of the permissions: + // https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html#scopes-for-requesting-clinical-data + if (SMART_V1_READ_RESOURCE_PERMISSIONS.equals(permissionString)) { + permissions.add(Permission.READ); + permissions.add(Permission.SEARCH); + return permissions; + } + if (SMART_V1_WRITE_RESOURCE_PERMISSIONS.equals(permissionString)) { + permissions.add(Permission.CREATE); + permissions.add(Permission.UPDATE); + permissions.add(Permission.DELETE); + return permissions; + } + char[] permissionTokens = permissionString.toCharArray(); + int permissionTokensCounter = 0; + // SMART guidelines recommend enforcing order in the permissions string: + // https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html#scopes-for-requesting-clinical-data + for (Permission permission : Permission.values()) { + if (Permission.getPermission(permissionTokens[permissionTokensCounter]) == permission) { + permissionTokensCounter++; + permissions.add(permission); + } + if (permissionTokensCounter == permissionTokens.length) { + break; + } + } + if (permissionTokensCounter != permissionTokens.length) { + throw new IllegalArgumentException( + String.format("Invalid permission string %s", permissionString)); + } + return permissions; + } + + enum Principal { + USER, + PATIENT, + SYSTEM; + + static Principal getPrincipal(String principal) { + return Principal.valueOf(principal.toUpperCase()); + } + } + + /** + * SMART Permission to specify the kind of permission that is allowed on a Resource. The order of + * the Permission is important to us in the way it has been listed here. Please see: + * https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html#scopes-for-requesting-clinical-data + * The given order of Permissions is how it is expected that permissions will be specified in the + * token claim + */ + enum Permission { + CREATE, + READ, + UPDATE, + DELETE, + SEARCH; + + static Permission getPermission(char permissionCode) { + switch (permissionCode) { + case 'c': + return CREATE; + case 'r': + return READ; + case 'u': + return UPDATE; + case 'd': + return DELETE; + case 's': + return SEARCH; + default: + throw new IllegalArgumentException( + String.format("Invalid permission code. %s", permissionCode)); + } + } + } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/SmartScopeChecker.java b/src/main/java/com/google/fhir/gateway/plugin/SmartScopeChecker.java new file mode 100644 index 00000000..07b1e26b --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/SmartScopeChecker.java @@ -0,0 +1,50 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import com.google.fhir.gateway.plugin.SmartFhirScope.Permission; +import com.google.fhir.gateway.plugin.SmartFhirScope.Principal; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class SmartScopeChecker { + + private final Map> permissionsByResourceType; + + SmartScopeChecker(List scopes, Principal permissionContext) { + this.permissionsByResourceType = + scopes.stream() + .filter(smartFhirScope -> smartFhirScope.getPrincipal() == permissionContext) + .collect( + Collectors.groupingBy( + SmartFhirScope::getResourceType, + Collectors.flatMapping( + resourceScopes -> resourceScopes.getPermissions().stream(), + Collectors.toSet()))); + } + + boolean hasPermission(String resourceType, Permission permission) { + return this.permissionsByResourceType + .getOrDefault(resourceType, Collections.emptySet()) + .contains(permission) + || this.permissionsByResourceType + .getOrDefault(SmartFhirScope.ALL_RESOURCE_TYPES_WILDCARD, Collections.emptySet()) + .contains(permission); + } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java b/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java new file mode 100755 index 00000000..8a0aad9e --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java @@ -0,0 +1,478 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +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.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; +import com.google.fhir.gateway.interfaces.RequestMutation; +import com.google.gson.Gson; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; +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.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.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; + +public class SyncAccessDecision implements AccessDecision { + public static final String SYNC_FILTER_IGNORE_RESOURCES_FILE_ENV = + "SYNC_FILTER_IGNORE_RESOURCES_FILE"; + public static final String MATCHES_ANY_VALUE = "ANY_VALUE"; + private static final Logger logger = LoggerFactory.getLogger(SyncAccessDecision.class); + private static final int LENGTH_OF_SEARCH_PARAM_AND_EQUALS = 5; + private final String syncStrategy; + private final String applicationId; + private final boolean accessGranted; + private final List careTeamIds; + private final List locationIds; + private final List organizationIds; + private final List roles; + private IgnoredResourcesConfig config; + private String keycloakUUID; + private Gson gson = new Gson(); + private FhirContext fhirR4Context = FhirContext.forR4(); + private IParser fhirR4JsonParser = fhirR4Context.newJsonParser(); + private IGenericClient fhirR4Client; + + private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + + public SyncAccessDecision( + String keycloakUUID, + String applicationId, + boolean accessGranted, + List locationIds, + List careTeamIds, + List organizationIds, + String syncStrategy, + List roles) { + this.keycloakUUID = keycloakUUID; + this.applicationId = applicationId; + this.accessGranted = accessGranted; + this.careTeamIds = careTeamIds; + this.locationIds = locationIds; + this.organizationIds = organizationIds; + this.syncStrategy = syncStrategy; + this.config = getSkippedResourcesConfigs(); + this.roles = roles; + try { + setFhirR4Client( + fhirR4Context.newRestfulGenericClient( + System.getenv(PermissionAccessChecker.Factory.PROXY_TO_ENV))); + } catch (NullPointerException e) { + logger.error(e.getMessage()); + } + + this.practitionerDetailsEndpointHelper = new PractitionerDetailsEndpointHelper(fhirR4Client); + } + + @Override + public boolean canAccess() { + return accessGranted; + } + + @Override + public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsReader) { + + RequestMutation requestMutation = null; + if (isSyncUrl(requestDetailsReader)) { + 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(getSyncTags(locationIds, careTeamIds, organizationIds)); + requestMutation = + RequestMutation.builder() + .queryParams( + Map.of( + ProxyConstants.TAG_SEARCH_PARAM, + Arrays.asList(StringUtils.join(syncFilterParameterValues, ",")))) + .build(); + } + } + + return requestMutation; + } + + /** + * 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 syncTags + * @return the extra query Parameter values + */ + private List addSyncFilters(Map syncTags) { + List paramValues = new ArrayList<>(); + + for (var entry : syncTags.entrySet()) { + paramValues.add(PractitionerDetailsEndpointHelper.createSearchTagValues(entry)); + } + + 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; + 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; + + 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); + } + + if (includeAttributedPractitioners(request.getRequestPath())) { + Bundle practitionerDetailsBundle = + this.practitionerDetailsEndpointHelper.getSupervisorPractitionerDetailsByKeycloakId( + keycloakUUID); + resultContent = fhirR4JsonParser.encodeResourceToString(practitionerDetailsBundle); + } + + return resultContent; + } + + private boolean includeAttributedPractitioners(String requestPath) { + return Constants.SYNC_STRATEGY_LOCATION.equalsIgnoreCase(syncStrategy) + && roles.contains(Constants.ROLE_SUPERVISOR) + && Constants.ENDPOINT_PRACTITIONER_DETAILS.equals(requestPath); + } + + @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 responseResource FHIR Resource result returned byt the HTTPResponse + * @return String content of the result Bundle + */ + private Bundle postProcessModeListEntries(IBaseResource responseResource) { + + Bundle requestBundle = null; + + if (responseResource instanceof ListResource && ((ListResource) responseResource).hasEntry()) { + + requestBundle = processListEntriesGatewayModeByListResource((ListResource) responseResource); + + } else if (responseResource instanceof Bundle) { + + requestBundle = processListEntriesGatewayModeByBundle(responseResource); + } + + return fhirR4Client.transaction().withBundle(requestBundle).execute(); + } + + /** + * Generates a map of Code.url to multiple Code.Value which contains all the possible filters that + * will be used in syncing + * + * @param locationIds + * @param careTeamIds + * @param organizationIds + * @return Pair of URL to [Code.url, [Code.Value]] map. The URL is complete url + */ + private Map getSyncTags( + List locationIds, List careTeamIds, List organizationIds) { + StringBuilder sb = new StringBuilder(); + Map map = new HashMap<>(); + + sb.append(ProxyConstants.TAG_SEARCH_PARAM); + sb.append(ProxyConstants.Literals.EQUALS); + + addTags(ProxyConstants.LOCATION_TAG_URL, locationIds, map, sb); + addTags(ProxyConstants.ORGANISATION_TAG_URL, organizationIds, map, sb); + addTags(ProxyConstants.CARE_TEAM_TAG_URL, careTeamIds, map, sb); + + return map; + } + + private void addTags( + String tagUrl, + List values, + Map map, + StringBuilder urlStringBuilder) { + int len = values.size(); + if (len > 0) { + if (urlStringBuilder.length() + != (ProxyConstants.TAG_SEARCH_PARAM + ProxyConstants.Literals.EQUALS).length()) { + urlStringBuilder.append(ProxyConstants.PARAM_VALUES_SEPARATOR); + } + + map.put(tagUrl, values.toArray(new String[0])); + + int i = 0; + for (String tagValue : values) { + urlStringBuilder.append(tagUrl); + urlStringBuilder.append(ProxyConstants.CODE_URL_VALUE_SEPARATOR); + urlStringBuilder.append(tagValue); + + if (i != len - 1) { + urlStringBuilder.append(ProxyConstants.PARAM_VALUES_SEPARATOR); + } + i++; + } + } + } + + private boolean isSyncUrl(RequestDetailsReader requestDetailsReader) { + if (requestDetailsReader.getRequestType() == RequestTypeEnum.GET + && !TextUtils.isEmpty(requestDetailsReader.getResourceName())) { + String requestPath = requestDetailsReader.getRequestPath(); + return isResourceTypeRequest( + requestPath.replace(requestDetailsReader.getFhirServerBase(), "")); + } + + return false; + } + + private boolean isResourceTypeRequest(String requestPath) { + if (!TextUtils.isEmpty(requestPath)) { + String[] sections = requestPath.split(ProxyConstants.HTTP_URL_SEPARATOR); + + return sections.length == 1 || (sections.length == 2 && TextUtils.isEmpty(sections[1])); + } + + return false; + } + + @VisibleForTesting + protected IgnoredResourcesConfig getIgnoredResourcesConfigFileConfiguration(String configFile) { + if (configFile != null && !configFile.isEmpty()) { + try { + 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!"); + } + for (IgnoredResourcesConfig entry : config.entries) { + if (entry.getPath() == null) { + throw new IllegalArgumentException("Allow-list entries should have a path."); + } + } + + } catch (IOException e) { + logger.error("IO error while reading sync-filter skip-list config file {}", configFile); + } + } + + return config; + } + + @VisibleForTesting + protected IgnoredResourcesConfig getSkippedResourcesConfigs() { + return getIgnoredResourcesConfigFileConfiguration( + System.getenv(SYNC_FILTER_IGNORE_RESOURCES_FILE_ENV)); + } + + /** + * This method checks the request to ensure the path, request type and parameters match values in + * the hapi_sync_filter_ignored_queries configuration + */ + private boolean shouldSkipDataFiltering(RequestDetailsReader requestDetailsReader) { + if (config == null) return false; + + for (IgnoredResourcesConfig entry : config.entries) { + + if (!entry.getPath().equals(requestDetailsReader.getRequestPath())) { + continue; + } + + if (entry.getMethodType() != null + && !entry.getMethodType().equals(requestDetailsReader.getRequestType().name())) { + continue; + } + + for (Map.Entry expectedParam : entry.getQueryParams().entrySet()) { + String[] actualQueryValue = + requestDetailsReader.getParameters().get(expectedParam.getKey()); + + if (actualQueryValue == null) { + return true; + } + + if (MATCHES_ANY_VALUE.equals(expectedParam.getValue())) { + return true; + } else { + if (actualQueryValue.length != 1) { + // We currently do not support multivalued query params in skip-lists. + return false; + } + + if (expectedParam.getValue() instanceof List) { + return CollectionUtils.isEqualCollection( + (List) expectedParam.getValue(), Arrays.asList(actualQueryValue[0].split(","))); + + } else if (actualQueryValue[0].equals(expectedParam.getValue())) { + return true; + } + } + } + } + return false; + } + + @VisibleForTesting + protected void setSkippedResourcesConfig(IgnoredResourcesConfig config) { + this.config = config; + } + + @VisibleForTesting + protected void setFhirR4Context(FhirContext fhirR4Context) { + this.fhirR4Context = fhirR4Context; + } + + @VisibleForTesting + public void setFhirR4Client(IGenericClient fhirR4Client) { + this.fhirR4Client = fhirR4Client; + } + + class IgnoredResourcesConfig { + @Getter List entries; + @Getter private String path; + @Getter private String methodType; + @Getter private Map queryParams; + + @Override + public String toString() { + return "SkippedFilesConfig{" + + methodType + + " path=" + + path + + " fhirResources=" + + Arrays.toString(queryParams.entrySet().toArray()) + + '}'; + } + } + + public static final class Constants { + public static final String FHIR_GATEWAY_MODE = "fhir-gateway-mode"; + public static final String LIST_ENTRIES = "list-entries"; + public static final String ROLE_SUPERVISOR = "SUPERVISOR"; + public static final String ENDPOINT_PRACTITIONER_DETAILS = "practitioner-details"; + public static final String SYNC_STRATEGY_LOCATION = "Location"; + } +} diff --git a/src/main/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java b/src/main/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java deleted file mode 100644 index 196f9afe..00000000 --- a/src/main/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2021-2023 Ona Systems, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.proxy.plugin; - -import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import com.google.fhir.proxy.ProxyConstants; -import com.google.fhir.proxy.interfaces.AccessDecision; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; -import org.apache.http.HttpResponse; -import org.apache.http.util.TextUtils; - -public class OpenSRPSyncAccessDecision implements AccessDecision { - - private String applicationId; - - private final List syncStrategy; - - private boolean accessGranted; - - private List careTeamIds; - - private List locationIds; - - private List organizationIds; - - public OpenSRPSyncAccessDecision( - String applicationId, - boolean accessGranted, - List locationIds, - List careTeamIds, - List organizationIds, - List syncStrategy) { - this.applicationId = applicationId; - this.accessGranted = accessGranted; - this.careTeamIds = careTeamIds; - this.locationIds = locationIds; - this.organizationIds = organizationIds; - this.syncStrategy = syncStrategy; - } - - @Override - public boolean canAccess() { - return accessGranted; - } - - @Override - public void preProcess(ServletRequestDetails servletRequestDetails) { - // 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(servletRequestDetails)) { - // 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"); - } - addSyncFilters(servletRequestDetails, getSyncTags(locationIds, careTeamIds, organizationIds)); - } - } - - /** - * Adds filters to the {@link ServletRequestDetails} for the _tag property to allow filtering by - * specific code-url-values that match specific locations, teams or organisations - * - * @param servletRequestDetails - * @param syncTags - */ - private void addSyncFilters( - ServletRequestDetails servletRequestDetails, Pair> syncTags) { - List paramValues = new ArrayList<>(); - - for (Map.Entry codeUrlValuesMap : syncTags.getValue().entrySet()) { - String codeUrl = codeUrlValuesMap.getKey(); - for (String codeValue : codeUrlValuesMap.getValue()) { - StringBuilder paramValueSb = new StringBuilder(codeUrl.length() + codeValue.length() + 2); - paramValueSb.append(codeUrl); - paramValueSb.append(ProxyConstants.CODE_URL_VALUE_SEPARATOR); - paramValueSb.append(codeValue); - paramValues.add(paramValueSb.toString()); - } - } - - String[] prevTagFilters = - servletRequestDetails.getParameters().get(ProxyConstants.SEARCH_PARAM_TAG); - if (prevTagFilters != null && prevTagFilters.length > 1) { - Collections.addAll(paramValues, prevTagFilters); - } - - servletRequestDetails.addParameter( - ProxyConstants.SEARCH_PARAM_TAG, paramValues.toArray(new String[0])); - } - - @Override - public String postProcess(HttpResponse response) throws IOException { - return null; - } - - /** - * Generates a map of Code.url to multiple Code.Value which contains all the possible filters that - * will be used in syncing - * - * @param locationIds - * @param careTeamIds - * @param organizationIds - * @return Pair of URL to [Code.url, [Code.Value]] map. The URL is complete url - */ - private Pair> getSyncTags( - List locationIds, List careTeamIds, List organizationIds) { - StringBuilder sb = new StringBuilder(); - Map map = new HashMap<>(); - - sb.append(ProxyConstants.SEARCH_PARAM_TAG); - sb.append(ProxyConstants.Literals.EQUALS); - - addTags(ProxyConstants.LOCATION_TAG_URL, locationIds, map, sb); - addTags(ProxyConstants.ORGANISATION_TAG_URL, organizationIds, map, sb); - addTags(ProxyConstants.CARE_TEAM_TAG_URL, careTeamIds, map, sb); - - return new ImmutablePair<>(sb.toString(), map); - } - - private void addTags( - String tagUrl, - List values, - Map map, - StringBuilder urlStringBuilder) { - int len = values.size(); - if (len > 0) { - if (urlStringBuilder.length() - != (ProxyConstants.SEARCH_PARAM_TAG + ProxyConstants.Literals.EQUALS).length()) { - urlStringBuilder.append(ProxyConstants.PARAM_VALUES_SEPARATOR); - } - - map.put(tagUrl, values.toArray(new String[0])); - - int i = 0; - for (String tagValue : values) { - urlStringBuilder.append(tagUrl); - urlStringBuilder.append(ProxyConstants.CODE_URL_VALUE_SEPARATOR); - urlStringBuilder.append(tagValue); - - if (i != len - 1) { - urlStringBuilder.append(ProxyConstants.PARAM_VALUES_SEPARATOR); - } - i++; - } - } - } - - private boolean isSyncUrl(ServletRequestDetails servletRequestDetails) { - if (servletRequestDetails.getRequestType() == RequestTypeEnum.GET - && !TextUtils.isEmpty(servletRequestDetails.getResourceName())) { - String requestPath = servletRequestDetails.getRequestPath(); - return isResourceTypeRequest( - requestPath.replace(servletRequestDetails.getFhirServerBase(), "")); - } - - return false; - } - - private boolean isResourceTypeRequest(String requestPath) { - if (!TextUtils.isEmpty(requestPath)) { - String[] sections = requestPath.split("/"); - - return sections.length == 1 || (sections.length == 2 && TextUtils.isEmpty(sections[1])); - } - - return false; - } -} diff --git a/src/test/java/org/smartregister/fhir/proxy/plugin/AccessCheckerTestBase.java b/src/test/java/com/google/fhir/gateway/plugin/AccessCheckerTestBase.java similarity index 91% rename from src/test/java/org/smartregister/fhir/proxy/plugin/AccessCheckerTestBase.java rename to src/test/java/com/google/fhir/gateway/plugin/AccessCheckerTestBase.java index ac2cbc7c..05bc1716 100644 --- a/src/test/java/org/smartregister/fhir/proxy/plugin/AccessCheckerTestBase.java +++ b/src/test/java/com/google/fhir/gateway/plugin/AccessCheckerTestBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 Ona Systems, Inc + * Copyright ${license.git.copyrightYears} Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.fhir.proxy.plugin; +package com.google.fhir.gateway.plugin; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -28,8 +28,8 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.google.common.collect.Maps; import com.google.common.io.Resources; -import com.google.fhir.proxy.interfaces.AccessChecker; -import com.google.fhir.proxy.interfaces.RequestDetailsReader; +import com.google.fhir.gateway.interfaces.AccessChecker; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; import java.io.IOException; import java.net.URL; import java.util.Map; @@ -75,6 +75,7 @@ public void createTest() { public void canAccessTest() { // Query: GET /Patient/PATIENT_AUTHORIZED_ID when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); when(requestMock.getId()).thenReturn(PATIENT_AUTHORIZED_ID); AccessChecker testInstance = getInstance(); assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); @@ -84,6 +85,7 @@ public void canAccessTest() { public void canAccessNotAuthorized() { // Query: GET /Patient/PATIENT_NON_AUTHORIZED_ID when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); when(requestMock.getId()).thenReturn(PATIENT_NON_AUTHORIZED_ID); AccessChecker testInstance = getInstance(); assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); @@ -93,6 +95,7 @@ public void canAccessNotAuthorized() { public void canAccessDirectResourceNotAuthorized() { // Query: GET /Observation/a-random-id when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); when(requestMock.getId()).thenReturn(new IdDt("a-random-id")); AccessChecker testInstance = getInstance(); testInstance.checkAccess(requestMock).canAccess(); @@ -102,6 +105,7 @@ public void canAccessDirectResourceNotAuthorized() { public void canAccessDirectResourceWithParamNotAuthorized() { // Query: GET /Observation/a-random-id?subject=PATIENT_AUTHORIZED when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); when(requestMock.getId()).thenReturn(new IdDt("a-random-id")); // This is to make sure that the presence of patient search params does not make any difference. Map params = Maps.newHashMap(); @@ -115,6 +119,7 @@ public void canAccessDirectResourceWithParamNotAuthorized() { public void canAccessSearchQuery() { // Query: GET /Observation?subject=PATIENT_AUTHORIZED when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); Map params = Maps.newHashMap(); params.put("subject", new String[] {PATIENT_AUTHORIZED}); when(requestMock.getParameters()).thenReturn(params); @@ -126,6 +131,7 @@ public void canAccessSearchQuery() { public void canAccessSearchQueryNotAuthorized() { // Query: GET /Observation?subject=PATIENT_AUTHORIZED when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); AccessChecker testInstance = getInstance(); testInstance.checkAccess(requestMock).canAccess(); } @@ -218,6 +224,7 @@ public void canAccessPostObservation() throws IOException { byte[] obsBytes = Resources.toByteArray(listUrl); when(requestMock.loadRequestContents()).thenReturn(obsBytes); when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.POST); + AccessChecker testInstance = getInstance(); assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); } @@ -292,12 +299,22 @@ public void canAccessBundleNonPatientResourcesNoPatientRefUnauthorized() throws testInstance.checkAccess(requestMock).canAccess(); } - @Test(expected = InvalidRequestException.class) - public void canAccessBundleDeletePatient() throws IOException { + @Test + public void canAccessBundleDeleteNonPatient() throws IOException { // Query: POST / -d @bundle_transaction_delete.json - setUpFhirBundle("bundle_transaction_delete.json"); + setUpFhirBundle("bundle_transaction_delete_non_patient.json"); AccessChecker testInstance = getInstance(); - testInstance.checkAccess(requestMock).canAccess(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundleDeletePatientUnAuthorized() throws IOException { + // Query: POST / -d @bundle_transaction_delete_patient_unauthorized.json + setUpFhirBundle("bundle_transaction_delete_patient_unauthorized.json"); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); } @Test(expected = InvalidRequestException.class) @@ -325,6 +342,7 @@ public void canAccessBundlePatchNoBinaryResourceUnauthorized() throws IOExceptio @Test(expected = InvalidRequestException.class) public void canAccessSearchChaining() { when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); Map params = Maps.newHashMap(); params.put("subject:Patient.name", new String[] {"random-name"}); when(requestMock.getParameters()).thenReturn(params); @@ -335,6 +353,7 @@ public void canAccessSearchChaining() { @Test(expected = InvalidRequestException.class) public void canAccessSearchReverseChaining() { when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); Map params = Maps.newHashMap(); params.put("subject", new String[] {PATIENT_AUTHORIZED}); params.put("_has", new String[] {"something"}); @@ -346,6 +365,7 @@ public void canAccessSearchReverseChaining() { @Test(expected = InvalidRequestException.class) public void canAccessSearchInclude() { when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); Map params = Maps.newHashMap(); params.put("subject", new String[] {PATIENT_AUTHORIZED}); params.put("_include", new String[] {"something"}); @@ -357,6 +377,7 @@ public void canAccessSearchInclude() { @Test(expected = InvalidRequestException.class) public void canAccessSearchRevinclude() { when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); Map params = Maps.newHashMap(); params.put("subject", new String[] {PATIENT_AUTHORIZED}); params.put("_revinclude", new String[] {"something"}); diff --git a/src/test/java/org/smartregister/fhir/proxy/plugin/AccessGrantedAndUpdateListTest.java b/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java similarity index 82% rename from src/test/java/org/smartregister/fhir/proxy/plugin/AccessGrantedAndUpdateListTest.java rename to src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java index 2272ab45..7f47e0a0 100644 --- a/src/test/java/org/smartregister/fhir/proxy/plugin/AccessGrantedAndUpdateListTest.java +++ b/src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright ${license.git.copyrightYears} Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.fhir.proxy.plugin; +package com.google.fhir.gateway.plugin; import ca.uhn.fhir.context.FhirContext; import com.google.common.io.Resources; -import com.google.fhir.proxy.HttpFhirClient; +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/src/test/java/com/google/fhir/gateway/plugin/ListAccessCheckerTest.java b/src/test/java/com/google/fhir/gateway/plugin/ListAccessCheckerTest.java new file mode 100644 index 00000000..b612f77b --- /dev/null +++ b/src/test/java/com/google/fhir/gateway/plugin/ListAccessCheckerTest.java @@ -0,0 +1,328 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.when; + +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.google.common.io.Resources; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.PatientFinderImp; +import com.google.fhir.gateway.interfaces.AccessChecker; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import org.apache.http.HttpResponse; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class ListAccessCheckerTest extends AccessCheckerTestBase { + + private static final String TEST_LIST_ID = "test-list"; + private static final String PATIENT_IN_BUNDLE_1 = "420e791b-e419-c19b-3144-29e101c2c12f"; + private static final String PATIENT_IN_BUNDLE_2 = "db6e42c7-04fc-4d9d-b394-9ff33a41e178"; + + @Mock private HttpFhirClient httpFhirClientMock; + + private void setUpFhirListSearchMock(String itemParam, String resourceFileToReturn) + throws IOException { + URL listUrl = Resources.getResource(resourceFileToReturn); + String testListJson = Resources.toString(listUrl, StandardCharsets.UTF_8); + HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); + when(httpFhirClientMock.getResource( + String.format("/List?_id=%s&_elements=id&%s", TEST_LIST_ID, itemParam))) + .thenReturn(fhirResponseMock); + TestUtil.setUpFhirResponseMock(fhirResponseMock, testListJson); + } + + private void setUpPatientSearchMock(String patientParam, String resourceFileToReturn) + throws IOException { + URL listUrl = Resources.getResource(resourceFileToReturn); + String testListJson = Resources.toString(listUrl, StandardCharsets.UTF_8); + HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); + doReturn(fhirResponseMock) + .when(httpFhirClientMock) + .getResource(String.format("/Patient?_id=%s&_elements=id", patientParam)); + TestUtil.setUpFhirResponseMock(fhirResponseMock, testListJson); + } + + @Before + public void setUp() throws IOException { + when(jwtMock.getClaim(ListAccessChecker.Factory.PATIENT_LIST_CLAIM)).thenReturn(claimMock); + when(claimMock.asString()).thenReturn(TEST_LIST_ID); + setUpFhirListSearchMock( + "item=Patient%2F" + PATIENT_AUTHORIZED, "bundle_list_patient_item.json"); + setUpFhirListSearchMock("item=Patient%2F" + PATIENT_NON_AUTHORIZED, "bundle_empty.json"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.GET); + } + + @Override + protected AccessChecker getInstance() { + return new ListAccessChecker.Factory() + .create( + jwtMock, httpFhirClientMock, fhirContext, PatientFinderImp.getInstance(fhirContext)); + } + + @Test + public void canAccessList() { + when(requestMock.getResourceName()).thenReturn("List"); + when(requestMock.getId()).thenReturn(new IdDt("List", TEST_LIST_ID)); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessListNotAuthorized() throws IOException { + when(requestMock.getResourceName()).thenReturn("List"); + when(requestMock.getId()).thenReturn(new IdDt("List", "wrong-id")); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + @Override + public void canAccessPostObservationWithPerformer() throws IOException { + setUpFhirListSearchMock( + String.format( + "item=Patient%%2Ftest-patient-2%%2CPatient%%2Ftest-patient-1%%2CPatient%%2F%s", + PATIENT_AUTHORIZED), + "bundle_list_patient_item.json"); + super.canAccessPostObservationWithPerformer(); + } + + @Test + public void canAccessPostPatient() { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.POST); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessPutExistingPatient() throws IOException { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PUT); + when(requestMock.getId()).thenReturn(PATIENT_AUTHORIZED_ID); + URL url = Resources.getResource("patient_id_search_single.json"); + String testJson = Resources.toString(url, StandardCharsets.UTF_8); + HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); + TestUtil.setUpFhirResponseMock(fhirResponseMock, testJson); + when(httpFhirClientMock.getResource( + String.format("/Patient?_id=%s&_elements=id", PATIENT_AUTHORIZED))) + .thenReturn(fhirResponseMock); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessPutNewPatient() throws IOException { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PUT); + when(requestMock.getId()).thenReturn(PATIENT_AUTHORIZED_ID); + URL url = Resources.getResource("bundle_empty.json"); + String testJson = Resources.toString(url, StandardCharsets.UTF_8); + HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); + TestUtil.setUpFhirResponseMock(fhirResponseMock, testJson); + when(httpFhirClientMock.getResource( + String.format("/Patient?_id=%s&_elements=id", PATIENT_AUTHORIZED))) + .thenReturn(fhirResponseMock); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundleGetNonPatientUnAuthorized() throws IOException { + setUpFhirBundle("bundle_transaction_get_non_patient_unauthorized.json"); + setUpFhirListSearchMock( + "item=Patient%2Fdb6e42c7-04fc-4d9d-b394-9ff33a41e178", "bundle_empty.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test(expected = InvalidRequestException.class) + public void canAccessBundleGetPatientNonAuthorized() throws IOException { + setUpFhirBundle("bundle_transaction_get_patient_unauthorized.json"); + setUpFhirListSearchMock( + "item=Patient%2Fdb6e42c7-04fc-4d9d-b394-9ff33a41e178", "bundle_empty.json"); + AccessChecker testInstance = getInstance(); + testInstance.checkAccess(requestMock).canAccess(); + } + + @Test + public void canAccessBundlePutExistingPatient() throws IOException { + setUpFhirBundle("bundle_transaction_put_patient.json"); + setUpPatientSearchMock(PATIENT_AUTHORIZED, "patient_id_search_single.json"); + setUpPatientSearchMock(PATIENT_IN_BUNDLE_1, "patient_id_search_single.json"); + setUpFhirListSearchMock( + String.format( + "item=Patient%%2F%s&item=Patient%%2F%s", PATIENT_IN_BUNDLE_1, PATIENT_AUTHORIZED), + "bundle_list_patient_item.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundlePutNewPatient() throws IOException { + setUpFhirBundle("bundle_transaction_put_patient.json"); + setUpPatientSearchMock(PATIENT_AUTHORIZED, "bundle_empty.json"); + setUpPatientSearchMock(PATIENT_IN_BUNDLE_1, "patient_id_search_single.json"); + setUpFhirListSearchMock( + String.format("item=Patient%%2F%s", PATIENT_IN_BUNDLE_1), "bundle_list_patient_item.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundlePutExistingPatientUnauthorized() throws IOException { + setUpFhirBundle("bundle_transaction_put_unauthorized.json"); + setUpFhirListSearchMock( + String.format("item=Patient%%2F%s", PATIENT_NON_AUTHORIZED), "bundle_empty.json"); + setUpPatientSearchMock(PATIENT_NON_AUTHORIZED, "bundle_list_patient_item.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test(expected = InvalidRequestException.class) + public void canAccessBundleNonPatientResourcesUnauthorized() throws IOException { + setUpFhirBundle("bundle_transaction_no_patient_in_url.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundleNonPatientResourcesAndNewPatient() throws IOException { + setUpFhirBundle("bundle_transaction_patient_and_non_patients.json"); + setUpFhirListSearchMock( + String.format( + "item=Patient%%2F%s&item=Patient%%2F%s%%2CPatient%%2F%s", + PATIENT_IN_BUNDLE_1, PATIENT_IN_BUNDLE_1, PATIENT_AUTHORIZED), + "bundle_list_patient_item.json"); + setUpPatientSearchMock(PATIENT_IN_BUNDLE_2, "bundle_empty.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundlePatchUnauthorized() throws IOException { + setUpFhirBundle("bundle_transaction_patch_unauthorized.json"); + setUpPatientSearchMock(PATIENT_AUTHORIZED, "bundle_list_patient_item.json"); + setUpFhirListSearchMock( + String.format( + "item=Patient%%2Fmichael%%2CPatient%%2Fbob&item=Patient%%2F%s&item=Patient%%2F%s", + PATIENT_IN_BUNDLE_1, PATIENT_AUTHORIZED), + "bundle_empty.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundlePostPatient() throws IOException { + setUpFhirBundle("bundle_transaction_post_patient.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundleDeletePatient() throws IOException { + // Query: POST / -d @bundle_transaction_delete_patient.json + setUpFhirBundle("bundle_transaction_delete_patient.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessPatchObservationUnauthorizedPatient() throws IOException { + // Query: PATCH /Observation?subject=Patient/PATIENT_AUTHORIZED -d \ + // @test_obs_patch_unauthorized_patient.json + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {"be92a43f-de46-affa-b131-bbf9eea51140"})); + URL listUrl = Resources.getResource("test_obs_patch_unauthorized_patient.json"); + byte[] obsBytes = Resources.toByteArray(listUrl); + when(requestMock.loadRequestContents()).thenReturn(obsBytes); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PATCH); + AccessChecker testInstance = getInstance(); + setUpFhirListSearchMock( + "item=Patient%2Fmichael%2CPatient%2Fbob&item=Patient%2F" + PATIENT_AUTHORIZED, + "bundle_empty.json"); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessDeletePatient() throws IOException { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getId()).thenReturn(PATIENT_AUTHORIZED_ID); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessDeletePatientUnauthorized() throws IOException { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getId()).thenReturn(PATIENT_NON_AUTHORIZED_ID); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessDeleteAccessListUnauthorized() throws IOException { + when(requestMock.getResourceName()).thenReturn("List"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getId()).thenReturn(new IdDt("List", TEST_LIST_ID)); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessDeleteObservation() throws IOException { + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {PATIENT_AUTHORIZED})); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessDeleteObservationUnauthorized() throws IOException { + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {PATIENT_NON_AUTHORIZED})); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + // TODO add an Appointment POST +} diff --git a/src/test/java/com/google/fhir/gateway/plugin/PatientAccessCheckerTest.java b/src/test/java/com/google/fhir/gateway/plugin/PatientAccessCheckerTest.java new file mode 100644 index 00000000..97d1fb8d --- /dev/null +++ b/src/test/java/com/google/fhir/gateway/plugin/PatientAccessCheckerTest.java @@ -0,0 +1,219 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Mockito.when; + +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.auth0.jwt.interfaces.Claim; +import com.google.common.io.Resources; +import com.google.fhir.gateway.PatientFinderImp; +import com.google.fhir.gateway.interfaces.AccessChecker; +import java.io.IOException; +import java.net.URL; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PatientAccessCheckerTest extends AccessCheckerTestBase { + + @Mock protected Claim scopeClaimMock; + + static final String DEFAULT_TEST_SCOPES_CLAIM = "patient/*.*"; + + @Before + public void setUp() throws IOException { + when(jwtMock.getClaim(PatientAccessChecker.Factory.PATIENT_CLAIM)).thenReturn(claimMock); + when(jwtMock.getClaim(PatientAccessChecker.Factory.SCOPES_CLAIM)).thenReturn(scopeClaimMock); + when(claimMock.asString()).thenReturn(PATIENT_AUTHORIZED); + when(scopeClaimMock.asString()).thenReturn(DEFAULT_TEST_SCOPES_CLAIM); + } + + @Override + protected AccessChecker getInstance() { + return new PatientAccessChecker.Factory() + .create(jwtMock, null, fhirContext, PatientFinderImp.getInstance(fhirContext)); + } + + @Test + public void canAccessPostPatient() { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.POST); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessPutPatient() { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PUT); + when(requestMock.getId()).thenReturn(PATIENT_AUTHORIZED_ID); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessPutUnauthorizedPatient() { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PUT); + when(requestMock.getId()).thenReturn(PATIENT_NON_AUTHORIZED_ID); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundleGetNonPatientUnauthorized() throws IOException { + setUpFhirBundle("bundle_transaction_get_non_patient_unauthorized.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundlePostPatientUnAuthorized() throws IOException { + setUpFhirBundle("bundle_transaction_post_patient.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundleDeletePatientUnAuthorized() throws IOException { + // Query: POST / -d @bundle_transaction_delete_patient.json + setUpFhirBundle("bundle_transaction_delete_patient.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessPatchObservationUnauthorizedPatient() throws IOException { + // Query: PATCH /Observation?subject=Patient/PATIENT_AUTHORIZED -d \ + // @test_obs_patch_unauthorized_patient.json + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {"be92a43f-de46-affa-b131-bbf9eea51140"})); + URL listUrl = Resources.getResource("test_obs_patch_unauthorized_patient.json"); + byte[] obsBytes = Resources.toByteArray(listUrl); + when(requestMock.loadRequestContents()).thenReturn(obsBytes); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PATCH); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessDeletePatientUnauthorized() { + when(requestMock.getResourceName()).thenReturn("Patient"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + AccessChecker testInstance = getInstance(); + + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessDeleteObservationAuthorized() { + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {PATIENT_AUTHORIZED})); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessPatchObservationNoValidPermissionForPatient() throws IOException { + // Query: PATCH /Observation?subject=Patient/PATIENT_AUTHORIZED -d @test_obs_patch.json + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {"be92a43f-de46-affa-b131-bbf9eea51140"})); + URL listUrl = Resources.getResource("test_obs_patch.json"); + byte[] obsBytes = Resources.toByteArray(listUrl); + when(requestMock.loadRequestContents()).thenReturn(obsBytes); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PATCH); + when(scopeClaimMock.asString()).thenReturn("patient/Patient.write patient/Observation.read"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test(expected = InvalidRequestException.class) + public void canAccessPutObservationInvalidPermissionScope() { + // Query: PUT /Observation -d @test_obs_unauthorized.json + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.PUT); + when(scopeClaimMock.asString()).thenReturn("patient/Observation.invalid"); + AccessChecker testInstance = getInstance(); + testInstance.checkAccess(requestMock).canAccess(); + } + + @Test + public void canAccessBundlePutPatient() throws IOException { + setUpFhirBundle("bundle_transaction_put_authorized_patient.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessDeleteObservationUnauthorized() { + when(requestMock.getResourceName()).thenReturn("Observation"); + when(requestMock.getRequestType()).thenReturn(RequestTypeEnum.DELETE); + when(requestMock.getParameters()) + .thenReturn(Map.of("subject", new String[] {PATIENT_NON_AUTHORIZED})); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundlePutPatientNoValidPermission() throws IOException { + setUpFhirBundle("bundle_transaction_put_authorized_patient.json"); + when(scopeClaimMock.asString()).thenReturn("patient/Observation.*"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundlePatchResources() throws IOException { + setUpFhirBundle("bundle_transaction_patch_authorized.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundlePatchResourcesNoValidPermission() throws IOException { + setUpFhirBundle("bundle_transaction_patch_authorized.json"); + when(scopeClaimMock.asString()).thenReturn("patient/Observation.*"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } + + @Test + public void canAccessBundleSearchResources() throws IOException { + setUpFhirBundle("bundle_transaction_get_non_patient_authorized.json"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(true)); + } + + @Test + public void canAccessBundleSearchResourcesNoValidPermission() throws IOException { + setUpFhirBundle("bundle_transaction_get_non_patient_authorized.json"); + when(scopeClaimMock.asString()).thenReturn("patient/Observation.*"); + AccessChecker testInstance = getInstance(); + assertThat(testInstance.checkAccess(requestMock).canAccess(), equalTo(false)); + } +} diff --git a/src/test/java/org/smartregister/fhir/proxy/plugin/PermissionAccessCheckerTest.java b/src/test/java/com/google/fhir/gateway/plugin/PermissionAccessCheckerTest.java old mode 100644 new mode 100755 similarity index 98% rename from src/test/java/org/smartregister/fhir/proxy/plugin/PermissionAccessCheckerTest.java rename to src/test/java/com/google/fhir/gateway/plugin/PermissionAccessCheckerTest.java index 2e12bf77..2d0e921c --- a/src/test/java/org/smartregister/fhir/proxy/plugin/PermissionAccessCheckerTest.java +++ b/src/test/java/com/google/fhir/gateway/plugin/PermissionAccessCheckerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright ${license.git.copyrightYears} Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.fhir.proxy.plugin; +package com.google.fhir.gateway.plugin; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; @@ -25,9 +25,9 @@ import com.auth0.jwt.interfaces.Claim; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.common.io.Resources; -import com.google.fhir.proxy.PatientFinderImp; -import com.google.fhir.proxy.interfaces.AccessChecker; -import com.google.fhir.proxy.interfaces.RequestDetailsReader; +import com.google.fhir.gateway.PatientFinderImp; +import com.google.fhir.gateway.interfaces.AccessChecker; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; import java.io.IOException; import java.net.URL; import java.util.Arrays; diff --git a/src/test/java/com/google/fhir/gateway/plugin/SmartScopeCheckerTest.java b/src/test/java/com/google/fhir/gateway/plugin/SmartScopeCheckerTest.java new file mode 100644 index 00000000..ed214082 --- /dev/null +++ b/src/test/java/com/google/fhir/gateway/plugin/SmartScopeCheckerTest.java @@ -0,0 +1,106 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +import com.google.fhir.gateway.plugin.SmartFhirScope.Permission; +import com.google.fhir.gateway.plugin.SmartFhirScope.Principal; +import java.util.List; +import org.hl7.fhir.r4.model.ResourceType; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SmartScopeCheckerTest { + + @Test + public void hasPermissionCreateObservationPatientPrincipal() { + SmartScopeChecker scopeChecker = + new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens( + List.of( + "user/Encounter.read", + "patient/Observation.read", + "patient/Observation.write")), + Principal.PATIENT); + assertThat( + scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.CREATE), + equalTo(true)); + } + + @Test + public void hasPermissionCreateObservationPatientPrincipalNoValidScope() { + SmartScopeChecker scopeChecker = + new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens( + List.of("user/Observation.create", "patient/Observation.read")), + Principal.PATIENT); + assertThat( + scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.CREATE), + equalTo(false)); + } + + @Test + public void hasPermissionReadObservationPatientPrincipalAllResources() { + SmartScopeChecker scopeChecker = + new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens( + List.of("user/*.read", "patient/Observation.write")), + Principal.PATIENT); + assertThat( + scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.READ), + equalTo(false)); + } + + @Test + public void hasPermissionDeleteObservationPatientPrincipalAllResources() { + SmartScopeChecker scopeChecker = + new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens( + List.of("patient/*.read", "patient/Observation.write")), + Principal.PATIENT); + assertThat( + scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.DELETE), + equalTo(true)); + } + + @Test + public void hasPermissionCreateObservationV2Scopes() { + SmartScopeChecker scopeChecker = + new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens( + List.of("patient/*.rs", "patient/Observation.u")), + Principal.PATIENT); + assertThat( + scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.CREATE), + equalTo(false)); + } + + @Test + public void hasPermissionDeleteObservationV2Scopes() { + SmartScopeChecker scopeChecker = + new SmartScopeChecker( + SmartFhirScope.extractSmartFhirScopesFromTokens( + List.of("user/*.rs", "user/Observation.cr")), + Principal.PATIENT); + assertThat( + scopeChecker.hasPermission(ResourceType.Observation.name(), Permission.DELETE), + equalTo(false)); + } +} diff --git a/src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java b/src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java new file mode 100755 index 00000000..566fd98e --- /dev/null +++ b/src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java @@ -0,0 +1,565 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +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.api.server.RequestDetails; +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.ProxyConstants; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import com.google.fhir.gateway.interfaces.RequestMutation; +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.hl7.fhir.r4.model.ListResource; +import org.junit.After; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Answers; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SyncAccessDecisionTest { + + private List locationIds = new ArrayList<>(); + + private List careTeamIds = new ArrayList<>(); + + private List organisationIds = new ArrayList<>(); + + private List userRoles = new ArrayList<>(); + + private SyncAccessDecision testInstance; + + @Test + public void + preprocessShouldAddAllFiltersWhenIdsForLocationsOrganisationsAndCareTeamsAreProvided() { + locationIds.addAll(Arrays.asList("my-location-id", "my-location-id2")); + careTeamIds.add("my-careteam-id"); + organisationIds.add("my-organization-id"); + + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Patient"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); + requestDetails.setRequestPath("Patient"); + + // Call the method under testing + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + List allIds = new ArrayList<>(); + allIds.addAll(locationIds); + allIds.addAll(organisationIds); + allIds.addAll(careTeamIds); + + List locationTagToValuesList = new ArrayList<>(); + + for (String locationId : locationIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); + + locationTagToValuesList.add(ProxyConstants.LOCATION_TAG_URL + "|" + locationId); + } + + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .get(0) + .contains(StringUtils.join(locationTagToValuesList, ","))); + + List careteamTagToValuesList = new ArrayList<>(); + + for (String careTeamId : careTeamIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(careTeamId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(careTeamId)); + careteamTagToValuesList.add(ProxyConstants.LOCATION_TAG_URL + "|" + careTeamId); + } + + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .get(0) + .contains(StringUtils.join(locationTagToValuesList, ","))); + + for (String organisationId : organisationIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(organisationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(organisationId)); + } + + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .get(0) + .contains( + StringUtils.join( + organisationIds, "," + ProxyConstants.ORGANISATION_TAG_URL + "|"))); + } + + @Test + public void preProcessShouldAddLocationIdFiltersWhenUserIsAssignedToLocationsOnly() + throws IOException { + locationIds.add("locationid12"); + locationIds.add("locationid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Patient"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); + requestDetails.setRequestPath("Patient"); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + for (String locationId : locationIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); + } + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .get(0) + .contains(StringUtils.join(locationIds, "," + ProxyConstants.LOCATION_TAG_URL + "|"))); + + for (String param : mutatedRequest.getQueryParams().get("_tag")) { + Assert.assertFalse(param.contains(ProxyConstants.CARE_TEAM_TAG_URL)); + Assert.assertFalse(param.contains(ProxyConstants.ORGANISATION_TAG_URL)); + } + } + + @Test + public void preProcessShouldAddCareTeamIdFiltersWhenUserIsAssignedToCareTeamsOnly() + throws IOException { + careTeamIds.add("careteamid1"); + careTeamIds.add("careteamid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Patient"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); + requestDetails.setRequestPath("Patient"); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + for (String locationId : careTeamIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); + } + + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .get(0) + .contains(StringUtils.join(careTeamIds, "," + ProxyConstants.CARE_TEAM_TAG_URL + "|"))); + + for (String param : mutatedRequest.getQueryParams().get("_tag")) { + Assert.assertFalse(param.contains(ProxyConstants.LOCATION_TAG_URL)); + Assert.assertFalse(param.contains(ProxyConstants.ORGANISATION_TAG_URL)); + } + } + + @Test + public void preProcessShouldAddOrganisationIdFiltersWhenUserIsAssignedToOrganisationsOnly() + throws IOException { + organisationIds.add("organizationid1"); + organisationIds.add("organizationid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Patient"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); + requestDetails.setRequestPath("Patient"); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + for (String locationId : careTeamIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .contains(ProxyConstants.ORGANISATION_TAG_URL + "|" + locationId)); + } + + for (String param : mutatedRequest.getQueryParams().get("_tag")) { + Assert.assertFalse(param.contains(ProxyConstants.LOCATION_TAG_URL)); + Assert.assertFalse(param.contains(ProxyConstants.CARE_TEAM_TAG_URL)); + } + } + + @Test + public void preProcessShouldAddFiltersWhenResourceNotInSyncFilterIgnoredResourcesFile() { + organisationIds.add("organizationid1"); + organisationIds.add("organizationid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Patient"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); + requestDetails.setRequestPath("Patient"); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + for (String locationId : organisationIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); + Assert.assertEquals(1, mutatedRequest.getQueryParams().size()); + } + Assert.assertTrue( + mutatedRequest + .getQueryParams() + .get("_tag") + .get(0) + .contains( + StringUtils.join( + organisationIds, "," + ProxyConstants.ORGANISATION_TAG_URL + "|"))); + } + + @Test + public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredResourcesFile() { + organisationIds.add("organizationid1"); + organisationIds.add("organizationid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("Questionnaire"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + requestDetails.setCompleteUrl("https://smartregister.org/fhir/Questionnaire"); + requestDetails.setRequestPath("Questionnaire"); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + for (String locationId : organisationIds) { + Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); + Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); + Assert.assertNull(mutatedRequest); + } + } + + @Test + public void + preProcessShouldSkipAddingFiltersWhenSearchResourceByIdsInSyncFilterIgnoredResourcesFile() { + organisationIds.add("organizationid1"); + organisationIds.add("organizationid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("StructureMap"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + List queryStringParamValues = Arrays.asList("1000", "2000", "3000"); + requestDetails.setCompleteUrl( + "https://smartregister.org/fhir/StructureMap?_id=" + + StringUtils.join(queryStringParamValues, ",")); + Assert.assertEquals( + "https://smartregister.org/fhir/StructureMap?_id=1000,2000,3000", + requestDetails.getCompleteUrl()); + requestDetails.setRequestPath("StructureMap"); + + Map params = Maps.newHashMap(); + params.put("_id", new String[] {StringUtils.join(queryStringParamValues, ",")}); + requestDetails.setParameters(params); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + Assert.assertNull(mutatedRequest); + } + + @Test + public void + preProcessShouldAddFiltersWhenSearchResourceByIdsDoNotMatchSyncFilterIgnoredResources() { + organisationIds.add("organizationid1"); + organisationIds.add("organizationid2"); + testInstance = createSyncAccessDecisionTestInstance(); + + RequestDetails requestDetails = new ServletRequestDetails(); + requestDetails.setRequestType(RequestTypeEnum.GET); + requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); + requestDetails.setResourceName("StructureMap"); + requestDetails.setFhirServerBase("https://smartregister.org/fhir"); + List queryStringParamValues = Arrays.asList("1000", "2000"); + requestDetails.setCompleteUrl( + "https://smartregister.org/fhir/StructureMap?_id=" + + StringUtils.join(queryStringParamValues, ",")); + Assert.assertEquals( + "https://smartregister.org/fhir/StructureMap?_id=1000,2000", + requestDetails.getCompleteUrl()); + requestDetails.setRequestPath("StructureMap"); + + Map params = Maps.newHashMap(); + params.put("_id", new String[] {StringUtils.join(queryStringParamValues, ",")}); + requestDetails.setParameters(params); + + RequestMutation mutatedRequest = + testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); + + List searchParamArrays = + mutatedRequest.getQueryParams().get(ProxyConstants.TAG_SEARCH_PARAM); + Assert.assertNotNull(searchParamArrays); + + Assert.assertTrue( + searchParamArrays + .get(0) + .contains( + StringUtils.join( + organisationIds, "," + ProxyConstants.ORGANISATION_TAG_URL + "|"))); + } + + @Test(expected = RuntimeException.class) + public void preprocessShouldThrowRuntimeExceptionWhenNoSyncStrategyFilterIsProvided() { + testInstance = createSyncAccessDecisionTestInstance(); + + 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"); + testInstance = Mockito.spy(createSyncAccessDecisionTestInstance()); + + FhirContext fhirR4Context = mock(FhirContext.class); + IGenericClient iGenericClient = mock(IGenericClient.class); + ITransaction iTransaction = mock(ITransaction.class); + ITransactionTyped iClientExecutable = mock(ITransactionTyped.class); + testInstance.setFhirR4Client(iGenericClient); + testInstance.setFhirR4Context(fhirR4Context); + + 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(SyncAccessDecision.Constants.FHIR_GATEWAY_MODE)) + .thenReturn(SyncAccessDecision.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 = createSyncAccessDecisionTestInstance(); + + RequestDetailsReader requestDetailsSpy = Mockito.mock(RequestDetailsReader.class); + Mockito.when(requestDetailsSpy.getHeader(SyncAccessDecision.Constants.FHIR_GATEWAY_MODE)) + .thenReturn(""); + + String resultContent = + testInstance.postProcess(requestDetailsSpy, Mockito.mock(HttpResponse.class)); + + // Verify no special Post-Processing happened + Assert.assertNull(resultContent); + } + + @Test + public void testPostProcessWithListModeHeaderSearchByTagShouldFetchListEntriesBundle() + throws IOException { + locationIds.add("Location-1"); + testInstance = Mockito.spy(createSyncAccessDecisionTestInstance()); + + FhirContext fhirR4Context = mock(FhirContext.class); + IGenericClient iGenericClient = mock(IGenericClient.class); + ITransaction iTransaction = mock(ITransaction.class); + ITransactionTyped iClientExecutable = mock(ITransactionTyped.class); + + 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(SyncAccessDecision.Constants.FHIR_GATEWAY_MODE)) + .thenReturn(SyncAccessDecision.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)); + + testInstance.setFhirR4Client(iGenericClient); + testInstance.setFhirR4Context(fhirR4Context); + 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 SyncAccessDecision createSyncAccessDecisionTestInstance() { + SyncAccessDecision accessDecision = + new SyncAccessDecision( + "sample-keycloak-id", + "sample-application-id", + true, + locationIds, + careTeamIds, + organisationIds, + null, + userRoles); + + URL configFileUrl = Resources.getResource("hapi_sync_filter_ignored_queries.json"); + SyncAccessDecision.IgnoredResourcesConfig skippedDataFilterConfig = + accessDecision.getIgnoredResourcesConfigFileConfiguration(configFileUrl.getPath()); + accessDecision.setSkippedResourcesConfig(skippedDataFilterConfig); + return accessDecision; + } +} diff --git a/src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java b/src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java new file mode 100644 index 00000000..708483ab --- /dev/null +++ b/src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java @@ -0,0 +1,106 @@ +/* + * Copyright ${license.git.copyrightYears} Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +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.api.server.RequestDetails; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import org.hl7.fhir.instance.model.api.IIdType; + +// Note instances of this class are expected to be one per thread and this class is not thread-safe +// the same way the underlying `requestDetails` is not. +public class TestRequestDetailsToReader implements RequestDetailsReader { + private final RequestDetails requestDetails; + + TestRequestDetailsToReader(RequestDetails requestDetails) { + this.requestDetails = requestDetails; + } + + public String getRequestId() { + return requestDetails.getRequestId(); + } + + public Charset getCharset() { + return requestDetails.getCharset(); + } + + public String getCompleteUrl() { + return requestDetails.getCompleteUrl(); + } + + public FhirContext getFhirContext() { + // TODO: There might be a race condition in the underlying `getFhirContext`; check if this is + // true. Note the `myServer` object is shared between threads. + return requestDetails.getFhirContext(); + } + + public String getFhirServerBase() { + return requestDetails.getFhirServerBase(); + } + + public String getHeader(String name) { + return requestDetails.getHeader(name); + } + + public List getHeaders(String name) { + return requestDetails.getHeaders(name); + } + + public IIdType getId() { + return requestDetails.getId(); + } + + public String getOperation() { + return requestDetails.getOperation(); + } + + public Map getParameters() { + return requestDetails.getParameters(); + } + + public String getRequestPath() { + return requestDetails.getRequestPath(); + } + + public RequestTypeEnum getRequestType() { + return requestDetails.getRequestType(); + } + + public String getResourceName() { + return requestDetails.getResourceName(); + } + + public RestOperationTypeEnum getRestOperationType() { + return requestDetails.getRestOperationType(); + } + + public String getSecondaryOperation() { + return requestDetails.getSecondaryOperation(); + } + + public boolean isRespondGzip() { + return requestDetails.isRespondGzip(); + } + + public byte[] loadRequestContents() { + return requestDetails.loadRequestContents(); + } +} diff --git a/src/test/java/org/smartregister/fhir/proxy/plugin/TestUtil.java b/src/test/java/com/google/fhir/gateway/plugin/TestUtil.java similarity index 93% rename from src/test/java/org/smartregister/fhir/proxy/plugin/TestUtil.java rename to src/test/java/com/google/fhir/gateway/plugin/TestUtil.java index 23b01451..1b5cfc7c 100644 --- a/src/test/java/org/smartregister/fhir/proxy/plugin/TestUtil.java +++ b/src/test/java/com/google/fhir/gateway/plugin/TestUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2022 Ona Systems, Inc + * Copyright ${license.git.copyrightYears} Ona Systems, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.smartregister.fhir.proxy.plugin; +package com.google.fhir.gateway.plugin; import static org.mockito.Mockito.when; diff --git a/src/test/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java b/src/test/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java deleted file mode 100644 index 5313b673..00000000 --- a/src/test/java/org/smartregister/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright 2021-2023 Ona Systems, Inc - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.proxy.plugin; - -import ca.uhn.fhir.rest.api.RequestTypeEnum; -import ca.uhn.fhir.rest.api.RestOperationTypeEnum; -import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; -import com.google.fhir.proxy.ProxyConstants; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; - -@RunWith(MockitoJUnitRunner.class) -public class OpenSRPSyncAccessDecisionTest { - - private List locationIds = new ArrayList<>(); - - private List careTeamIds = new ArrayList<>(); - - private List organisationIds = new ArrayList<>(); - - private OpenSRPSyncAccessDecision testInstance; - - @Test - public void preprocessShouldAddAllFiltersWhenIdsForLocationsOrganisationsAndCareTeamsAreProvided() - throws IOException { - - testInstance = createOpenSRPSyncAccessDecisionTestInstance(); - - ServletRequestDetails requestDetails = new ServletRequestDetails(); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); - requestDetails.setResourceName("Patient"); - requestDetails.setFhirServerBase("https://smartregister.org/fhir"); - requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); - requestDetails.setRequestPath("Patient"); - - // Call the method under testing - testInstance.preProcess(requestDetails); - - List allIds = new ArrayList<>(); - allIds.addAll(locationIds); - allIds.addAll(organisationIds); - allIds.addAll(careTeamIds); - - for (String locationId : locationIds) { - Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); - Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - Assert.assertTrue( - Arrays.asList(requestDetails.getParameters().get("_tag")) - .contains(ProxyConstants.LOCATION_TAG_URL + "|" + locationId)); - } - - for (String careTeamId : careTeamIds) { - Assert.assertFalse(requestDetails.getCompleteUrl().contains(careTeamId)); - Assert.assertFalse(requestDetails.getRequestPath().contains(careTeamId)); - Assert.assertTrue( - Arrays.asList(requestDetails.getParameters().get("_tag")) - .contains(ProxyConstants.CARE_TEAM_TAG_URL + "|" + careTeamId)); - } - - for (String organisationId : organisationIds) { - Assert.assertFalse(requestDetails.getCompleteUrl().contains(organisationId)); - Assert.assertFalse(requestDetails.getRequestPath().contains(organisationId)); - Assert.assertTrue( - Arrays.asList(requestDetails.getParameters().get("_tag")) - .contains(ProxyConstants.ORGANISATION_TAG_URL + "|" + organisationId)); - } - } - - @Test - public void preProcessShouldAddLocationIdFiltersWhenUserIsAssignedToLocationsOnly() - throws IOException { - locationIds.add("locationid12"); - locationIds.add("locationid2"); - testInstance = createOpenSRPSyncAccessDecisionTestInstance(); - - ServletRequestDetails requestDetails = new ServletRequestDetails(); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); - requestDetails.setResourceName("Patient"); - requestDetails.setFhirServerBase("https://smartregister.org/fhir"); - requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); - requestDetails.setRequestPath("Patient"); - - testInstance.preProcess(requestDetails); - - for (String locationId : locationIds) { - Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); - Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - Assert.assertTrue( - Arrays.asList(requestDetails.getParameters().get("_tag")) - .contains(ProxyConstants.LOCATION_TAG_URL + "|" + locationId)); - } - - for (String param : requestDetails.getParameters().get("_tag")) { - Assert.assertFalse(param.contains(ProxyConstants.CARE_TEAM_TAG_URL)); - Assert.assertFalse(param.contains(ProxyConstants.ORGANISATION_TAG_URL)); - } - } - - @Test - public void preProcessShouldAddCareTeamIdFiltersWhenUserIsAssignedToCareTeamsOnly() - throws IOException { - careTeamIds.add("careteamid1"); - careTeamIds.add("careteamid2"); - testInstance = createOpenSRPSyncAccessDecisionTestInstance(); - - ServletRequestDetails requestDetails = new ServletRequestDetails(); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); - requestDetails.setResourceName("Patient"); - requestDetails.setFhirServerBase("https://smartregister.org/fhir"); - requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); - requestDetails.setRequestPath("Patient"); - - testInstance.preProcess(requestDetails); - - for (String locationId : careTeamIds) { - Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); - Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - Assert.assertTrue( - Arrays.asList(requestDetails.getParameters().get("_tag")) - .contains(ProxyConstants.CARE_TEAM_TAG_URL + "|" + locationId)); - } - - for (String param : requestDetails.getParameters().get("_tag")) { - Assert.assertFalse(param.contains(ProxyConstants.LOCATION_TAG_URL)); - Assert.assertFalse(param.contains(ProxyConstants.ORGANISATION_TAG_URL)); - } - } - - @Test - public void preProcessShouldAddOrganisationIdFiltersWhenUserIsAssignedToOrganisationsOnly() - throws IOException { - organisationIds.add("organizationid1"); - organisationIds.add("organizationid2"); - testInstance = createOpenSRPSyncAccessDecisionTestInstance(); - - ServletRequestDetails requestDetails = new ServletRequestDetails(); - requestDetails.setRequestType(RequestTypeEnum.GET); - requestDetails.setRestOperationType(RestOperationTypeEnum.SEARCH_TYPE); - requestDetails.setResourceName("Patient"); - requestDetails.setFhirServerBase("https://smartregister.org/fhir"); - requestDetails.setCompleteUrl("https://smartregister.org/fhir/Patient"); - requestDetails.setRequestPath("Patient"); - - testInstance.preProcess(requestDetails); - - for (String locationId : careTeamIds) { - Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); - Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - Assert.assertTrue( - Arrays.asList(requestDetails.getParameters().get("_tag")) - .contains(ProxyConstants.ORGANISATION_TAG_URL + "|" + locationId)); - } - - for (String param : requestDetails.getParameters().get("_tag")) { - Assert.assertFalse(param.contains(ProxyConstants.LOCATION_TAG_URL)); - Assert.assertFalse(param.contains(ProxyConstants.CARE_TEAM_TAG_URL)); - } - } - - private OpenSRPSyncAccessDecision createOpenSRPSyncAccessDecisionTestInstance() { - return new OpenSRPSyncAccessDecision( - "sample-application-id", true, locationIds, careTeamIds, organisationIds, null); - } -} diff --git a/src/test/resources/bundle_transaction_delete_non_patient.json b/src/test/resources/bundle_transaction_delete_non_patient.json new file mode 100644 index 00000000..3a68625b --- /dev/null +++ b/src/test/resources/bundle_transaction_delete_non_patient.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "request": { + "method": "DELETE", + "url": "Observation?patient=be92a43f-de46-affa-b131-bbf9eea51140&_id=4047" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/bundle_transaction_delete_patient.json b/src/test/resources/bundle_transaction_delete_patient.json new file mode 100644 index 00000000..dd713d5a --- /dev/null +++ b/src/test/resources/bundle_transaction_delete_patient.json @@ -0,0 +1,34 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "name": [ + { + "family": "Smith", + "given": [ + "Darcy" + ] + } + ], + "gender": "female", + "address": [ + { + "line": [ + "123 Main St." + ], + "city": "Anycity", + "state": "CA", + "postalCode": "12345" + } + ] + }, + "request": { + "method": "DELETE", + "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/bundle_transaction_delete.json b/src/test/resources/bundle_transaction_delete_patient_unauthorized.json similarity index 92% rename from src/test/resources/bundle_transaction_delete.json rename to src/test/resources/bundle_transaction_delete_patient_unauthorized.json index 7eb25325..aef9470a 100644 --- a/src/test/resources/bundle_transaction_delete.json +++ b/src/test/resources/bundle_transaction_delete_patient_unauthorized.json @@ -27,7 +27,7 @@ }, "request": { "method": "DELETE", - "url": "Patient/7890" + "url": "Patient/patient-non-authorized" } } ] diff --git a/src/test/resources/bundle_transaction_get_non_patient_authorized.json b/src/test/resources/bundle_transaction_get_non_patient_authorized.json new file mode 100644 index 00000000..e25d6515 --- /dev/null +++ b/src/test/resources/bundle_transaction_get_non_patient_authorized.json @@ -0,0 +1,12 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "request": { + "method": "GET", + "url": "Encounter?patient=be92a43f-de46-affa-b131-bbf9eea51140" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/bundle_transaction_patch_authorized.json b/src/test/resources/bundle_transaction_patch_authorized.json new file mode 100644 index 00000000..39e684fe --- /dev/null +++ b/src/test/resources/bundle_transaction_patch_authorized.json @@ -0,0 +1,30 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "fullUrl": "Patient/be92a43f-de46-affa-b131-bbf9eea51140", + "resource": { + "resourceType": "Binary", + "contentType": "application/json-patch+json", + "data": "WyB7ICJvcCI6InJlcGxhY2UiLCAicGF0aCI6Ii9hY3RpdmUiLCAidmFsdWUiOnRydWUgfSBd" + }, + "request": { + "method": "PATCH", + "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" + } + }, + { + "fullUrl": "Observation/observation-jamess-bond-id-1", + "resource": { + "resourceType": "Binary", + "contentType": "application/json-patch+json", + "data": "WwogIHsKICAgICJvcCI6ICJyZXBsYWNlIiwKICAgICJwYXRoIjogIi9zdWJqZWN0L3JlZmVyZW5jZSIsCiAgICAidmFsdWUiOiAiUGF0aWVudC9iZTkyYTQzZi1kZTQ2LWFmZmEtYjEzMS1iYmY5ZWVhNTExNDAiCiAgfSx7CiAgIm9wIjogImFkZCIsCiAgInBhdGgiOiAiL3BlcmZvcm1lciIsCiAgInZhbHVlIjpbXQp9LAogIHsKICAgICJvcCI6ICJhZGQiLAogICAgInBhdGgiIDogIi9wZXJmb3JtZXIvMCIsCiAgICAidmFsdWUiIDogewogICAgICAicmVmZXJlbmNlIjogIlBhdGllbnQvbWljaGFlbCIKICAgIH0KICB9Cl0=" + }, + "request": { + "method": "PATCH", + "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/be92a43f-de46-affa-b131-bbf9eea51140" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/bundle_transaction_put_authorized_patient.json b/src/test/resources/bundle_transaction_put_authorized_patient.json new file mode 100644 index 00000000..f6cee449 --- /dev/null +++ b/src/test/resources/bundle_transaction_put_authorized_patient.json @@ -0,0 +1,34 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "name": [ + { + "family": "Smith", + "given": [ + "Darcy" + ] + } + ], + "gender": "female", + "address": [ + { + "line": [ + "123 Main St." + ], + "city": "Anycity", + "state": "CA", + "postalCode": "12345" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/hapi_sync_filter_ignored_queries.json b/src/test/resources/hapi_sync_filter_ignored_queries.json new file mode 100644 index 00000000..45a10ad5 --- /dev/null +++ b/src/test/resources/hapi_sync_filter_ignored_queries.json @@ -0,0 +1,29 @@ +{ + "entries": [ + { + "path": "Questionnaire", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "List", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "StructureMap", + "methodType": "GET", + "queryParams": { + "_id": [ + "1000", + "2000", + "3000" + ] + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/test_bundle_transaction.json b/src/test/resources/test_bundle_transaction.json new file mode 100644 index 00000000..54714c68 --- /dev/null +++ b/src/test/resources/test_bundle_transaction.json @@ -0,0 +1,61 @@ +{ + "resourceType": "Bundle", + "type": "transaction", + "entry": [ + { + "resource": { + "resourceType": "Patient", + "name": [ + { + "family": "Smith", + "given": [ + "Darcy" + ] + } + ], + "gender": "female", + "address": [ + { + "line": [ + "123 Main St." + ], + "city": "Anycity", + "state": "CA", + "postalCode": "12345" + } + ] + }, + "request": { + "method": "POST", + "url": "Patient" + } + }, { + "resource": { + "resourceType": "Patient", + "name": [ + { + "family": "Smith", + "given": [ + "Darcy" + ] + } + ], + "gender": "female", + "address": [ + { + "line": [ + "123 Main St." + ], + "city": "Anycity", + "state": "CA", + "postalCode": "12345" + } + ] + }, + "request": { + "method": "PUT", + "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" + } + } + ] +} \ No newline at end of file diff --git a/src/test/resources/test_list_resource.json b/src/test/resources/test_list_resource.json new file mode 100644 index 00000000..6d384d29 --- /dev/null +++ b/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" + } + } + ] +} From 72255c01f0315a8f8143463fe6f3c3d4c1c0dc58 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 23 Aug 2023 17:42:58 +0500 Subject: [PATCH 02/22] Remove redundant code --- pom.xml | 2 +- .../plugin/AccessGrantedAndUpdateList.java | 159 ------- .../gateway/plugin/ListAccessChecker.java | 384 ----------------- .../gateway/plugin/PatientAccessChecker.java | 315 -------------- .../plugin/PermissionAccessChecker.java | 2 +- .../PractitionerDetailsEndpointHelper.java | 2 +- .../fhir/gateway/plugin/SmartFhirScope.java | 164 -------- .../gateway/plugin/SmartScopeChecker.java | 50 --- .../gateway/plugin/SyncAccessDecision.java | 2 +- .../gateway/plugin/AccessCheckerTestBase.java | 388 ------------------ .../AccessGrantedAndUpdateListTest.java | 72 ---- .../gateway/plugin/ListAccessCheckerTest.java | 328 --------------- .../plugin/PatientAccessCheckerTest.java | 219 ---------- .../plugin/PermissionAccessCheckerTest.java | 2 +- .../gateway/plugin/SmartScopeCheckerTest.java | 106 ----- .../plugin/SyncAccessDecisionTest.java | 2 +- .../plugin/TestRequestDetailsToReader.java | 2 +- .../google/fhir/gateway/plugin/TestUtil.java | 2 +- .../resources/bundle_list_patient_item.json | 41 -- ...bundle_transaction_delete_non_patient.json | 12 - .../bundle_transaction_delete_patient.json | 34 -- ...ansaction_delete_patient_unauthorized.json | 34 -- ...action_get_multiple_with_null_patient.json | 18 - ...ransaction_get_non_patient_authorized.json | 12 - ...nsaction_get_non_patient_unauthorized.json | 12 - ..._transaction_get_patient_unauthorized.json | 12 - .../bundle_transaction_no_patient_in_url.json | 102 ----- .../bundle_transaction_no_patient_ref.json | 96 ----- .../bundle_transaction_no_resource_field.json | 91 ---- .../bundle_transaction_non_patients.json | 102 ----- .../bundle_transaction_patch_authorized.json | 30 -- .../bundle_transaction_patch_not_binary.json | 41 -- ...bundle_transaction_patch_unauthorized.json | 30 -- .../bundle_transaction_post_patient.json | 34 -- ...le_transaction_put_authorized_patient.json | 34 -- .../bundle_transaction_put_unauthorized.json | 34 -- src/test/resources/capability.json | 82 ---- src/test/resources/patient_id_search.json | 225 ---------- .../resources/patient_id_search_single.json | 41 -- src/test/resources/test_obs.json | 48 --- src/test/resources/test_obs_no_subject.json | 45 -- src/test/resources/test_obs_patch.json | 22 - .../test_obs_patch_no_reference.json | 14 - ..._obs_patch_unauthorized_no_patient_id.json | 9 - .../test_obs_patch_unauthorized_patient.json | 18 - .../test_obs_patch_unauthorized_remove.json | 16 - src/test/resources/test_obs_performers.json | 59 --- src/test/resources/test_obs_unauthorized.json | 48 --- 48 files changed, 8 insertions(+), 3589 deletions(-) delete mode 100644 src/main/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateList.java delete mode 100644 src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java delete mode 100644 src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java mode change 100755 => 100644 src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java delete mode 100644 src/main/java/com/google/fhir/gateway/plugin/SmartFhirScope.java delete mode 100644 src/main/java/com/google/fhir/gateway/plugin/SmartScopeChecker.java delete mode 100644 src/test/java/com/google/fhir/gateway/plugin/AccessCheckerTestBase.java delete mode 100644 src/test/java/com/google/fhir/gateway/plugin/AccessGrantedAndUpdateListTest.java delete mode 100644 src/test/java/com/google/fhir/gateway/plugin/ListAccessCheckerTest.java delete mode 100644 src/test/java/com/google/fhir/gateway/plugin/PatientAccessCheckerTest.java delete mode 100644 src/test/java/com/google/fhir/gateway/plugin/SmartScopeCheckerTest.java delete mode 100644 src/test/resources/bundle_list_patient_item.json delete mode 100644 src/test/resources/bundle_transaction_delete_non_patient.json delete mode 100644 src/test/resources/bundle_transaction_delete_patient.json delete mode 100644 src/test/resources/bundle_transaction_delete_patient_unauthorized.json delete mode 100644 src/test/resources/bundle_transaction_get_multiple_with_null_patient.json delete mode 100644 src/test/resources/bundle_transaction_get_non_patient_authorized.json delete mode 100644 src/test/resources/bundle_transaction_get_non_patient_unauthorized.json delete mode 100644 src/test/resources/bundle_transaction_get_patient_unauthorized.json delete mode 100644 src/test/resources/bundle_transaction_no_patient_in_url.json delete mode 100644 src/test/resources/bundle_transaction_no_patient_ref.json delete mode 100644 src/test/resources/bundle_transaction_no_resource_field.json delete mode 100644 src/test/resources/bundle_transaction_non_patients.json delete mode 100644 src/test/resources/bundle_transaction_patch_authorized.json delete mode 100644 src/test/resources/bundle_transaction_patch_not_binary.json delete mode 100644 src/test/resources/bundle_transaction_patch_unauthorized.json delete mode 100644 src/test/resources/bundle_transaction_post_patient.json delete mode 100644 src/test/resources/bundle_transaction_put_authorized_patient.json delete mode 100644 src/test/resources/bundle_transaction_put_unauthorized.json delete mode 100644 src/test/resources/capability.json delete mode 100644 src/test/resources/patient_id_search.json delete mode 100644 src/test/resources/patient_id_search_single.json delete mode 100644 src/test/resources/test_obs.json delete mode 100644 src/test/resources/test_obs_no_subject.json delete mode 100644 src/test/resources/test_obs_patch.json delete mode 100644 src/test/resources/test_obs_patch_no_reference.json delete mode 100644 src/test/resources/test_obs_patch_unauthorized_no_patient_id.json delete mode 100644 src/test/resources/test_obs_patch_unauthorized_patient.json delete mode 100644 src/test/resources/test_obs_patch_unauthorized_remove.json delete mode 100644 src/test/resources/test_obs_performers.json delete mode 100644 src/test/resources/test_obs_unauthorized.json diff --git a/pom.xml b/pom.xml index 4b0fb992..952c4f2d 100755 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ +This repo holds the OpenSRP permissions checker and data access checker diff --git a/pom.xml b/pom.xml index 952c4f2d..cea07f28 100755 --- a/pom.xml +++ b/pom.xml @@ -36,18 +36,18 @@ https://github.com/opensrp/fhir-access-proxy-plugin - - - - - - - - - - - - + + + nexus-releases + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + + false + nexus-snapshots + Nexus Snapshots Repository + https://oss.sonatype.org/content/repositories/snapshots + + UTF-8 From 4e006e2cc0b2e786a93e6ee4366b96a657f6659a Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 6 Sep 2023 16:18:55 +0500 Subject: [PATCH 04/22] Move PractitionerDetailEndpoint to the plugins repo. --- pom.xml | 26 +++---- .../plugin/PractitionerDetailEndpoint.java | 78 +++++++++++++++++++ .../PractitionerDetailsEndpointHelper.java | 25 +++--- .../fhir/gateway/plugin/ProxyConstants.java | 45 +++++++++++ .../gateway/plugin/SyncAccessDecision.java | 25 +++--- 5 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java create mode 100644 src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java diff --git a/pom.xml b/pom.xml index cea07f28..4339e3f8 100755 --- a/pom.xml +++ b/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.2.0 + 0.1.32 org.smartregister @@ -36,18 +36,18 @@ https://github.com/opensrp/fhir-access-proxy-plugin - - - nexus-releases - https://oss.sonatype.org/service/local/staging/deploy/maven2 - - - false - nexus-snapshots - Nexus Snapshots Repository - https://oss.sonatype.org/content/repositories/snapshots - - + + + + + + + + + + + + UTF-8 diff --git a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java b/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java new file mode 100644 index 00000000..2772a54a --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021-2023 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + + +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.fhir.gateway.FhirClientFactory; +import com.google.fhir.gateway.HttpFhirClient; +import org.apache.http.HttpStatus; +import com.google.fhir.gateway.TokenVerifier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.practitioner.PractitionerDetails; + +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization + * header. Although it is not a standard FHIR query, but it uses the FHIR server to construct the + * response. In this example, it inspects the JWT and depending on its claims, constructs the list + * of Patient IDs that the user has access to. + * + *

The two types of tokens resemble {@link com.google.fhir.gateway.plugin.ListAccessChecker} and + * {@link com.google.fhir.gateway.plugin.PatientAccessChecker} expected tokens. But those are just + * picked as examples and this custom endpoint is independent of any {@link + * com.google.fhir.gateway.interfaces.AccessChecker}. + */ +@WebServlet("/PractitionerDetail") +public class PractitionerDetailEndpoint extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(PractitionerDetailEndpoint.class); + private final TokenVerifier tokenVerifier; + + private final HttpFhirClient fhirClient; + + private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + public PractitionerDetailEndpoint() throws IOException { + this.tokenVerifier = TokenVerifier.createFromEnvVars(); + this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + // Check the Bearer token to be a valid JWT with required claims. + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) { + throw new ServletException("No Authorization header provided!"); + } + List patientIds = new ArrayList<>(); + // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: + DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); + String keycloakUuid = request.getParameter("keycloak-uuid"); + PractitionerDetails practitionerDetails = practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); + response.setStatus(HttpStatus.SC_OK); + } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java b/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java index cf016b48..26295ee9 100644 --- a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java +++ b/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java @@ -19,7 +19,6 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -import com.google.fhir.gateway.ProxyConstants; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -38,7 +37,6 @@ import org.hl7.fhir.r4.model.OrganizationAffiliation; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.model.location.LocationHierarchy; @@ -46,8 +44,6 @@ import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; import org.smartregister.utils.Constants; -import org.springframework.lang.Nullable; - public class PractitionerDetailsEndpointHelper { private static final Logger logger = LoggerFactory.getLogger(PractitionerDetailsEndpointHelper.class); @@ -158,7 +154,6 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract return responseBundle; } - @NotNull public static List getAttributedLocations(List locationHierarchies) { List parentChildrenList = locationHierarchies.stream() @@ -410,7 +405,7 @@ private Bundle getOrganizationsById(List organizationIds) { .execute(); } - private @Nullable List getLocationsByIds(List locationIds) { + private List getLocationsByIds(List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); } @@ -428,7 +423,7 @@ private Bundle getOrganizationsById(List organizationIds) { .collect(Collectors.toList()); } - private @Nullable List getOfficialLocationIdentifiersByLocationIds( + private List getOfficialLocationIdentifiersByLocationIds( List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); @@ -537,12 +532,14 @@ private List getLocationsHierarchyByOfficialLocationIdentifie } public static String createSearchTagValues(Map.Entry entry) { - return entry.getKey() - + ProxyConstants.CODE_URL_VALUE_SEPARATOR - + StringUtils.join( - entry.getValue(), - ProxyConstants.PARAM_VALUES_SEPARATOR - + entry.getKey() - + ProxyConstants.CODE_URL_VALUE_SEPARATOR); +// return entry.getKey() +// + ProxyConstants.CODE_URL_VALUE_SEPARATOR +// + StringUtils.join( +// entry.getValue(), +// ProxyConstants.PARAM_VALUES_SEPARATOR +// + entry.getKey() +// + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + + return ""; } } diff --git a/src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java b/src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java new file mode 100644 index 00000000..35148fb2 --- /dev/null +++ b/src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java @@ -0,0 +1,45 @@ +/* + * Copyright 2021-2023 Ona Systems, Inc + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.fhir.gateway.plugin; + +import ca.uhn.fhir.rest.api.Constants; +import org.apache.http.entity.ContentType; + +public class ProxyConstants { + + public static final String CARE_TEAM_TAG_URL = "https://smartregister.org/care-team-tag-id"; + + public static final String LOCATION_TAG_URL = "https://smartregister.org/location-tag-id"; + + public static final String ORGANISATION_TAG_URL = "https://smartregister.org/organisation-tag-id"; + + public static final String TAG_SEARCH_PARAM = "_tag"; + + public static final String PARAM_VALUES_SEPARATOR = ","; + + public static final String CODE_URL_VALUE_SEPARATOR = "|"; + + public static final String HTTP_URL_SEPARATOR = "/"; + + // Note we should not set charset here; otherwise GCP FHIR store complains about Content-Type. + static final ContentType JSON_PATCH_CONTENT = ContentType.create(Constants.CT_JSON_PATCH); + public static final String SYNC_STRATEGY = "syncStrategy"; + public static final String REALM_ACCESS = "realm_access"; + + public interface Literals { + String EQUALS = "="; + } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java b/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java index ae30770e..1753eb6b 100755 --- a/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java +++ b/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java @@ -47,6 +47,8 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static com.google.fhir.gateway.plugin.ProxyConstants.*; + public class SyncAccessDecision implements AccessDecision { public static final String SYNC_FILTER_IGNORE_RESOURCES_FILE_ENV = "SYNC_FILTER_IGNORE_RESOURCES_FILE"; @@ -128,8 +130,7 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea requestMutation = RequestMutation.builder() .queryParams( - Map.of( - ProxyConstants.TAG_SEARCH_PARAM, + Map.of(TAG_SEARCH_PARAM, Arrays.asList(StringUtils.join(syncFilterParameterValues, ",")))) .build(); } @@ -301,12 +302,12 @@ private Map getSyncTags( StringBuilder sb = new StringBuilder(); Map map = new HashMap<>(); - sb.append(ProxyConstants.TAG_SEARCH_PARAM); - sb.append(ProxyConstants.Literals.EQUALS); + sb.append(TAG_SEARCH_PARAM); + sb.append(Literals.EQUALS); - addTags(ProxyConstants.LOCATION_TAG_URL, locationIds, map, sb); - addTags(ProxyConstants.ORGANISATION_TAG_URL, organizationIds, map, sb); - addTags(ProxyConstants.CARE_TEAM_TAG_URL, careTeamIds, map, sb); + addTags(LOCATION_TAG_URL, locationIds, map, sb); + addTags(ORGANISATION_TAG_URL, organizationIds, map, sb); + addTags(CARE_TEAM_TAG_URL, careTeamIds, map, sb); return map; } @@ -319,8 +320,8 @@ private void addTags( int len = values.size(); if (len > 0) { if (urlStringBuilder.length() - != (ProxyConstants.TAG_SEARCH_PARAM + ProxyConstants.Literals.EQUALS).length()) { - urlStringBuilder.append(ProxyConstants.PARAM_VALUES_SEPARATOR); + != (TAG_SEARCH_PARAM + com.google.fhir.gateway.plugin.ProxyConstants.Literals.EQUALS).length()) { + urlStringBuilder.append(PARAM_VALUES_SEPARATOR); } map.put(tagUrl, values.toArray(new String[0])); @@ -328,11 +329,11 @@ private void addTags( int i = 0; for (String tagValue : values) { urlStringBuilder.append(tagUrl); - urlStringBuilder.append(ProxyConstants.CODE_URL_VALUE_SEPARATOR); + urlStringBuilder.append(CODE_URL_VALUE_SEPARATOR); urlStringBuilder.append(tagValue); if (i != len - 1) { - urlStringBuilder.append(ProxyConstants.PARAM_VALUES_SEPARATOR); + urlStringBuilder.append(PARAM_VALUES_SEPARATOR); } i++; } @@ -352,7 +353,7 @@ private boolean isSyncUrl(RequestDetailsReader requestDetailsReader) { private boolean isResourceTypeRequest(String requestPath) { if (!TextUtils.isEmpty(requestPath)) { - String[] sections = requestPath.split(ProxyConstants.HTTP_URL_SEPARATOR); + String[] sections = requestPath.split(HTTP_URL_SEPARATOR); return sections.length == 1 || (sections.length == 2 && TextUtils.isEmpty(sections[1])); } From 48bb75cbb4e8369be4ae51660997b2ded6dd7112 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Mon, 25 Sep 2023 13:07:51 +0500 Subject: [PATCH 05/22] Issue#5 : Move OpenSRP Specific Changes to Fhir Gateway Plugins (custom repo) --- LICENSE | 202 +++++++ customplugins/LICENSE | 202 +++++++ customplugins/license-header.txt | 13 + customplugins/pom.xml | 71 +++ customplugins/resources/README.md | 13 + .../hapi_page_url_allowed_queries.json | 30 + .../hapi_sync_filter_ignored_queries.json | 60 ++ .../fhir/gateway/plugins/Constants.java | 64 +++ .../plugins}/PermissionAccessChecker.java | 8 +- .../plugins}/PractitionerDetailEndpoint.java | 24 +- .../PractitionerDetailsEndpointHelper.java | 29 +- .../fhir/gateway/plugins}/ProxyConstants.java | 4 +- .../gateway/plugins/ResourceFinderImp.java | 108 ++++ .../fhir/gateway/plugins/RestUtil.java | 34 ++ .../gateway/plugins}/SyncAccessDecision.java | 33 +- .../plugins/interfaces/ResourceFinder.java | 25 + .../plugins}/PermissionAccessCheckerTest.java | 4 +- .../plugins}/SyncAccessDecisionTest.java | 4 +- .../plugins}/TestRequestDetailsToReader.java | 4 +- .../fhir/gateway/plugins}/TestUtil.java | 4 +- .../hapi_sync_filter_ignored_queries.json | 0 .../resources/test_bundle_transaction.json | 0 .../test/resources/test_list_resource.json | 0 exec/README.md | 12 + exec/pom.xml | 115 ++++ .../smartregister/fhir/gateway/MainApp.java | 35 ++ .../gateway/PractitionerDetailEndpoint.java | 79 +++ .../PractitionerDetailsEndpointHelper.java | 534 ++++++++++++++++++ .../fhir/gateway/MainAppTest.java | 30 + license-header.txt | 2 +- pom.xml | 60 +- src/test/resources/bundle_empty.json | 19 - ..._transaction_patient_and_non_patients.json | 130 ----- .../bundle_transaction_put_patient.json | 62 -- src/test/resources/patient-list-example.json | 22 - src/test/resources/test_patient.json | 108 ---- 36 files changed, 1693 insertions(+), 451 deletions(-) create mode 100644 LICENSE create mode 100644 customplugins/LICENSE create mode 100644 customplugins/license-header.txt create mode 100644 customplugins/pom.xml create mode 100644 customplugins/resources/README.md create mode 100644 customplugins/resources/hapi_page_url_allowed_queries.json create mode 100644 customplugins/resources/hapi_sync_filter_ignored_queries.json create mode 100644 customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java rename {src/main/java/com/google/fhir/gateway/plugin => customplugins/src/main/java/org/smartregister/fhir/gateway/plugins}/PermissionAccessChecker.java (98%) rename {src/main/java/com/google/fhir/gateway/plugin => customplugins/src/main/java/org/smartregister/fhir/gateway/plugins}/PractitionerDetailEndpoint.java (93%) rename {src/main/java/com/google/fhir/gateway/plugin => customplugins/src/main/java/org/smartregister/fhir/gateway/plugins}/PractitionerDetailsEndpointHelper.java (96%) rename {src/main/java/com/google/fhir/gateway/plugin => customplugins/src/main/java/org/smartregister/fhir/gateway/plugins}/ProxyConstants.java (95%) create mode 100755 customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java create mode 100644 customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java rename {src/main/java/com/google/fhir/gateway/plugin => customplugins/src/main/java/org/smartregister/fhir/gateway/plugins}/SyncAccessDecision.java (93%) create mode 100755 customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java rename {src/test/java/com/google/fhir/gateway/plugin => customplugins/src/test/java/org/smartregister/fhir/gateway/plugins}/PermissionAccessCheckerTest.java (99%) rename {src/test/java/com/google/fhir/gateway/plugin => customplugins/src/test/java/org/smartregister/fhir/gateway/plugins}/SyncAccessDecisionTest.java (99%) rename {src/test/java/com/google/fhir/gateway/plugin => customplugins/src/test/java/org/smartregister/fhir/gateway/plugins}/TestRequestDetailsToReader.java (97%) rename {src/test/java/com/google/fhir/gateway/plugin => customplugins/src/test/java/org/smartregister/fhir/gateway/plugins}/TestUtil.java (94%) rename {src => customplugins/src}/test/resources/hapi_sync_filter_ignored_queries.json (100%) rename {src => customplugins/src}/test/resources/test_bundle_transaction.json (100%) rename {src => customplugins/src}/test/resources/test_list_resource.json (100%) create mode 100644 exec/README.md create mode 100755 exec/pom.xml create mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java create mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java create mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java create mode 100644 exec/src/test/java/org/smartregister/fhir/gateway/MainAppTest.java delete mode 100644 src/test/resources/bundle_empty.json delete mode 100644 src/test/resources/bundle_transaction_patient_and_non_patients.json delete mode 100644 src/test/resources/bundle_transaction_put_patient.json delete mode 100644 src/test/resources/patient-list-example.json delete mode 100644 src/test/resources/test_patient.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/customplugins/LICENSE b/customplugins/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/customplugins/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/customplugins/license-header.txt b/customplugins/license-header.txt new file mode 100644 index 00000000..edb10158 --- /dev/null +++ b/customplugins/license-header.txt @@ -0,0 +1,13 @@ +Copyright ${license.git.copyrightYears} Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/customplugins/pom.xml b/customplugins/pom.xml new file mode 100644 index 00000000..9c20e393 --- /dev/null +++ b/customplugins/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + org.smartregister + opensrp-fhir-proxy-plugin + 1.0.0 + + + customplugins + + + UTF-8 + 11 + 11 + ${project.basedir} + 6.0.1 + 0.2.0 + + + + + + com.google.fhir.gateway + server + 0.1.32 + + + com.google.fhir.gateway + plugins + 0.1.32 + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${hapifhir_version} + + + org.smartregister + fhir-common-utils + 0.0.9-SNAPSHOT + compile + + + + + diff --git a/customplugins/resources/README.md b/customplugins/resources/README.md new file mode 100644 index 00000000..f6125144 --- /dev/null +++ b/customplugins/resources/README.md @@ -0,0 +1,13 @@ +# Files description + +- `patient-list-example.json`: This is a sample list of patient IDs that can be + uploaded to a FHIR store and used as authorization list: + ```shell + $ curl --request PUT \ + -H "Authorization: Bearer $(gcloud auth print-access-token)" \ + -H "Content-Type: application/fhir+json; charset=utf-8" \ + "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/patient-list-example" \ + -d @patient-list-example.json + ``` + The test user that is configured on the default test Keycloak IDP has the ID + of this list as its `patient_list` claim. diff --git a/customplugins/resources/hapi_page_url_allowed_queries.json b/customplugins/resources/hapi_page_url_allowed_queries.json new file mode 100644 index 00000000..927b2c33 --- /dev/null +++ b/customplugins/resources/hapi_page_url_allowed_queries.json @@ -0,0 +1,30 @@ +{ + "entries": [ + { + "path": "", + "queryParams": { + "_getpages": "ANY_VALUE" + }, + "allowExtraParams": true, + "allParamsRequired": false + }, + { + "path": "Composition", + "methodType": "GET", + "queryParams": { + "identifier":"ANY_VALUE" + }, + "allowExtraParams": true, + "allParamsRequired": true + }, + { + "path": "Binary", + "methodType": "GET", + "queryParams": { + "_id":"ANY_VALUE" + }, + "allowExtraParams": true, + "allParamsRequired": true + } + ] +} diff --git a/customplugins/resources/hapi_sync_filter_ignored_queries.json b/customplugins/resources/hapi_sync_filter_ignored_queries.json new file mode 100644 index 00000000..b71ffedc --- /dev/null +++ b/customplugins/resources/hapi_sync_filter_ignored_queries.json @@ -0,0 +1,60 @@ +{ + "entries": [ + { + "path": "Questionnaire", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "StructureMap", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "List", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "PlanDefinition", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "Library", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "Measure", + "methodType": "GET", + "queryParams": { + "_id": "ANY_VALUE" + } + }, + { + "path": "LocationHierarchy/", + "methodType": "GET", + "queryParams": { + "identifier": "ANY_VALUE" + } + }, + { + "path": "PractitionerDetails", + "methodType": "GET", + "queryParams": { + "keycloak-uuid": "ANY_VALUE" + } + } + ] +} diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java new file mode 100644 index 00000000..017db0bf --- /dev/null +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway.plugins; + +public interface Constants { + String SLASH_UNDERSCORE = "/_"; + String LOCATION = "Location"; + String FORWARD_SLASH = "/"; + String IDENTIFIER = "identifier"; + String LOCATION_RESOURCE_NOT_FOUND = "Location Resource : Not Found"; + String LOCATION_RESOURCE = "Location Resource : "; + String PART_OF = "partof"; + String KEYCLOAK_UUID = "keycloak-uuid"; + String PRACTITIONER = "practitioner"; + String PARTICIPANT = "participant"; + String KEYCLOAK_USER_NOT_FOUND = "Keycloak User Not Found"; + String PRACTITIONER_NOT_FOUND = "Practitioner Not Found"; + String PRIMARY_ORGANIZATION = "primary-organization"; + String ID = "_id"; + String PREFFERED_USERNAME = "Preferred Username"; + String USERNAME = "Username"; + String FAMILY_NAME = "Family Name"; + String GIVEN_NAME = "Given Name"; + String EMAIL = "Email"; + String EMAIL_VERIFIED = "Email verified"; + String ROLE = "Role"; + String COLON = ":"; + String SPACE = " "; + String EMPTY_STRING = ""; + String _PRACTITIONER = "Practitioner"; + String PRACTITIONER_ROLE = "PractitionerRole"; + String CARE_TEAM = "CareTeam"; + String ORGANIZATION = "Organization"; + String ORGANIZATION_AFFILIATION = "OrganizationAffiliation"; + String CODE = "code"; + String MEMBER = "member"; + String GROUP = "Group"; + String PROXY_TO_ENV = "PROXY_TO"; + String PRACTITIONER_GROUP_CODE = "405623001"; + String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; + + String PRACTITIONER_DETAILS = "PractitionerDetails"; + + String LOCATION_HIERARCHY = "LocationHierarchy"; + + String PRACTITONER_RESOURCE_PATH = "Practitioner"; + String QUESTION_MARK = "?"; + + String EQUALS_TO_SIGN = "="; + String HTTP_GET_METHOD = "GET"; +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java similarity index 98% rename from src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java rename to customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java index d349b152..bea203bb 100644 --- a/src/main/java/com/google/fhir/gateway/plugin/PermissionAccessChecker.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import static com.google.fhir.gateway.ProxyConstants.SYNC_STRATEGY; @@ -80,7 +80,7 @@ private PermissionAccessChecker( @Override public AccessDecision checkAccess(RequestDetailsReader requestDetails) { - // For a Bundle requestDetails.getResourceName() returns null + // For a Bundle requestDetails.getResourceName() returns null if (requestDetails.getRequestType() == RequestTypeEnum.POST && requestDetails.getResourceName() == null) { return processBundle(requestDetails); @@ -327,7 +327,7 @@ public AccessChecker create( ? practitionerDetails.getFhirPractitionerDetails().getCareTeams() : Collections.singletonList(new CareTeam()); for (CareTeam careTeam : careTeams) { - if (careTeam.getIdElement() != null) { + if (careTeam.getIdElement() != null && careTeam.getIdElement().getIdPart() != null) { careTeamIds.add(careTeam.getIdElement().getIdPart()); } } diff --git a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java similarity index 93% rename from src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java rename to customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java index 2772a54a..932fdf33 100644 --- a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailEndpoint.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,26 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; - +package org.smartregister.fhir.gateway.plugins; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.FhirClientFactory; import com.google.fhir.gateway.HttpFhirClient; -import org.apache.http.HttpStatus; import com.google.fhir.gateway.TokenVerifier; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.smartregister.model.practitioner.PractitionerDetails; - +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.practitioner.PractitionerDetails; /** * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization @@ -54,6 +52,7 @@ public class PractitionerDetailEndpoint extends HttpServlet { private final HttpFhirClient fhirClient; private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + public PractitionerDetailEndpoint() throws IOException { this.tokenVerifier = TokenVerifier.createFromEnvVars(); this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); @@ -71,7 +70,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); String keycloakUuid = request.getParameter("keycloak-uuid"); - PractitionerDetails practitionerDetails = practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + PractitionerDetails practitionerDetails = + practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); response.setStatus(HttpStatus.SC_OK); } diff --git a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java similarity index 96% rename from src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java rename to customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index 26295ee9..0927a51c 100644 --- a/src/main/java/com/google/fhir/gateway/plugin/PractitionerDetailsEndpointHelper.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import static org.smartregister.utils.Constants.EMPTY_STRING; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import com.google.fhir.gateway.ProxyConstants; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -37,6 +38,7 @@ import org.hl7.fhir.r4.model.OrganizationAffiliation; import org.hl7.fhir.r4.model.Practitioner; import org.hl7.fhir.r4.model.PractitionerRole; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.model.location.LocationHierarchy; @@ -44,6 +46,8 @@ import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; import org.smartregister.utils.Constants; +import org.springframework.lang.Nullable; + public class PractitionerDetailsEndpointHelper { private static final Logger logger = LoggerFactory.getLogger(PractitionerDetailsEndpointHelper.class); @@ -154,6 +158,7 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract return responseBundle; } + @NotNull public static List getAttributedLocations(List locationHierarchies) { List parentChildrenList = locationHierarchies.stream() @@ -405,7 +410,7 @@ private Bundle getOrganizationsById(List organizationIds) { .execute(); } - private List getLocationsByIds(List locationIds) { + private @Nullable List getLocationsByIds(List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); } @@ -423,7 +428,7 @@ private List getLocationsByIds(List locationIds) { .collect(Collectors.toList()); } - private List getOfficialLocationIdentifiersByLocationIds( + private @Nullable List getOfficialLocationIdentifiersByLocationIds( List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); @@ -532,14 +537,12 @@ private List getLocationsHierarchyByOfficialLocationIdentifie } public static String createSearchTagValues(Map.Entry entry) { -// return entry.getKey() -// + ProxyConstants.CODE_URL_VALUE_SEPARATOR -// + StringUtils.join( -// entry.getValue(), -// ProxyConstants.PARAM_VALUES_SEPARATOR -// + entry.getKey() -// + ProxyConstants.CODE_URL_VALUE_SEPARATOR); - - return ""; + return entry.getKey() + + com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR + + StringUtils.join( + entry.getValue(), + com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR + + entry.getKey() + + ProxyConstants.CODE_URL_VALUE_SEPARATOR); } } diff --git a/src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java similarity index 95% rename from src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java rename to customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java index 35148fb2..3488bc8c 100644 --- a/src/main/java/com/google/fhir/gateway/plugin/ProxyConstants.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import ca.uhn.fhir.rest.api.Constants; import org.apache.http.entity.ContentType; diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java new file mode 100755 index 00000000..8b056027 --- /dev/null +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java @@ -0,0 +1,108 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway.plugins; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import com.google.fhir.gateway.BundleResources; +import com.google.fhir.gateway.ExceptionUtil; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import com.google.fhir.gateway.interfaces.ResourceFinder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import lombok.SneakyThrows; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ResourceFinderImp implements ResourceFinder { + private static final Logger logger = LoggerFactory.getLogger(ResourceFinderImp.class); + private static ResourceFinderImp instance = null; + private final FhirContext fhirContext; + + // This is supposed to be instantiated with getInstance method only. + private ResourceFinderImp(FhirContext fhirContext) { + this.fhirContext = fhirContext; + } + + private IBaseResource createResourceFromRequest(RequestDetailsReader request) { + byte[] requestContentBytes = request.loadRequestContents(); + Charset charset = request.getCharset(); + if (charset == null) { + charset = StandardCharsets.UTF_8; + } + String requestContent = new String(requestContentBytes, charset); + IParser jsonParser = fhirContext.newJsonParser(); + return jsonParser.parseResource(requestContent); + } + + @SneakyThrows + @Override + public List findResourcesInBundle(RequestDetailsReader request) { + IBaseResource resource = createResourceFromRequest(request); + if (!(resource instanceof Bundle)) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + "The provided resource is not a Bundle!", + InvalidRequestException.class.newInstance()); + } + Bundle bundle = (Bundle) resource; + + if (bundle.getType() != Bundle.BundleType.TRANSACTION) { + // Currently, support only for transaction bundles + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + "Bundle type needs to be transaction!", + InvalidRequestException.class.newInstance()); + } + + List requestTypeEnumList = new ArrayList<>(); + if (!bundle.hasEntry()) { + return requestTypeEnumList; + } + + for (Bundle.BundleEntryComponent entryComponent : bundle.getEntry()) { + Bundle.HTTPVerb httpMethod = entryComponent.getRequest().getMethod(); + if (httpMethod != Bundle.HTTPVerb.GET && !entryComponent.hasResource()) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, + "Bundle entry requires a resource field!", + InvalidRequestException.class.newInstance()); + } + + requestTypeEnumList.add( + new BundleResources( + RequestTypeEnum.valueOf(httpMethod.name()), entryComponent.getResource())); + } + + return requestTypeEnumList; + } + + // A singleton instance of this class should be used, hence the constructor is private. + public static synchronized ResourceFinderImp getInstance(FhirContext fhirContext) { + if (instance != null) { + return instance; + } + + instance = new ResourceFinderImp(fhirContext); + return instance; + } +} diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java new file mode 100644 index 00000000..8b633ade --- /dev/null +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java @@ -0,0 +1,34 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway.plugins; + +import com.google.fhir.gateway.ResourceFinderImp; + +public class RestUtil { + + private static ResourceFinderImp instance = null; + + // private final FhirContext fhirContext; + // + // public static synchronized ResourceFinderImp getInstance(FhirContext fhirContext) { + // if (instance != null) { + // return instance; + // } else { + // instance = new ResourceFinderImp(fhirContext); + // return instance; + // } + // } +} diff --git a/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java similarity index 93% rename from src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java rename to customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java index 1753eb6b..4d716294 100755 --- a/src/main/java/com/google/fhir/gateway/plugin/SyncAccessDecision.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; @@ -22,7 +22,6 @@ 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; import com.google.fhir.gateway.interfaces.RequestMutation; @@ -47,8 +46,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.google.fhir.gateway.plugin.ProxyConstants.*; - public class SyncAccessDecision implements AccessDecision { public static final String SYNC_FILTER_IGNORE_RESOURCES_FILE_ENV = "SYNC_FILTER_IGNORE_RESOURCES_FILE"; @@ -130,7 +127,8 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea requestMutation = RequestMutation.builder() .queryParams( - Map.of(TAG_SEARCH_PARAM, + Map.of( + com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM, Arrays.asList(StringUtils.join(syncFilterParameterValues, ",")))) .build(); } @@ -302,12 +300,12 @@ private Map getSyncTags( StringBuilder sb = new StringBuilder(); Map map = new HashMap<>(); - sb.append(TAG_SEARCH_PARAM); - sb.append(Literals.EQUALS); + sb.append(com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM); + sb.append(com.google.fhir.gateway.ProxyConstants.Literals.EQUALS); - addTags(LOCATION_TAG_URL, locationIds, map, sb); - addTags(ORGANISATION_TAG_URL, organizationIds, map, sb); - addTags(CARE_TEAM_TAG_URL, careTeamIds, map, sb); + addTags(com.google.fhir.gateway.ProxyConstants.LOCATION_TAG_URL, locationIds, map, sb); + addTags(com.google.fhir.gateway.ProxyConstants.ORGANISATION_TAG_URL, organizationIds, map, sb); + addTags(com.google.fhir.gateway.ProxyConstants.CARE_TEAM_TAG_URL, careTeamIds, map, sb); return map; } @@ -320,8 +318,10 @@ private void addTags( int len = values.size(); if (len > 0) { if (urlStringBuilder.length() - != (TAG_SEARCH_PARAM + com.google.fhir.gateway.plugin.ProxyConstants.Literals.EQUALS).length()) { - urlStringBuilder.append(PARAM_VALUES_SEPARATOR); + != (com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM + + com.google.fhir.gateway.ProxyConstants.Literals.EQUALS) + .length()) { + urlStringBuilder.append(com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR); } map.put(tagUrl, values.toArray(new String[0])); @@ -329,11 +329,11 @@ private void addTags( int i = 0; for (String tagValue : values) { urlStringBuilder.append(tagUrl); - urlStringBuilder.append(CODE_URL_VALUE_SEPARATOR); + urlStringBuilder.append(com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR); urlStringBuilder.append(tagValue); if (i != len - 1) { - urlStringBuilder.append(PARAM_VALUES_SEPARATOR); + urlStringBuilder.append(com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR); } i++; } @@ -353,7 +353,8 @@ private boolean isSyncUrl(RequestDetailsReader requestDetailsReader) { private boolean isResourceTypeRequest(String requestPath) { if (!TextUtils.isEmpty(requestPath)) { - String[] sections = requestPath.split(HTTP_URL_SEPARATOR); + String[] sections = + requestPath.split(com.google.fhir.gateway.ProxyConstants.HTTP_URL_SEPARATOR); return sections.length == 1 || (sections.length == 2 && TextUtils.isEmpty(sections[1])); } diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java new file mode 100755 index 00000000..ab1af4b1 --- /dev/null +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java @@ -0,0 +1,25 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway.plugins.interfaces; + +import com.google.fhir.gateway.BundleResources; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import java.util.List; + +public interface ResourceFinder { + + List findResourcesInBundle(RequestDetailsReader request); +} diff --git a/src/test/java/com/google/fhir/gateway/plugin/PermissionAccessCheckerTest.java b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java similarity index 99% rename from src/test/java/com/google/fhir/gateway/plugin/PermissionAccessCheckerTest.java rename to customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java index 4921e086..28c2d9f4 100755 --- a/src/test/java/com/google/fhir/gateway/plugin/PermissionAccessCheckerTest.java +++ b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; diff --git a/src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java similarity index 99% rename from src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java rename to customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java index afbf0a3f..a52b40dc 100755 --- a/src/test/java/com/google/fhir/gateway/plugin/SyncAccessDecisionTest.java +++ b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; diff --git a/src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java similarity index 97% rename from src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java rename to customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java index b8b91878..3d688d8c 100644 --- a/src/test/java/com/google/fhir/gateway/plugin/TestRequestDetailsToReader.java +++ b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.RequestTypeEnum; diff --git a/src/test/java/com/google/fhir/gateway/plugin/TestUtil.java b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java similarity index 94% rename from src/test/java/com/google/fhir/gateway/plugin/TestUtil.java rename to customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java index 0eb41796..20b3a673 100644 --- a/src/test/java/com/google/fhir/gateway/plugin/TestUtil.java +++ b/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java @@ -1,5 +1,5 @@ /* - * Copyright 2021-2023 Ona Systems, Inc + * Copyright 2021-2023 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.google.fhir.gateway.plugin; +package org.smartregister.fhir.gateway.plugins; import static org.mockito.Mockito.when; diff --git a/src/test/resources/hapi_sync_filter_ignored_queries.json b/customplugins/src/test/resources/hapi_sync_filter_ignored_queries.json similarity index 100% rename from src/test/resources/hapi_sync_filter_ignored_queries.json rename to customplugins/src/test/resources/hapi_sync_filter_ignored_queries.json diff --git a/src/test/resources/test_bundle_transaction.json b/customplugins/src/test/resources/test_bundle_transaction.json similarity index 100% rename from src/test/resources/test_bundle_transaction.json rename to customplugins/src/test/resources/test_bundle_transaction.json diff --git a/src/test/resources/test_list_resource.json b/customplugins/src/test/resources/test_list_resource.json similarity index 100% rename from src/test/resources/test_list_resource.json rename to customplugins/src/test/resources/test_list_resource.json diff --git a/exec/README.md b/exec/README.md new file mode 100644 index 00000000..e0cab368 --- /dev/null +++ b/exec/README.md @@ -0,0 +1,12 @@ +# Sample application + +This module is to show simple examples of how to use the FHIR Gateway. The +minimal application is +[MainApp](src/main/java/com/google/fhir/gateway/MainApp.java). With this single +class, you can create an executable app with the Gateway [server](../server) and +all of the `AccessChecker` [plugins](../plugins), namely +[ListAccessChecker](../plugins/src/main/java/com/google/fhir/gateway/plugin/ListAccessChecker.java) +and +[PatientAccessChecker](../plugins/src/main/java/com/google/fhir/gateway/plugin/PatientAccessChecker.java). + +Two other classes are provided to show how to implement custom endpoints. diff --git a/exec/pom.xml b/exec/pom.xml new file mode 100755 index 00000000..2b25da63 --- /dev/null +++ b/exec/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + org.smartregister + opensrp-fhir-proxy-plugin + 1.0.0 + + + exec + jar + + + ${project.parent.basedir} + 2.7.5 + + true + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + + + + + + com.google.fhir.gateway + server + 0.1.32 + + + + com.google.fhir.gateway + plugins + 0.1.32 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + ${project.parent.artifactId}-${project.artifactId} + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring-boot.version} + + + repackage + + repackage + + + org.smartregister.fhir.gateway.MainApp + + ZIP + + + + + + + + diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java b/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java new file mode 100644 index 00000000..3d6e2662 --- /dev/null +++ b/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +/** + * This class shows the minimum that is required to create a FHIR Gateway with all AccessChecker + * plugins defined in "com.google.fhir.gateway.plugin". + */ +@SpringBootApplication( + scanBasePackages = {"org.smartregister.fhir.gateway.plugins", "com.google.fhir.gateway.plugin"}) +@ServletComponentScan({"org.smartregister.fhir.gateway", "com.google.fhir.gateway"}) +public class MainApp { + + public static void main(String[] args) { + System.out.println("Running exec module"); + SpringApplication.run(MainApp.class, args); + } +} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java new file mode 100644 index 00000000..8a8ee296 --- /dev/null +++ b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java @@ -0,0 +1,79 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway; + +import ca.uhn.fhir.rest.client.impl.GenericClient; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.fhir.gateway.FhirClientFactory; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.TokenVerifier; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.practitioner.PractitionerDetails; + +/** + * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization + * header. Although it is not a standard FHIR query, but it uses the FHIR server to construct the + * response. In this example, it inspects the JWT and depending on its claims, constructs the list + * of Patient IDs that the user has access to. + * + *

The two types of tokens resemble {@link com.google.fhir.gateway.plugin.ListAccessChecker} and + * {@link com.google.fhir.gateway.plugin.PatientAccessChecker} expected tokens. But those are just + * picked as examples and this custom endpoint is independent of any {@link + * com.google.fhir.gateway.interfaces.AccessChecker}. + */ +@WebServlet("/PractitionerDetail") +public class PractitionerDetailEndpoint extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(PractitionerDetailEndpoint.class); + private final TokenVerifier tokenVerifier; + + private final HttpFhirClient fhirClient; + + private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + + public PractitionerDetailEndpoint() throws IOException { + this.tokenVerifier = TokenVerifier.createFromEnvVars(); + this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + // Check the Bearer token to be a valid JWT with required claims. + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) { + throw new ServletException("No Authorization header provided!"); + } + List patientIds = new ArrayList<>(); + // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: + DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); + String keycloakUuid = request.getParameter("keycloak-uuid"); + PractitionerDetails practitionerDetails = + practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); + response.setStatus(HttpStatus.SC_OK); + } +} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java new file mode 100644 index 00000000..9a159313 --- /dev/null +++ b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java @@ -0,0 +1,534 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway; + +import static org.smartregister.utils.Constants.EMPTY_STRING; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.hl7.fhir.r4.model.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.ParentChildrenMap; +import org.smartregister.model.practitioner.FhirPractitionerDetails; +import org.smartregister.model.practitioner.PractitionerDetails; +import org.smartregister.utils.Constants; + +public class PractitionerDetailsEndpointHelper { + private static final Logger logger = + LoggerFactory.getLogger(PractitionerDetailsEndpointHelper.class); + public static final String PRACTITIONER_GROUP_CODE = "405623001"; + public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; + public static final Bundle EMPTY_BUNDLE = new Bundle(); + private IGenericClient r4FhirClient; + + public PractitionerDetailsEndpointHelper(IGenericClient fhirClient) { + this.r4FhirClient = fhirClient; + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } + + public PractitionerDetails getPractitionerDetailsByKeycloakId(String keycloakUuid) { + PractitionerDetails practitionerDetails = new PractitionerDetails(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); + } + + return practitionerDetails; + } + + public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { + Bundle bundle = new Bundle(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + } + + return bundle; + } + + private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { + Bundle responseBundle = new Bundle(); + List attributedPractitioners = new ArrayList<>(); + PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); + // Get other guys. + + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamList); + List supervisorCareTeamOrganizationLocationIds = + getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); + List officialLocationIds = + getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); + List locationHierarchies = + getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); + List attributedLocationsList = getAttributedLocations(locationHierarchies); + List attributedOrganizationIds = + getOrganizationIdsByLocationIds(attributedLocationsList); + + // Get care teams by organization Ids + List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); + + for (CareTeam careTeam : careTeamList) { + attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); + } + + careTeamList.addAll(attributedCareTeams); + + for (CareTeam careTeam : careTeamList) { + // Add current supervisor practitioners + attributedPractitioners.addAll( + careTeam.getParticipant().stream() + .filter( + it -> + it.hasMember() + && it.getMember() + .getReference() + .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) + .map( + it -> + getPractitionerByIdentifier( + getReferenceIDPart(it.getMember().getReference()))) + .collect(Collectors.toList())); + } + + List bundleEntryComponentList = new ArrayList<>(); + + for (Practitioner attributedPractitioner : attributedPractitioners) { + bundleEntryComponentList.add( + new Bundle.BundleEntryComponent() + .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); + } + + responseBundle.setEntry(bundleEntryComponentList); + responseBundle.setTotal(bundleEntryComponentList.size()); + return responseBundle; + } + + public static List getAttributedLocations(List locationHierarchies) { + List parentChildrenList = + locationHierarchies.stream() + .flatMap( + locationHierarchy -> + locationHierarchy + .getLocationHierarchyTree() + .getLocationsHierarchy() + .getParentChildren() + .stream()) + .collect(Collectors.toList()); + List attributedLocationsList = + parentChildrenList.stream() + .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) + .map(it -> getReferenceIDPart(it.toString())) + .collect(Collectors.toList()); + return attributedLocationsList; + } + + private List getOrganizationIdsByLocationIds(List attributedLocationsList) { + if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { + return new ArrayList<>(); + } + + Bundle organizationAffiliationsBundle = + getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) + .returnBundle(Bundle.class) + .execute(); + + return organizationAffiliationsBundle.getEntry().stream() + .map( + bundleEntryComponent -> + getReferenceIDPart( + ((OrganizationAffiliation) bundleEntryComponent.getResource()) + .getOrganization() + .getReference())) + .distinct() + .collect(Collectors.toList()); + } + + private String getPractitionerIdentifier(Practitioner practitioner) { + String practitionerId = EMPTY_STRING; + if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { + practitionerId = practitioner.getIdElement().getIdPart(); + } + return practitionerId; + } + + private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); + String practitionerId = getPractitionerIdentifier(practitioner); + + logger.info("Searching for care teams for practitioner with id: " + practitioner); + Bundle careTeams = getCareTeams(practitionerId); + List careTeamsList = mapBundleToCareTeams(careTeams); + fhirPractitionerDetails.setCareTeams(careTeamsList); + fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); + + logger.info("Searching for Organizations tied with CareTeams: "); + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamsList); + + Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); + logger.info("Managing Organization are fetched"); + + List managingOrganizationTeams = + mapBundleToOrganizations(careTeamManagingOrganizations); + + logger.info("Searching for organizations of practitioner with id: " + practitioner); + + List practitionerRoleList = + getPractitionerRolesByPractitionerId(practitionerId); + logger.info("Practitioner Roles are fetched"); + + List practitionerOrganizationIds = + getOrganizationIdsByPractitionerRoles(practitionerRoleList); + + Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); + + List teams = mapBundleToOrganizations(practitionerOrganizations); + // TODO Fix Distinct + List bothOrganizations = + Stream.concat(managingOrganizationTeams.stream(), teams.stream()) + .distinct() + .collect(Collectors.toList()); + + fhirPractitionerDetails.setOrganizations(bothOrganizations); + fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); + + Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); + logger.info("Groups are fetched"); + + List groupsList = mapBundleToGroups(groupsBundle); + fhirPractitionerDetails.setGroups(groupsList); + fhirPractitionerDetails.setId(practitionerId); + + logger.info("Searching for locations by organizations"); + + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle( + Stream.concat( + careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) + .distinct() + .collect(Collectors.toList())); + + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + + fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); + + List locationIds = + getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + + List locationsIdentifiers = + getOfficialLocationIdentifiersByLocationIds( + locationIds); // TODO Investigate why the Location ID and official identifiers are + // different + + logger.info("Searching for location hierarchy list by locations identifiers"); + List locationHierarchyList = + getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); + fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); + + logger.info("Searching for locations by ids"); + List locationsList = getLocationsByIds(locationIds); + fhirPractitionerDetails.setLocations(locationsList); + + practitionerDetails.setId(practitionerId); + practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); + + return practitionerDetails; + } + + private List mapBundleToOrganizations(Bundle organizationBundle) { + return organizationBundle.getEntry().stream() + .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private Bundle getGroupsAssignedToPractitioner(String practitionerId) { + return getFhirClientForR4() + .search() + .forResource(Group.class) + .where(Group.MEMBER.hasId(practitionerId)) + .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) + .returnBundle(Bundle.class) + .execute(); + } + + public static Predicate distinctByKey(Function keyExtractor) { + Set seen = ConcurrentHashMap.newKeySet(); + return t -> seen.add(keyExtractor.apply(t)); + } + + private List getPractitionerRolesByPractitionerId(String practitionerId) { + Bundle practitionerRoles = getPractitionerRoles(practitionerId); + return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); + } + + private List getOrganizationIdsByPractitionerRoles( + List practitionerRoles) { + return practitionerRoles.stream() + .filter(practitionerRole -> practitionerRole.hasOrganization()) + .map(it -> getReferenceIDPart(it.getOrganization().getReference())) + .collect(Collectors.toList()); + } + + private Practitioner getPractitionerByIdentifier(String identifier) { + Bundle resultBundle = + getFhirClientForR4() + .search() + .forResource(Practitioner.class) + .where(Practitioner.IDENTIFIER.exactly().identifier(identifier)) + .returnBundle(Bundle.class) + .execute(); + + return resultBundle != null + ? (Practitioner) resultBundle.getEntryFirstRep().getResource() + : null; + } + + private List getCareTeamsByOrganizationIds(List organizationIds) { + if (organizationIds.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasAnyOfIds( + organizationIds.stream() + .map( + it -> + Enumerations.ResourceType.ORGANIZATION.toCode() + + Constants.FORWARD_SLASH + + it) + .collect(Collectors.toList()))) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) + .map(it -> ((CareTeam) it.getResource())) + .collect(Collectors.toList()); + } + + private Bundle getCareTeams(String practitionerId) { + logger.info("Searching for Care Teams with practitioner id :" + practitionerId); + + return getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasId( + Enumerations.ResourceType.PRACTITIONER.toCode() + + Constants.FORWARD_SLASH + + practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private Bundle getPractitionerRoles(String practitionerId) { + logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); + return getFhirClientForR4() + .search() + .forResource(PractitionerRole.class) + .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private static String getReferenceIDPart(String reference) { + return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); + } + + private Bundle getOrganizationsById(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(Organization.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private List getLocationsByIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) + .returnBundle(Bundle.class) + .execute(); + + return locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + } + + private List getOfficialLocationIdentifiersByLocationIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + List locations = getLocationsByIds(locationIds); + + return locations.stream() + .map( + it -> + it.getIdentifier().stream() + .filter( + id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) + .map(it2 -> it2.getValue()) + .collect(Collectors.toList())) + .flatMap(it3 -> it3.stream()) + .collect(Collectors.toList()); + } + + private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { + if (organizationIds == null || organizationIds.isEmpty()) { + return new ArrayList<>(); + } + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + } + + private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private List getLocationIdentifiersByOrganizationAffiliations( + List organizationAffiliations) { + + return organizationAffiliations.stream() + .map( + organizationAffiliation -> + getReferenceIDPart( + organizationAffiliation.getLocation().stream() + .findFirst() + .get() + .getReference())) + .collect(Collectors.toList()); + } + + private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { + logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); + return careTeamsList.stream() + .filter(careTeam -> careTeam.hasManagingOrganization()) + .flatMap(it -> it.getManagingOrganization().stream()) + .map(it -> getReferenceIDPart(it.getReference())) + .collect(Collectors.toList()); + } + + private List mapBundleToCareTeams(Bundle careTeams) { + return careTeams.getEntry().stream() + .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToPractitionerRolesWithOrganization( + Bundle practitionerRoles) { + return practitionerRoles.getEntry().stream() + .map(it -> (PractitionerRole) it.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToGroups(Bundle groupsBundle) { + return groupsBundle.getEntry().stream() + .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToOrganizationAffiliation( + Bundle organizationAffiliationBundle) { + return organizationAffiliationBundle.getEntry().stream() + .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List getLocationsHierarchyByOfficialLocationIdentifiers( + List officialLocationIdentifiers) { + if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(LocationHierarchy.class) + .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .map(it -> ((LocationHierarchy) it.getResource())) + .collect(Collectors.toList()); + } + + public static String createSearchTagValues(Map.Entry entry) { + // return entry.getKey() + // + ProxyConstants.CODE_URL_VALUE_SEPARATOR + // + StringUtils.join( + // entry.getValue(), + // ProxyConstants.PARAM_VALUES_SEPARATOR + // + entry.getKey() + // + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + + return ""; + } +} diff --git a/exec/src/test/java/org/smartregister/fhir/gateway/MainAppTest.java b/exec/src/test/java/org/smartregister/fhir/gateway/MainAppTest.java new file mode 100644 index 00000000..10d89d0a --- /dev/null +++ b/exec/src/test/java/org/smartregister/fhir/gateway/MainAppTest.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +// TODO change this test to fail if the expected plugins cannot be found. +@RunWith(SpringRunner.class) +@SpringBootTest +public class MainAppTest { + + @Test + public void contextLoads() {} +} diff --git a/license-header.txt b/license-header.txt index bcf42506..edb10158 100644 --- a/license-header.txt +++ b/license-header.txt @@ -1,4 +1,4 @@ -Copyright ${license.git.copyrightYears} Ona Systems, Inc +Copyright ${license.git.copyrightYears} Google LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/pom.xml b/pom.xml index 4339e3f8..ff58866c 100755 --- a/pom.xml +++ b/pom.xml @@ -1,7 +1,7 @@ - - - - - - - - - - - - - - UTF-8 - 11 - 11 - ${project.basedir} - 6.0.1 - 0.2.0 - 4.1 - - - - ${project.parent.groupId} - server - 0.1.32 - - - - javax.servlet - javax.servlet-api - 4.0.1 - provided - - - ca.uhn.hapi.fhir - hapi-fhir-client - ${hapifhir_version} - - - org.smartregister - fhir-common-utils - 0.0.9-SNAPSHOT - compile - - + + exec + customplugins + diff --git a/src/test/resources/bundle_empty.json b/src/test/resources/bundle_empty.json deleted file mode 100644 index 0fc287d6..00000000 --- a/src/test/resources/bundle_empty.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "link": [ - { - "relation": "search", - "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140TTTT" - }, - { - "relation": "first", - "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140TTTT" - }, - { - "relation": "self", - "url": "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/?_elements=id&_id=patient-list-example&item=Patient%2Fbe92a43f-de46-affa-b131-bbf9eea51140TTTT" - } - ], - "resourceType": "Bundle", - "total": 0, - "type": "searchset" -} diff --git a/src/test/resources/bundle_transaction_patient_and_non_patients.json b/src/test/resources/bundle_transaction_patient_and_non_patients.json deleted file mode 100644 index e64eadf9..00000000 --- a/src/test/resources/bundle_transaction_patient_and_non_patients.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "resourceType": "Bundle", - "type": "transaction", - "entry": [ - { - "fullUrl": "Observation/observation-jamess-bond-id-1", - "resource": { - "category": [ - { - "coding": [ - { - "code": "laboratory", - "display": "laboratory", - "system": "http://terminology.hl7.org/CodeSystem/observation-category" - } - ] - } - ], - "code": { - "coding": [ - { - "code": "33914-3", - "display": "Estimated Glomerular Filtration Rate", - "system": "http://loinc.org" - } - ], - "text": "Estimated Glomerular Filtration Rate" - }, - "effectiveDateTime": "2020-10-01T18:56:10-04:00", - "encounter": { - "reference": "Encounter/encounter-jamess-bond-id-1" - }, - "id": "observation-jamess-bond-id-1", - "issued": "2020-10-01T18:56:10.396-04:00", - "resourceType": "Observation", - "status": "final", - "subject": { - "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" - }, - "performer":[{ - "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" - }], - "valueQuantity": { - "code": "mL/min/{1.73_m2}", - "system": "http://unitsofmeasure.org", - "unit": "mL/min/{1.73_m2}", - "value": 76.02971496321274 - } - }, - "request": { - "method": "PUT", - "url": "Observation?_id=observation-jamess-bond-id-1&subject=Patient/420e791b-e419-c19b-3144-29e101c2c12f" - } - }, - { - "fullUrl": "Encounter/encounter-jamess-bond-id-1", - "resource": { - "class": { - "code": "AMB", - "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode" - }, - "id": "encounter-jamess-bond-id-1", - "period": { - "end": "2015-07-01T19:11:10-04:00", - "start": "2015-07-01T18:56:10-04:00" - }, - "reasonCode": [ - { - "coding": [ - { - "code": "444814009", - "display": "Viral sinusitis (disorder)", - "system": "http://snomed.info/sct" - } - ] - } - ], - "resourceType": "Encounter", - "status": "finished", - "subject": { - "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" - }, - "type": [ - { - "coding": [ - { - "code": "185345009", - "display": "Encounter for symptom", - "system": "http://snomed.info/sct" - } - ], - "text": "Encounter for symptom" - } - ] - }, - "request": { - "method": "PUT", - "url": "Encounter?_id=encounter-jamess-bond-id-1&patient=Patient/420e791b-e419-c19b-3144-29e101c2c12f" - } - }, - { - "resource": { - "resourceType": "Patient", - "name": [ - { - "family": "Smith", - "given": [ - "Darcy" - ] - } - ], - "gender": "female", - "address": [ - { - "line": [ - "123 Main St." - ], - "city": "Anycity", - "state": "CA", - "postalCode": "12345" - } - ] - }, - "request": { - "method": "PUT", - "url": "Patient/db6e42c7-04fc-4d9d-b394-9ff33a41e178" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/bundle_transaction_put_patient.json b/src/test/resources/bundle_transaction_put_patient.json deleted file mode 100644 index 4b91d61a..00000000 --- a/src/test/resources/bundle_transaction_put_patient.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "resourceType": "Bundle", - "type": "transaction", - "entry": [ - { - "resource": { - "resourceType": "Patient", - "name": [ - { - "family": "Smith", - "given": [ - "Darcy" - ] - } - ], - "gender": "female", - "address": [ - { - "line": [ - "123 Main St." - ], - "city": "Anycity", - "state": "CA", - "postalCode": "12345" - } - ] - }, - "request": { - "method": "PUT", - "url": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" - } - }, - { - "resource": { - "resourceType": "Patient", - "name": [ - { - "family": "Smith", - "given": [ - "Darcy" - ] - } - ], - "gender": "female", - "address": [ - { - "line": [ - "123 Main St." - ], - "city": "Anycity", - "state": "CA", - "postalCode": "12345" - } - ] - }, - "request": { - "method": "PUT", - "url": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" - } - } - ] -} \ No newline at end of file diff --git a/src/test/resources/patient-list-example.json b/src/test/resources/patient-list-example.json deleted file mode 100644 index f63baaa6..00000000 --- a/src/test/resources/patient-list-example.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "entry": [ - { - "item": { - "reference": "Patient/be92a43f-de46-affa-b131-bbf9eea51140" - } - }, - { - "item": { - "reference": "Patient/420e791b-e419-c19b-3144-29e101c2c12f" - } - } - ], - "id": "patient-list-example", - "meta": { - "lastUpdated": "2021-11-25T18:54:23.389580+00:00", - "versionId": "MTYzNzg2NjQ2MzM4OTU4MDAwMA" - }, - "mode": "working", - "resourceType": "List", - "status": "current" -} \ No newline at end of file diff --git a/src/test/resources/test_patient.json b/src/test/resources/test_patient.json deleted file mode 100644 index 384d0a52..00000000 --- a/src/test/resources/test_patient.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "address": [ - { - "city": "Braintree", - "country": "US", - "extension": [ - { - "extension": [ - { - "url": "latitude", - "valueDecimal": 42.21111261554208 - }, - { - "url": "longitude", - "valueDecimal": -70.96074217780242 - } - ], - "url": "http://hl7.org/fhir/StructureDefinition/geolocation" - } - ], - "line": [ - "864 Schuster Rue" - ], - "postalCode": "02184", - "state": "MA" - } - ], - "birthDate": "1971-01-13", - "gender": "male", - "id": "be92a43f-de46-affa-b131-bbf9eea51140", - "identifier": [ - { - "system": "https://github.com/synthetichealth/synthea", - "value": "be92a43f-de46-affa-b131-bbf9eea51140" - }, - { - "system": "http://hospital.smarthealthit.org", - "type": { - "coding": [ - { - "code": "MR", - "display": "Medical Record Number", - "system": "http://terminology.hl7.org/CodeSystem/v2-0203" - } - ], - "text": "Medical Record Number" - }, - "value": "be92a43f-de46-affa-b131-bbf9eea51140" - }, - { - "system": "http://hl7.org/fhir/sid/us-ssn", - "type": { - "coding": [ - { - "code": "SS", - "display": "Social Security Number", - "system": "http://terminology.hl7.org/CodeSystem/v2-0203" - } - ], - "text": "Social Security Number" - }, - "value": "999-17-8182" - } - ], - "maritalStatus": { - "coding": [ - { - "code": "S", - "display": "S", - "system": "http://terminology.hl7.org/CodeSystem/v3-MaritalStatus" - } - ], - "text": "S" - }, - "meta": { - "lastUpdated": "2021-11-25T19:03:28.085054+00:00", - "profile": [ - "http://hl7.org/fhir/us/core/StructureDefinition/us-core-patient" - ], - "versionId": "MTYzNzg2NzAwODA4NTA1NDAwMA" - }, - "multipleBirthBoolean": false, - "name": [ - { - "family": "Anderson154", - "given": [ - "Micheal721", - "NEW_NAME" - ], - "prefix": [ - "Mr." - ], - "use": "official" - } - ], - "resourceType": "Patient", - "telecom": [ - { - "system": "phone", - "use": "home", - "value": "555-435-4405" - } - ], - "text": { - "div": "
Generated by Synthea.Version identifier: a3482c8\n . Person seed: -3805195760489406909 Population seed: 1632186774891
", - "status": "generated" - } -} From 6a4429c742a40a432a150b54b63848bf1c46a9c5 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Mon, 25 Sep 2023 13:26:29 +0500 Subject: [PATCH 06/22] Issue#5 : Move OpenSRP Specific Changes to Fhir Gateway Plugins (custom repo) --- .../plugins/PractitionerDetailEndpoint.java | 511 ++++++++++++++++- .../gateway/PractitionerDetailEndpoint.java | 516 +++++++++++++++++- 2 files changed, 1018 insertions(+), 9 deletions(-) diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java index 932fdf33..2434947a 100644 --- a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java @@ -15,22 +15,39 @@ */ package org.smartregister.fhir.gateway.plugins; +import static org.smartregister.utils.Constants.EMPTY_STRING; + +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.FhirClientFactory; import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.ProxyConstants; import com.google.fhir.gateway.TokenVerifier; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; +import org.hl7.fhir.r4.model.*; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.ParentChildrenMap; +import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; +import org.smartregister.utils.Constants; +import org.springframework.lang.Nullable; /** * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization @@ -51,6 +68,10 @@ public class PractitionerDetailEndpoint extends HttpServlet { private final HttpFhirClient fhirClient; + public static final String PRACTITIONER_GROUP_CODE = "405623001"; + public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; + public static final Bundle EMPTY_BUNDLE = new Bundle(); + private IGenericClient r4FhirClient; private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; public PractitionerDetailEndpoint() throws IOException { @@ -70,9 +91,491 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); String keycloakUuid = request.getParameter("keycloak-uuid"); - PractitionerDetails practitionerDetails = - practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + // PractitionerDetails practitionerDetails = + // practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); + } response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); response.setStatus(HttpStatus.SC_OK); } + + public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { + Bundle bundle = new Bundle(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + } + + return bundle; + } + + private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { + Bundle responseBundle = new Bundle(); + List attributedPractitioners = new ArrayList<>(); + PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); + // Get other guys. + + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamList); + List supervisorCareTeamOrganizationLocationIds = + getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); + List officialLocationIds = + getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); + List locationHierarchies = + getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); + List attributedLocationsList = getAttributedLocations(locationHierarchies); + List attributedOrganizationIds = + getOrganizationIdsByLocationIds(attributedLocationsList); + + // Get care teams by organization Ids + List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); + + for (CareTeam careTeam : careTeamList) { + attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); + } + + careTeamList.addAll(attributedCareTeams); + + for (CareTeam careTeam : careTeamList) { + // Add current supervisor practitioners + attributedPractitioners.addAll( + careTeam.getParticipant().stream() + .filter( + it -> + it.hasMember() + && it.getMember() + .getReference() + .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) + .map( + it -> + getPractitionerByIdentifier( + getReferenceIDPart(it.getMember().getReference()))) + .collect(Collectors.toList())); + } + + List bundleEntryComponentList = new ArrayList<>(); + + for (Practitioner attributedPractitioner : attributedPractitioners) { + bundleEntryComponentList.add( + new Bundle.BundleEntryComponent() + .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); + } + + responseBundle.setEntry(bundleEntryComponentList); + responseBundle.setTotal(bundleEntryComponentList.size()); + return responseBundle; + } + + @NotNull + public static List getAttributedLocations(List locationHierarchies) { + List parentChildrenList = + locationHierarchies.stream() + .flatMap( + locationHierarchy -> + locationHierarchy + .getLocationHierarchyTree() + .getLocationsHierarchy() + .getParentChildren() + .stream()) + .collect(Collectors.toList()); + List attributedLocationsList = + parentChildrenList.stream() + .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) + .map(it -> getReferenceIDPart(it.toString())) + .collect(Collectors.toList()); + return attributedLocationsList; + } + + private List getOrganizationIdsByLocationIds(List attributedLocationsList) { + if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { + return new ArrayList<>(); + } + + Bundle organizationAffiliationsBundle = + getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) + .returnBundle(Bundle.class) + .execute(); + + return organizationAffiliationsBundle.getEntry().stream() + .map( + bundleEntryComponent -> + getReferenceIDPart( + ((OrganizationAffiliation) bundleEntryComponent.getResource()) + .getOrganization() + .getReference())) + .distinct() + .collect(Collectors.toList()); + } + + private String getPractitionerIdentifier(Practitioner practitioner) { + String practitionerId = EMPTY_STRING; + if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { + practitionerId = practitioner.getIdElement().getIdPart(); + } + return practitionerId; + } + + private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); + String practitionerId = getPractitionerIdentifier(practitioner); + + logger.info("Searching for care teams for practitioner with id: " + practitioner); + Bundle careTeams = getCareTeams(practitionerId); + List careTeamsList = mapBundleToCareTeams(careTeams); + fhirPractitionerDetails.setCareTeams(careTeamsList); + fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); + + logger.info("Searching for Organizations tied with CareTeams: "); + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamsList); + + Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); + logger.info("Managing Organization are fetched"); + + List managingOrganizationTeams = + mapBundleToOrganizations(careTeamManagingOrganizations); + + logger.info("Searching for organizations of practitioner with id: " + practitioner); + + List practitionerRoleList = + getPractitionerRolesByPractitionerId(practitionerId); + logger.info("Practitioner Roles are fetched"); + + List practitionerOrganizationIds = + getOrganizationIdsByPractitionerRoles(practitionerRoleList); + + Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); + + List teams = mapBundleToOrganizations(practitionerOrganizations); + // TODO Fix Distinct + List bothOrganizations = + Stream.concat(managingOrganizationTeams.stream(), teams.stream()) + .distinct() + .collect(Collectors.toList()); + + fhirPractitionerDetails.setOrganizations(bothOrganizations); + fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); + + Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); + logger.info("Groups are fetched"); + + List groupsList = mapBundleToGroups(groupsBundle); + fhirPractitionerDetails.setGroups(groupsList); + fhirPractitionerDetails.setId(practitionerId); + + logger.info("Searching for locations by organizations"); + + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle( + Stream.concat( + careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) + .distinct() + .collect(Collectors.toList())); + + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + + fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); + + List locationIds = + getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + + List locationsIdentifiers = + getOfficialLocationIdentifiersByLocationIds( + locationIds); // TODO Investigate why the Location ID and official identifiers are + // different + + logger.info("Searching for location hierarchy list by locations identifiers"); + List locationHierarchyList = + getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); + fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); + + logger.info("Searching for locations by ids"); + List locationsList = getLocationsByIds(locationIds); + fhirPractitionerDetails.setLocations(locationsList); + + practitionerDetails.setId(practitionerId); + practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); + + return practitionerDetails; + } + + private List mapBundleToOrganizations(Bundle organizationBundle) { + return organizationBundle.getEntry().stream() + .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private Bundle getGroupsAssignedToPractitioner(String practitionerId) { + return getFhirClientForR4() + .search() + .forResource(Group.class) + .where(Group.MEMBER.hasId(practitionerId)) + .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) + .returnBundle(Bundle.class) + .execute(); + } + + public static Predicate distinctByKey(Function keyExtractor) { + Set seen = ConcurrentHashMap.newKeySet(); + return t -> seen.add(keyExtractor.apply(t)); + } + + private List getPractitionerRolesByPractitionerId(String practitionerId) { + Bundle practitionerRoles = getPractitionerRoles(practitionerId); + return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); + } + + private List getOrganizationIdsByPractitionerRoles( + List practitionerRoles) { + return practitionerRoles.stream() + .filter(practitionerRole -> practitionerRole.hasOrganization()) + .map(it -> getReferenceIDPart(it.getOrganization().getReference())) + .collect(Collectors.toList()); + } + + private Practitioner getPractitionerByIdentifier(String identifier) { + Bundle resultBundle = + getFhirClientForR4() + .search() + .forResource(Practitioner.class) + .where(Practitioner.IDENTIFIER.exactly().identifier(identifier)) + .returnBundle(Bundle.class) + .execute(); + + return resultBundle != null + ? (Practitioner) resultBundle.getEntryFirstRep().getResource() + : null; + } + + private List getCareTeamsByOrganizationIds(List organizationIds) { + if (organizationIds.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasAnyOfIds( + organizationIds.stream() + .map( + it -> + Enumerations.ResourceType.ORGANIZATION.toCode() + + Constants.FORWARD_SLASH + + it) + .collect(Collectors.toList()))) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) + .map(it -> ((CareTeam) it.getResource())) + .collect(Collectors.toList()); + } + + private Bundle getCareTeams(String practitionerId) { + logger.info("Searching for Care Teams with practitioner id :" + practitionerId); + + return getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasId( + Enumerations.ResourceType.PRACTITIONER.toCode() + + Constants.FORWARD_SLASH + + practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private Bundle getPractitionerRoles(String practitionerId) { + logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); + return getFhirClientForR4() + .search() + .forResource(PractitionerRole.class) + .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private static String getReferenceIDPart(String reference) { + return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); + } + + private Bundle getOrganizationsById(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(Organization.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private @Nullable List getLocationsByIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) + .returnBundle(Bundle.class) + .execute(); + + return locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + } + + private @Nullable List getOfficialLocationIdentifiersByLocationIds( + List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + List locations = getLocationsByIds(locationIds); + + return locations.stream() + .map( + it -> + it.getIdentifier().stream() + .filter( + id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) + .map(it2 -> it2.getValue()) + .collect(Collectors.toList())) + .flatMap(it3 -> it3.stream()) + .collect(Collectors.toList()); + } + + private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { + if (organizationIds == null || organizationIds.isEmpty()) { + return new ArrayList<>(); + } + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + } + + private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private List getLocationIdentifiersByOrganizationAffiliations( + List organizationAffiliations) { + + return organizationAffiliations.stream() + .map( + organizationAffiliation -> + getReferenceIDPart( + organizationAffiliation.getLocation().stream() + .findFirst() + .get() + .getReference())) + .collect(Collectors.toList()); + } + + private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { + logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); + return careTeamsList.stream() + .filter(careTeam -> careTeam.hasManagingOrganization()) + .flatMap(it -> it.getManagingOrganization().stream()) + .map(it -> getReferenceIDPart(it.getReference())) + .collect(Collectors.toList()); + } + + private List mapBundleToCareTeams(Bundle careTeams) { + return careTeams.getEntry().stream() + .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToPractitionerRolesWithOrganization( + Bundle practitionerRoles) { + return practitionerRoles.getEntry().stream() + .map(it -> (PractitionerRole) it.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToGroups(Bundle groupsBundle) { + return groupsBundle.getEntry().stream() + .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToOrganizationAffiliation( + Bundle organizationAffiliationBundle) { + return organizationAffiliationBundle.getEntry().stream() + .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List getLocationsHierarchyByOfficialLocationIdentifiers( + List officialLocationIdentifiers) { + if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(LocationHierarchy.class) + .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .map(it -> ((LocationHierarchy) it.getResource())) + .collect(Collectors.toList()); + } + + public static String createSearchTagValues(Map.Entry entry) { + return entry.getKey() + + com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR + + StringUtils.join( + entry.getValue(), + com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR + + entry.getKey() + + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } } diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java index 8a8ee296..329ebea5 100644 --- a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java +++ b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java @@ -15,23 +15,41 @@ */ package org.smartregister.fhir.gateway; -import ca.uhn.fhir.rest.client.impl.GenericClient; +import static com.google.fhir.gateway.util.Constants.PROXY_TO_ENV; +import static org.smartregister.utils.Constants.EMPTY_STRING; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.FhirClientFactory; import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.ProxyConstants; import com.google.fhir.gateway.TokenVerifier; import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; +import org.hl7.fhir.r4.model.*; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.ParentChildrenMap; +import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; +import org.smartregister.utils.Constants; +import org.springframework.lang.Nullable; /** * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization @@ -52,8 +70,14 @@ public class PractitionerDetailEndpoint extends HttpServlet { private final HttpFhirClient fhirClient; + public static final String PRACTITIONER_GROUP_CODE = "405623001"; + public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; + public static final Bundle EMPTY_BUNDLE = new Bundle(); + private IGenericClient r4FhirClient; private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + private FhirContext fhirR4Context = FhirContext.forR4(); + public PractitionerDetailEndpoint() throws IOException { this.tokenVerifier = TokenVerifier.createFromEnvVars(); this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); @@ -71,9 +95,491 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); String keycloakUuid = request.getParameter("keycloak-uuid"); - PractitionerDetails practitionerDetails = - practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + // PractitionerDetails practitionerDetails = + // practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); + } response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); response.setStatus(HttpStatus.SC_OK); } + + public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { + Bundle bundle = new Bundle(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + } + + return bundle; + } + + private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { + Bundle responseBundle = new Bundle(); + List attributedPractitioners = new ArrayList<>(); + PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); + // Get other guys. + + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamList); + List supervisorCareTeamOrganizationLocationIds = + getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); + List officialLocationIds = + getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); + List locationHierarchies = + getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); + List attributedLocationsList = getAttributedLocations(locationHierarchies); + List attributedOrganizationIds = + getOrganizationIdsByLocationIds(attributedLocationsList); + + // Get care teams by organization Ids + List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); + + for (CareTeam careTeam : careTeamList) { + attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); + } + + careTeamList.addAll(attributedCareTeams); + + for (CareTeam careTeam : careTeamList) { + // Add current supervisor practitioners + attributedPractitioners.addAll( + careTeam.getParticipant().stream() + .filter( + it -> + it.hasMember() + && it.getMember() + .getReference() + .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) + .map( + it -> + getPractitionerByIdentifier( + getReferenceIDPart(it.getMember().getReference()))) + .collect(Collectors.toList())); + } + + List bundleEntryComponentList = new ArrayList<>(); + + for (Practitioner attributedPractitioner : attributedPractitioners) { + bundleEntryComponentList.add( + new Bundle.BundleEntryComponent() + .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); + } + + responseBundle.setEntry(bundleEntryComponentList); + responseBundle.setTotal(bundleEntryComponentList.size()); + return responseBundle; + } + + @NotNull + public static List getAttributedLocations(List locationHierarchies) { + List parentChildrenList = + locationHierarchies.stream() + .flatMap( + locationHierarchy -> + locationHierarchy + .getLocationHierarchyTree() + .getLocationsHierarchy() + .getParentChildren() + .stream()) + .collect(Collectors.toList()); + List attributedLocationsList = + parentChildrenList.stream() + .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) + .map(it -> getReferenceIDPart(it.toString())) + .collect(Collectors.toList()); + return attributedLocationsList; + } + + private List getOrganizationIdsByLocationIds(List attributedLocationsList) { + if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { + return new ArrayList<>(); + } + + Bundle organizationAffiliationsBundle = + getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) + .returnBundle(Bundle.class) + .execute(); + + return organizationAffiliationsBundle.getEntry().stream() + .map( + bundleEntryComponent -> + getReferenceIDPart( + ((OrganizationAffiliation) bundleEntryComponent.getResource()) + .getOrganization() + .getReference())) + .distinct() + .collect(Collectors.toList()); + } + + private String getPractitionerIdentifier(Practitioner practitioner) { + String practitionerId = EMPTY_STRING; + if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { + practitionerId = practitioner.getIdElement().getIdPart(); + } + return practitionerId; + } + + private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); + String practitionerId = getPractitionerIdentifier(practitioner); + + logger.info("Searching for care teams for practitioner with id: " + practitioner); + Bundle careTeams = getCareTeams(practitionerId); + List careTeamsList = mapBundleToCareTeams(careTeams); + fhirPractitionerDetails.setCareTeams(careTeamsList); + fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); + + logger.info("Searching for Organizations tied with CareTeams: "); + List careTeamManagingOrganizationIds = + getManagingOrganizationsOfCareTeamIds(careTeamsList); + + Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); + logger.info("Managing Organization are fetched"); + + List managingOrganizationTeams = + mapBundleToOrganizations(careTeamManagingOrganizations); + + logger.info("Searching for organizations of practitioner with id: " + practitioner); + + List practitionerRoleList = + getPractitionerRolesByPractitionerId(practitionerId); + logger.info("Practitioner Roles are fetched"); + + List practitionerOrganizationIds = + getOrganizationIdsByPractitionerRoles(practitionerRoleList); + + Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); + + List teams = mapBundleToOrganizations(practitionerOrganizations); + // TODO Fix Distinct + List bothOrganizations = + Stream.concat(managingOrganizationTeams.stream(), teams.stream()) + .distinct() + .collect(Collectors.toList()); + + fhirPractitionerDetails.setOrganizations(bothOrganizations); + fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); + + Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); + logger.info("Groups are fetched"); + + List groupsList = mapBundleToGroups(groupsBundle); + fhirPractitionerDetails.setGroups(groupsList); + fhirPractitionerDetails.setId(practitionerId); + + logger.info("Searching for locations by organizations"); + + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle( + Stream.concat( + careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) + .distinct() + .collect(Collectors.toList())); + + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + + fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); + + List locationIds = + getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + + List locationsIdentifiers = + getOfficialLocationIdentifiersByLocationIds( + locationIds); // TODO Investigate why the Location ID and official identifiers are + // different + + logger.info("Searching for location hierarchy list by locations identifiers"); + List locationHierarchyList = + getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); + fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); + + logger.info("Searching for locations by ids"); + List locationsList = getLocationsByIds(locationIds); + fhirPractitionerDetails.setLocations(locationsList); + + practitionerDetails.setId(practitionerId); + practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); + + return practitionerDetails; + } + + private List mapBundleToOrganizations(Bundle organizationBundle) { + return organizationBundle.getEntry().stream() + .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private Bundle getGroupsAssignedToPractitioner(String practitionerId) { + return getFhirClientForR4() + .search() + .forResource(Group.class) + .where(Group.MEMBER.hasId(practitionerId)) + .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) + .returnBundle(Bundle.class) + .execute(); + } + + public static Predicate distinctByKey(Function keyExtractor) { + Set seen = ConcurrentHashMap.newKeySet(); + return t -> seen.add(keyExtractor.apply(t)); + } + + private List getPractitionerRolesByPractitionerId(String practitionerId) { + Bundle practitionerRoles = getPractitionerRoles(practitionerId); + return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); + } + + private List getOrganizationIdsByPractitionerRoles( + List practitionerRoles) { + return practitionerRoles.stream() + .filter(practitionerRole -> practitionerRole.hasOrganization()) + .map(it -> getReferenceIDPart(it.getOrganization().getReference())) + .collect(Collectors.toList()); + } + + private Practitioner getPractitionerByIdentifier(String identifier) { + Bundle resultBundle = + getFhirClientForR4() + .search() + .forResource(Practitioner.class) + .where(Practitioner.IDENTIFIER.exactly().identifier(identifier)) + .returnBundle(Bundle.class) + .execute(); + + return resultBundle != null + ? (Practitioner) resultBundle.getEntryFirstRep().getResource() + : null; + } + + private List getCareTeamsByOrganizationIds(List organizationIds) { + if (organizationIds.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasAnyOfIds( + organizationIds.stream() + .map( + it -> + Enumerations.ResourceType.ORGANIZATION.toCode() + + Constants.FORWARD_SLASH + + it) + .collect(Collectors.toList()))) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) + .map(it -> ((CareTeam) it.getResource())) + .collect(Collectors.toList()); + } + + private Bundle getCareTeams(String practitionerId) { + logger.info("Searching for Care Teams with practitioner id :" + practitionerId); + + return getFhirClientForR4() + .search() + .forResource(CareTeam.class) + .where( + CareTeam.PARTICIPANT.hasId( + Enumerations.ResourceType.PRACTITIONER.toCode() + + Constants.FORWARD_SLASH + + practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private Bundle getPractitionerRoles(String practitionerId) { + logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); + return getFhirClientForR4() + .search() + .forResource(PractitionerRole.class) + .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) + .returnBundle(Bundle.class) + .execute(); + } + + private static String getReferenceIDPart(String reference) { + return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); + } + + private Bundle getOrganizationsById(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(Organization.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private @Nullable List getLocationsByIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) + .returnBundle(Bundle.class) + .execute(); + + return locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + } + + private @Nullable List getOfficialLocationIdentifiersByLocationIds( + List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + List locations = getLocationsByIds(locationIds); + + return locations.stream() + .map( + it -> + it.getIdentifier().stream() + .filter( + id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) + .map(it2 -> it2.getValue()) + .collect(Collectors.toList())) + .flatMap(it3 -> it3.stream()) + .collect(Collectors.toList()); + } + + private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { + if (organizationIds == null || organizationIds.isEmpty()) { + return new ArrayList<>(); + } + Bundle organizationAffiliationsBundle = + getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); + List organizationAffiliations = + mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); + return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + } + + private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { + return organizationIds.isEmpty() + ? EMPTY_BUNDLE + : getFhirClientForR4() + .search() + .forResource(OrganizationAffiliation.class) + .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) + .returnBundle(Bundle.class) + .execute(); + } + + private List getLocationIdentifiersByOrganizationAffiliations( + List organizationAffiliations) { + + return organizationAffiliations.stream() + .map( + organizationAffiliation -> + getReferenceIDPart( + organizationAffiliation.getLocation().stream() + .findFirst() + .get() + .getReference())) + .collect(Collectors.toList()); + } + + private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { + logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); + return careTeamsList.stream() + .filter(careTeam -> careTeam.hasManagingOrganization()) + .flatMap(it -> it.getManagingOrganization().stream()) + .map(it -> getReferenceIDPart(it.getReference())) + .collect(Collectors.toList()); + } + + private List mapBundleToCareTeams(Bundle careTeams) { + return careTeams.getEntry().stream() + .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToPractitionerRolesWithOrganization( + Bundle practitionerRoles) { + return practitionerRoles.getEntry().stream() + .map(it -> (PractitionerRole) it.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToGroups(Bundle groupsBundle) { + return groupsBundle.getEntry().stream() + .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List mapBundleToOrganizationAffiliation( + Bundle organizationAffiliationBundle) { + return organizationAffiliationBundle.getEntry().stream() + .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) + .collect(Collectors.toList()); + } + + private List getLocationsHierarchyByOfficialLocationIdentifiers( + List officialLocationIdentifiers) { + if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); + + Bundle bundle = + getFhirClientForR4() + .search() + .forResource(LocationHierarchy.class) + .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) + .returnBundle(Bundle.class) + .execute(); + + return bundle.getEntry().stream() + .map(it -> ((LocationHierarchy) it.getResource())) + .collect(Collectors.toList()); + } + + public static String createSearchTagValues(Map.Entry entry) { + return entry.getKey() + + com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR + + StringUtils.join( + entry.getValue(), + com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR + + entry.getKey() + + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + } + + private IGenericClient getFhirClientForR4() { + return fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + } } From 1f027df757caee8bc2287f3e14427371bf3284ed Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 27 Sep 2023 17:03:38 +0500 Subject: [PATCH 07/22] Issue#5 : Move OpenSRP Specific Changes to Fhir Gateway Plugins (custom repo) --- .../fhir/gateway/plugins/Constants.java | 4 + .../plugins/PractitionerDetailEndpoint.java | 22 ++- .../gateway/plugins/SyncAccessDecision.java | 4 +- exec/pom.xml | 16 +- .../gateway/LocationHierarchyEndpoint.java | 84 +++++++++++ .../LocationHierarchyEndpointHelper.java | 139 ++++++++++++++++++ .../gateway/PractitionerDetailEndpoint.java | 64 ++++---- .../PractitionerDetailsEndpointHelper.java | 24 +-- pom.xml | 10 +- 9 files changed, 309 insertions(+), 58 deletions(-) create mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java create mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java index 017db0bf..89a39818 100644 --- a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java @@ -61,4 +61,8 @@ public interface Constants { String EQUALS_TO_SIGN = "="; String HTTP_GET_METHOD = "GET"; + + public static final String CODE_URL_VALUE_SEPARATOR = "|"; + + public static final String HTTP_URL_SEPARATOR = "/"; } diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java index 2434947a..25de5326 100644 --- a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java @@ -15,6 +15,9 @@ */ package org.smartregister.fhir.gateway.plugins; +import static org.smartregister.fhir.gateway.plugins.Constants.CODE_URL_VALUE_SEPARATOR; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.PARAM_VALUES_SEPARATOR; +import static org.smartregister.utils.Constants.*; import static org.smartregister.utils.Constants.EMPTY_STRING; import ca.uhn.fhir.rest.client.api.IGenericClient; @@ -22,7 +25,6 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.FhirClientFactory; import com.google.fhir.gateway.HttpFhirClient; -import com.google.fhir.gateway.ProxyConstants; import com.google.fhir.gateway.TokenVerifier; import java.io.IOException; import java.util.*; @@ -46,7 +48,6 @@ import org.smartregister.model.location.ParentChildrenMap; import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; -import org.smartregister.utils.Constants; import org.springframework.lang.Nullable; /** @@ -105,7 +106,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } else { logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); + practitionerDetails.setId(PRACTITIONER_NOT_FOUND); } response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); response.setStatus(HttpStatus.SC_OK); @@ -387,7 +388,7 @@ private List getCareTeamsByOrganizationIds(List organizationId .map( it -> Enumerations.ResourceType.ORGANIZATION.toCode() - + Constants.FORWARD_SLASH + + FORWARD_SLASH + it) .collect(Collectors.toList()))) .returnBundle(Bundle.class) @@ -407,9 +408,7 @@ private Bundle getCareTeams(String practitionerId) { .forResource(CareTeam.class) .where( CareTeam.PARTICIPANT.hasId( - Enumerations.ResourceType.PRACTITIONER.toCode() - + Constants.FORWARD_SLASH - + practitionerId)) + Enumerations.ResourceType.PRACTITIONER.toCode() + FORWARD_SLASH + practitionerId)) .returnBundle(Bundle.class) .execute(); } @@ -425,7 +424,7 @@ private Bundle getPractitionerRoles(String practitionerId) { } private static String getReferenceIDPart(String reference) { - return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); + return reference.substring(reference.indexOf(FORWARD_SLASH) + 1); } private Bundle getOrganizationsById(List organizationIds) { @@ -567,12 +566,9 @@ private List getLocationsHierarchyByOfficialLocationIdentifie public static String createSearchTagValues(Map.Entry entry) { return entry.getKey() - + com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR + + CODE_URL_VALUE_SEPARATOR + StringUtils.join( - entry.getValue(), - com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR - + entry.getKey() - + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); } private IGenericClient getFhirClientForR4() { diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java index 4d716294..0e3b24f5 100755 --- a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java +++ b/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java @@ -15,6 +15,8 @@ */ package org.smartregister.fhir.gateway.plugins; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.PARAM_VALUES_SEPARATOR; + import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -321,7 +323,7 @@ private void addTags( != (com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM + com.google.fhir.gateway.ProxyConstants.Literals.EQUALS) .length()) { - urlStringBuilder.append(com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR); + urlStringBuilder.append(PARAM_VALUES_SEPARATOR); } map.put(tagUrl, values.toArray(new String[0])); diff --git a/exec/pom.xml b/exec/pom.xml index 2b25da63..75bd4c6e 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -55,13 +55,13 @@ com.google.fhir.gateway server - 0.1.32 + 0.2.1-SNAPSHOT com.google.fhir.gateway plugins - 0.1.32 + 0.2.1-SNAPSHOT @@ -76,6 +76,18 @@ test + + org.smartregister + fhir-common-utils + 0.0.9-SNAPSHOT + compile + + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${hapifhir_version} + diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java new file mode 100644 index 00000000..7bdbe5fa --- /dev/null +++ b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java @@ -0,0 +1,84 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.fhir.gateway.FhirClientFactory; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.TokenVerifier; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.ServletException; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; + +@WebServlet("/LocationHierarchy") +public class LocationHierarchyEndpoint extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(LocationHierarchyEndpoint.class); + private final TokenVerifier tokenVerifier; + + private final HttpFhirClient fhirClient; + String PROXY_TO_ENV = "PROXY_TO"; + + private FhirContext fhirR4Context = FhirContext.forR4(); + private IGenericClient r4FhirClient = + fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + + private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); + + private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; + + public LocationHierarchyEndpoint() throws IOException { + this.tokenVerifier = TokenVerifier.createFromEnvVars(); + this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + // Check the Bearer token to be a valid JWT with required claims. + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) { + throw new ServletException("No Authorization header provided!"); + } + List patientIds = new ArrayList<>(); + // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: + DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); + String identifier = request.getParameter("identifier"); + + LocationHierarchy locationHierarchy = + locationHierarchyEndpointHelper.getLocationHierarchy(identifier); + String resultContent = fhirR4JsonParser.encodeResourceToString(locationHierarchy); + response.getOutputStream().print(resultContent); + response.setStatus(HttpStatus.SC_OK); + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } +} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java new file mode 100644 index 00000000..71113620 --- /dev/null +++ b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java @@ -0,0 +1,139 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway; + +import static org.smartregister.utils.Constants.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import ca.uhn.fhir.rest.gclient.TokenClientParam; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.BaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.StringType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.LocationHierarchyTree; + +public class LocationHierarchyEndpointHelper { + + private static final Logger logger = + LoggerFactory.getLogger(LocationHierarchyEndpointHelper.class); + String PROXY_TO_ENV = "PROXY_TO"; + + private FhirContext fhirR4Context = FhirContext.forR4(); + private IGenericClient r4FhirClient = + fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + + public LocationHierarchy getLocationHierarchy(String identifier) { + Location location = getLocationsByIdentifier(identifier); + String locationId = EMPTY_STRING; + if (location != null && location.getIdElement() != null) { + locationId = location.getIdElement().getIdPart(); + } + + LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); + LocationHierarchy locationHierarchy = new LocationHierarchy(); + if (StringUtils.isNotBlank(locationId) && location != null) { + logger.info("Building Location Hierarchy of Location Id : " + locationId); + locationHierarchyTree.buildTreeFromList(getLocationHierarchy(locationId, location)); + StringType locationIdString = new StringType().setId(locationId).getIdElement(); + locationHierarchy.setLocationId(locationIdString); + locationHierarchy.setId(LOCATION_RESOURCE + locationId); + + locationHierarchy.setLocationHierarchyTree(locationHierarchyTree); + } else { + locationHierarchy.setId(LOCATION_RESOURCE_NOT_FOUND); + } + return locationHierarchy; + } + + private List getLocationHierarchy(String locationId, Location parentLocation) { + return descendants(locationId, parentLocation); + } + + public List descendants(String locationId, Location parentLocation) { + + Bundle childLocationBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(Location.SP_PARTOF).hasAnyOfIds(locationId)) + .returnBundle(Bundle.class) + .execute(); + + List allLocations = new ArrayList<>(); + if (parentLocation != null) { + allLocations.add((Location) parentLocation); + } + + if (childLocationBundle != null) { + for (Bundle.BundleEntryComponent childLocation : childLocationBundle.getEntry()) { + Location childLocationEntity = (Location) childLocation.getResource(); + allLocations.add(childLocationEntity); + allLocations.addAll(descendants(childLocationEntity.getIdElement().getIdPart(), null)); + } + } + + return allLocations; + } + + private @Nullable List getLocationsByIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) + .returnBundle(Bundle.class) + .execute(); + + return locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + } + + private @Nullable Location getLocationsByIdentifier(String identifier) { + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new TokenClientParam(Location.SP_IDENTIFIER).exactly().identifier(identifier)) + .returnBundle(Bundle.class) + .execute(); + + List locationsList = new ArrayList<>(); + if (locationsBundle != null) + locationsList = locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + return locationsList.size() > 0 ? locationsList.get(0) : new Location(); + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } +} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java index 329ebea5..8009bf38 100644 --- a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java +++ b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java @@ -15,16 +15,15 @@ */ package org.smartregister.fhir.gateway; -import static com.google.fhir.gateway.util.Constants.PROXY_TO_ENV; -import static org.smartregister.utils.Constants.EMPTY_STRING; +import static org.smartregister.utils.Constants.*; import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.FhirClientFactory; import com.google.fhir.gateway.HttpFhirClient; -import com.google.fhir.gateway.ProxyConstants; import com.google.fhir.gateway.TokenVerifier; import java.io.IOException; import java.util.*; @@ -48,7 +47,6 @@ import org.smartregister.model.location.ParentChildrenMap; import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; -import org.smartregister.utils.Constants; import org.springframework.lang.Nullable; /** @@ -72,15 +70,28 @@ public class PractitionerDetailEndpoint extends HttpServlet { public static final String PRACTITIONER_GROUP_CODE = "405623001"; public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; + + public static final String CODE_URL_VALUE_SEPARATOR = "|"; + + public static final String HTTP_URL_SEPARATOR = "/"; + + public static final String PARAM_VALUES_SEPARATOR = ","; public static final Bundle EMPTY_BUNDLE = new Bundle(); - private IGenericClient r4FhirClient; - private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + + String PROXY_TO_ENV = "PROXY_TO"; private FhirContext fhirR4Context = FhirContext.forR4(); + private IGenericClient r4FhirClient = + fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + + private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); + private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; + private LocationHierarchyEndpoint locationHierarchyEndpoint; public PractitionerDetailEndpoint() throws IOException { this.tokenVerifier = TokenVerifier.createFromEnvVars(); this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); } @Override @@ -109,9 +120,11 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } else { logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); + practitionerDetails.setId(PRACTITIONER_NOT_FOUND); } - response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); + String resultContent = fhirR4JsonParser.encodeResourceToString(practitionerDetails); + + response.getOutputStream().print(resultContent); response.setStatus(HttpStatus.SC_OK); } @@ -391,7 +404,7 @@ private List getCareTeamsByOrganizationIds(List organizationId .map( it -> Enumerations.ResourceType.ORGANIZATION.toCode() - + Constants.FORWARD_SLASH + + FORWARD_SLASH + it) .collect(Collectors.toList()))) .returnBundle(Bundle.class) @@ -411,9 +424,7 @@ private Bundle getCareTeams(String practitionerId) { .forResource(CareTeam.class) .where( CareTeam.PARTICIPANT.hasId( - Enumerations.ResourceType.PRACTITIONER.toCode() - + Constants.FORWARD_SLASH - + practitionerId)) + Enumerations.ResourceType.PRACTITIONER.toCode() + FORWARD_SLASH + practitionerId)) .returnBundle(Bundle.class) .execute(); } @@ -429,7 +440,7 @@ private Bundle getPractitionerRoles(String practitionerId) { } private static String getReferenceIDPart(String reference) { - return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); + return reference.substring(reference.indexOf(FORWARD_SLASH) + 1); } private Bundle getOrganizationsById(List organizationIds) { @@ -556,30 +567,23 @@ private List getLocationsHierarchyByOfficialLocationIdentifie List officialLocationIdentifiers) { if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(LocationHierarchy.class) - .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) - .returnBundle(Bundle.class) - .execute(); - - return bundle.getEntry().stream() - .map(it -> ((LocationHierarchy) it.getResource())) - .collect(Collectors.toList()); + List locationHierarchyList = new ArrayList<>(); + for (String officialLocationIdentifier : officialLocationIdentifiers) { + LocationHierarchy locationHierarchy = + locationHierarchyEndpointHelper.getLocationHierarchy(officialLocationIdentifier); + locationHierarchyList.add(locationHierarchy); + } + return locationHierarchyList; } public static String createSearchTagValues(Map.Entry entry) { return entry.getKey() - + com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR + + CODE_URL_VALUE_SEPARATOR + StringUtils.join( - entry.getValue(), - com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR - + entry.getKey() - + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); } private IGenericClient getFhirClientForR4() { - return fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + return r4FhirClient; } } diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java index 9a159313..ea91e3b8 100644 --- a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java +++ b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java @@ -15,6 +15,8 @@ */ package org.smartregister.fhir.gateway; +import static org.smartregister.fhir.gateway.PractitionerDetailEndpoint.CODE_URL_VALUE_SEPARATOR; +import static org.smartregister.fhir.gateway.PractitionerDetailEndpoint.PARAM_VALUES_SEPARATOR; import static org.smartregister.utils.Constants.EMPTY_STRING; import ca.uhn.fhir.rest.client.api.IGenericClient; @@ -25,7 +27,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.*; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.model.location.LocationHierarchy; @@ -33,6 +37,7 @@ import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; import org.smartregister.utils.Constants; +import org.springframework.lang.Nullable; public class PractitionerDetailsEndpointHelper { private static final Logger logger = @@ -144,6 +149,7 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract return responseBundle; } + @NotNull public static List getAttributedLocations(List locationHierarchies) { List parentChildrenList = locationHierarchies.stream() @@ -395,7 +401,7 @@ private Bundle getOrganizationsById(List organizationIds) { .execute(); } - private List getLocationsByIds(List locationIds) { + private @Nullable List getLocationsByIds(List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); } @@ -413,7 +419,8 @@ private List getLocationsByIds(List locationIds) { .collect(Collectors.toList()); } - private List getOfficialLocationIdentifiersByLocationIds(List locationIds) { + private @Nullable List getOfficialLocationIdentifiersByLocationIds( + List locationIds) { if (locationIds == null || locationIds.isEmpty()) { return new ArrayList<>(); } @@ -521,14 +528,9 @@ private List getLocationsHierarchyByOfficialLocationIdentifie } public static String createSearchTagValues(Map.Entry entry) { - // return entry.getKey() - // + ProxyConstants.CODE_URL_VALUE_SEPARATOR - // + StringUtils.join( - // entry.getValue(), - // ProxyConstants.PARAM_VALUES_SEPARATOR - // + entry.getKey() - // + ProxyConstants.CODE_URL_VALUE_SEPARATOR); - - return ""; + return entry.getKey() + + CODE_URL_VALUE_SEPARATOR + + StringUtils.join( + entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); } } diff --git a/pom.xml b/pom.xml index ff58866c..c8c0d302 100755 --- a/pom.xml +++ b/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.32 + 0.2.1-SNAPSHOT org.smartregister @@ -36,4 +36,12 @@ customplugins + + + ca.uhn.hapi.fhir + hapi-fhir-client + ${hapifhir_version} + + + From 449f4cbb2ce2d206740a893eaaf5705b67ddb34f Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Thu, 5 Oct 2023 02:05:30 +0500 Subject: [PATCH 08/22] Code cleanup and removal of license related files --- LICENSE | 202 ------------------ customplugins/LICENSE | 202 ------------------ customplugins/license-header.txt | 13 -- .../gateway/LocationHierarchyEndpoint.java | 1 + license-header.txt | 13 -- {customplugins => plugins}/pom.xml | 6 +- .../resources/README.md | 0 .../hapi_page_url_allowed_queries.json | 0 .../hapi_sync_filter_ignored_queries.json | 0 .../fhir/gateway/plugins/BundleResources.java | 33 +++ .../fhir/gateway/plugins/Constants.java | 0 .../plugins/PermissionAccessChecker.java | 25 ++- .../plugins/PractitionerDetailEndpoint.java | 0 .../PractitionerDetailsEndpointHelper.java | 0 .../fhir/gateway/plugins/ProxyConstants.java | 0 .../gateway/plugins/ResourceFinderImp.java | 0 .../fhir/gateway/plugins/RestUtil.java | 0 .../gateway/plugins/SyncAccessDecision.java | 0 .../plugins/interfaces/ResourceFinder.java | 0 .../plugins/PermissionAccessCheckerTest.java | 0 .../plugins/SyncAccessDecisionTest.java | 0 .../plugins/TestRequestDetailsToReader.java | 0 .../fhir/gateway/plugins/TestUtil.java | 0 .../hapi_sync_filter_ignored_queries.json | 0 .../resources/test_bundle_transaction.json | 0 .../test/resources/test_list_resource.json | 0 pom.xml | 15 +- 27 files changed, 71 insertions(+), 439 deletions(-) delete mode 100644 LICENSE delete mode 100644 customplugins/LICENSE delete mode 100644 customplugins/license-header.txt delete mode 100644 license-header.txt rename {customplugins => plugins}/pom.xml (95%) rename {customplugins => plugins}/resources/README.md (100%) rename {customplugins => plugins}/resources/hapi_page_url_allowed_queries.json (100%) rename {customplugins => plugins}/resources/hapi_sync_filter_ignored_queries.json (100%) create mode 100755 plugins/src/main/java/org/smartregister/fhir/gateway/plugins/BundleResources.java rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java (94%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java (100%) rename {customplugins => plugins}/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java (100%) rename {customplugins => plugins}/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java (100%) rename {customplugins => plugins}/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java (100%) rename {customplugins => plugins}/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java (100%) rename {customplugins => plugins}/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java (100%) rename {customplugins => plugins}/src/test/resources/hapi_sync_filter_ignored_queries.json (100%) rename {customplugins => plugins}/src/test/resources/test_bundle_transaction.json (100%) rename {customplugins => plugins}/src/test/resources/test_list_resource.json (100%) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/customplugins/LICENSE b/customplugins/LICENSE deleted file mode 100644 index d6456956..00000000 --- a/customplugins/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/customplugins/license-header.txt b/customplugins/license-header.txt deleted file mode 100644 index edb10158..00000000 --- a/customplugins/license-header.txt +++ /dev/null @@ -1,13 +0,0 @@ -Copyright ${license.git.copyrightYears} Google LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java index 7bdbe5fa..bb2852f5 100644 --- a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java +++ b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java @@ -74,6 +74,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) LocationHierarchy locationHierarchy = locationHierarchyEndpointHelper.getLocationHierarchy(identifier); String resultContent = fhirR4JsonParser.encodeResourceToString(locationHierarchy); + response.setContentType("application/json"); response.getOutputStream().print(resultContent); response.setStatus(HttpStatus.SC_OK); } diff --git a/license-header.txt b/license-header.txt deleted file mode 100644 index edb10158..00000000 --- a/license-header.txt +++ /dev/null @@ -1,13 +0,0 @@ -Copyright ${license.git.copyrightYears} Google LLC - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/customplugins/pom.xml b/plugins/pom.xml similarity index 95% rename from customplugins/pom.xml rename to plugins/pom.xml index 9c20e393..df112427 100644 --- a/customplugins/pom.xml +++ b/plugins/pom.xml @@ -24,7 +24,7 @@ 1.0.0 - customplugins + plugins UTF-8 @@ -40,12 +40,12 @@ com.google.fhir.gateway server - 0.1.32 + 0.3.0 com.google.fhir.gateway plugins - 0.1.32 + 0.3.0 diff --git a/customplugins/resources/README.md b/plugins/resources/README.md similarity index 100% rename from customplugins/resources/README.md rename to plugins/resources/README.md diff --git a/customplugins/resources/hapi_page_url_allowed_queries.json b/plugins/resources/hapi_page_url_allowed_queries.json similarity index 100% rename from customplugins/resources/hapi_page_url_allowed_queries.json rename to plugins/resources/hapi_page_url_allowed_queries.json diff --git a/customplugins/resources/hapi_sync_filter_ignored_queries.json b/plugins/resources/hapi_sync_filter_ignored_queries.json similarity index 100% rename from customplugins/resources/hapi_sync_filter_ignored_queries.json rename to plugins/resources/hapi_sync_filter_ignored_queries.json diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/BundleResources.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/BundleResources.java new file mode 100755 index 00000000..6d0d4f4a --- /dev/null +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/BundleResources.java @@ -0,0 +1,33 @@ +/* + * Copyright 2021-2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.smartregister.fhir.gateway.plugins; + +import ca.uhn.fhir.rest.api.RequestTypeEnum; +import lombok.Getter; +import lombok.Setter; +import org.hl7.fhir.instance.model.api.IBaseResource; + +@Getter +@Setter +public class BundleResources { + private RequestTypeEnum requestType; + private IBaseResource resource; + + public BundleResources(RequestTypeEnum requestType, IBaseResource resource) { + this.requestType = requestType; + this.resource = resource; + } +} diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Constants.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java similarity index 94% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java index bea203bb..7141d2d9 100644 --- a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -27,16 +27,31 @@ import com.auth0.jwt.interfaces.DecodedJWT; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; -import com.google.fhir.gateway.*; -import com.google.fhir.gateway.interfaces.*; +import com.google.fhir.gateway.FhirProxyServer; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.JwtUtil; +import com.google.fhir.gateway.interfaces.RequestDetailsReader; +import com.google.fhir.gateway.interfaces.ResourceFinder; +import com.google.fhir.gateway.interfaces.AccessChecker; +import com.google.fhir.gateway.interfaces.AccessDecision; +import com.google.fhir.gateway.interfaces.NoOpAccessDecision; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import java.util.*; + +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.HashMap; import java.util.stream.Collectors; import javax.inject.Named; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.*; +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.CareTeam; +import org.hl7.fhir.r4.model.Organization; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.smartregister.model.practitioner.PractitionerDetails; @@ -80,7 +95,7 @@ private PermissionAccessChecker( @Override public AccessDecision checkAccess(RequestDetailsReader requestDetails) { - // For a Bundle requestDetails.getResourceName() returns null + // For a Bundle requestDetails.getResourceName() returns null if (requestDetails.getRequestType() == RequestTypeEnum.POST && requestDetails.getResourceName() == null) { return processBundle(requestDetails); diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java diff --git a/customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java similarity index 100% rename from customplugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java rename to plugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java diff --git a/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java similarity index 100% rename from customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java rename to plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java diff --git a/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java similarity index 100% rename from customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java rename to plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java diff --git a/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java similarity index 100% rename from customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java rename to plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestRequestDetailsToReader.java diff --git a/customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java similarity index 100% rename from customplugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java rename to plugins/src/test/java/org/smartregister/fhir/gateway/plugins/TestUtil.java diff --git a/customplugins/src/test/resources/hapi_sync_filter_ignored_queries.json b/plugins/src/test/resources/hapi_sync_filter_ignored_queries.json similarity index 100% rename from customplugins/src/test/resources/hapi_sync_filter_ignored_queries.json rename to plugins/src/test/resources/hapi_sync_filter_ignored_queries.json diff --git a/customplugins/src/test/resources/test_bundle_transaction.json b/plugins/src/test/resources/test_bundle_transaction.json similarity index 100% rename from customplugins/src/test/resources/test_bundle_transaction.json rename to plugins/src/test/resources/test_bundle_transaction.json diff --git a/customplugins/src/test/resources/test_list_resource.json b/plugins/src/test/resources/test_list_resource.json similarity index 100% rename from customplugins/src/test/resources/test_list_resource.json rename to plugins/src/test/resources/test_list_resource.json diff --git a/pom.xml b/pom.xml index c8c0d302..a7593b28 100755 --- a/pom.xml +++ b/pom.xml @@ -33,7 +33,7 @@ exec - customplugins + plugins @@ -44,4 +44,17 @@ + + + + com.mycila + license-maven-plugin + 4.1 + + true + + + + + From a28b4a3caec5bfb9746438daaaa433c541eb5aa6 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Fri, 13 Oct 2023 14:44:39 +0500 Subject: [PATCH 09/22] Code refactoring, addressing PR feedback, package and module restructuring --- exec/application.properties | 1 + exec/pom.xml | 10 +- .../gateway/LocationHierarchyEndpoint.java | 85 --- .../LocationHierarchyEndpointHelper.java | 139 ----- .../smartregister/fhir/gateway/MainApp.java | 4 +- .../gateway/PractitionerDetailEndpoint.java | 589 ------------------ .../PractitionerDetailsEndpointHelper.java | 536 ---------------- plugins/pom.xml | 4 +- .../plugins/LocationHierarchyEndpoint.java | 98 +++ .../LocationHierarchyEndpointHelper.java | 140 +++++ .../plugins/PermissionAccessChecker.java | 19 +- .../plugins/PractitionerDetailEndpoint.java | 65 +- .../PractitionerDetailsEndpointHelper.java | 10 +- .../gateway/plugins/ResourceFinderImp.java | 3 +- .../fhir/gateway/plugins/RestUtil.java | 34 - .../gateway/plugins/SyncAccessDecision.java | 24 +- .../plugins/interfaces/ResourceFinder.java | 2 +- .../plugins/SyncAccessDecisionTest.java | 39 +- pom.xml | 2 +- 19 files changed, 331 insertions(+), 1473 deletions(-) create mode 100644 exec/application.properties delete mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java delete mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java delete mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java delete mode 100644 exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java create mode 100644 plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java create mode 100644 plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java delete mode 100644 plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java diff --git a/exec/application.properties b/exec/application.properties new file mode 100644 index 00000000..70957423 --- /dev/null +++ b/exec/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true \ No newline at end of file diff --git a/exec/pom.xml b/exec/pom.xml index 75bd4c6e..817888fd 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -55,13 +55,13 @@ com.google.fhir.gateway server - 0.2.1-SNAPSHOT + 0.3.1 com.google.fhir.gateway plugins - 0.2.1-SNAPSHOT + 0.3.1 @@ -83,6 +83,12 @@ compile + + org.smartregister + plugins + 1.0.0 + + ca.uhn.hapi.fhir hapi-fhir-client diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java deleted file mode 100644 index bb2852f5..00000000 --- a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpoint.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2021-2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.gateway; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.google.fhir.gateway.FhirClientFactory; -import com.google.fhir.gateway.HttpFhirClient; -import com.google.fhir.gateway.TokenVerifier; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.apache.http.HttpStatus; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.smartregister.model.location.LocationHierarchy; - -@WebServlet("/LocationHierarchy") -public class LocationHierarchyEndpoint extends HttpServlet { - - private static final Logger logger = LoggerFactory.getLogger(LocationHierarchyEndpoint.class); - private final TokenVerifier tokenVerifier; - - private final HttpFhirClient fhirClient; - String PROXY_TO_ENV = "PROXY_TO"; - - private FhirContext fhirR4Context = FhirContext.forR4(); - private IGenericClient r4FhirClient = - fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); - - private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); - - private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; - - public LocationHierarchyEndpoint() throws IOException { - this.tokenVerifier = TokenVerifier.createFromEnvVars(); - this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); - this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // Check the Bearer token to be a valid JWT with required claims. - String authHeader = request.getHeader("Authorization"); - if (authHeader == null) { - throw new ServletException("No Authorization header provided!"); - } - List patientIds = new ArrayList<>(); - // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: - DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); - String identifier = request.getParameter("identifier"); - - LocationHierarchy locationHierarchy = - locationHierarchyEndpointHelper.getLocationHierarchy(identifier); - String resultContent = fhirR4JsonParser.encodeResourceToString(locationHierarchy); - response.setContentType("application/json"); - response.getOutputStream().print(resultContent); - response.setStatus(HttpStatus.SC_OK); - } - - private IGenericClient getFhirClientForR4() { - return r4FhirClient; - } -} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java b/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java deleted file mode 100644 index 71113620..00000000 --- a/exec/src/main/java/org/smartregister/fhir/gateway/LocationHierarchyEndpointHelper.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2021-2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.gateway; - -import static org.smartregister.utils.Constants.*; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -import ca.uhn.fhir.rest.gclient.TokenClientParam; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import javax.annotation.Nullable; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.BaseResource; -import org.hl7.fhir.r4.model.Bundle; -import org.hl7.fhir.r4.model.Location; -import org.hl7.fhir.r4.model.StringType; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.smartregister.model.location.LocationHierarchy; -import org.smartregister.model.location.LocationHierarchyTree; - -public class LocationHierarchyEndpointHelper { - - private static final Logger logger = - LoggerFactory.getLogger(LocationHierarchyEndpointHelper.class); - String PROXY_TO_ENV = "PROXY_TO"; - - private FhirContext fhirR4Context = FhirContext.forR4(); - private IGenericClient r4FhirClient = - fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); - - public LocationHierarchy getLocationHierarchy(String identifier) { - Location location = getLocationsByIdentifier(identifier); - String locationId = EMPTY_STRING; - if (location != null && location.getIdElement() != null) { - locationId = location.getIdElement().getIdPart(); - } - - LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); - LocationHierarchy locationHierarchy = new LocationHierarchy(); - if (StringUtils.isNotBlank(locationId) && location != null) { - logger.info("Building Location Hierarchy of Location Id : " + locationId); - locationHierarchyTree.buildTreeFromList(getLocationHierarchy(locationId, location)); - StringType locationIdString = new StringType().setId(locationId).getIdElement(); - locationHierarchy.setLocationId(locationIdString); - locationHierarchy.setId(LOCATION_RESOURCE + locationId); - - locationHierarchy.setLocationHierarchyTree(locationHierarchyTree); - } else { - locationHierarchy.setId(LOCATION_RESOURCE_NOT_FOUND); - } - return locationHierarchy; - } - - private List getLocationHierarchy(String locationId, Location parentLocation) { - return descendants(locationId, parentLocation); - } - - public List descendants(String locationId, Location parentLocation) { - - Bundle childLocationBundle = - getFhirClientForR4() - .search() - .forResource(Location.class) - .where(new ReferenceClientParam(Location.SP_PARTOF).hasAnyOfIds(locationId)) - .returnBundle(Bundle.class) - .execute(); - - List allLocations = new ArrayList<>(); - if (parentLocation != null) { - allLocations.add((Location) parentLocation); - } - - if (childLocationBundle != null) { - for (Bundle.BundleEntryComponent childLocation : childLocationBundle.getEntry()) { - Location childLocationEntity = (Location) childLocation.getResource(); - allLocations.add(childLocationEntity); - allLocations.addAll(descendants(childLocationEntity.getIdElement().getIdPart(), null)); - } - } - - return allLocations; - } - - private @Nullable List getLocationsByIds(List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - Bundle locationsBundle = - getFhirClientForR4() - .search() - .forResource(Location.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) - .returnBundle(Bundle.class) - .execute(); - - return locationsBundle.getEntry().stream() - .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) - .collect(Collectors.toList()); - } - - private @Nullable Location getLocationsByIdentifier(String identifier) { - Bundle locationsBundle = - getFhirClientForR4() - .search() - .forResource(Location.class) - .where(new TokenClientParam(Location.SP_IDENTIFIER).exactly().identifier(identifier)) - .returnBundle(Bundle.class) - .execute(); - - List locationsList = new ArrayList<>(); - if (locationsBundle != null) - locationsList = locationsBundle.getEntry().stream() - .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) - .collect(Collectors.toList()); - return locationsList.size() > 0 ? locationsList.get(0) : new Location(); - } - - private IGenericClient getFhirClientForR4() { - return r4FhirClient; - } -} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java b/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java index 3d6e2662..cc952f89 100644 --- a/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java +++ b/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java @@ -24,8 +24,8 @@ * plugins defined in "com.google.fhir.gateway.plugin". */ @SpringBootApplication( - scanBasePackages = {"org.smartregister.fhir.gateway.plugins", "com.google.fhir.gateway.plugin"}) -@ServletComponentScan({"org.smartregister.fhir.gateway", "com.google.fhir.gateway"}) + scanBasePackages = {"org.smartregister.fhir.gateway", "com.google.fhir.gateway.plugin"}) +@ServletComponentScan({"org.smartregister.fhir.gateway.plugins", "com.google.fhir.gateway"}) public class MainApp { public static void main(String[] args) { diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java deleted file mode 100644 index 8009bf38..00000000 --- a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailEndpoint.java +++ /dev/null @@ -1,589 +0,0 @@ -/* - * Copyright 2021-2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.gateway; - -import static org.smartregister.utils.Constants.*; - -import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.parser.IParser; -import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -import com.auth0.jwt.interfaces.DecodedJWT; -import com.google.fhir.gateway.FhirClientFactory; -import com.google.fhir.gateway.HttpFhirClient; -import com.google.fhir.gateway.TokenVerifier; -import java.io.IOException; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; -import org.hl7.fhir.r4.model.*; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.smartregister.model.location.LocationHierarchy; -import org.smartregister.model.location.ParentChildrenMap; -import org.smartregister.model.practitioner.FhirPractitionerDetails; -import org.smartregister.model.practitioner.PractitionerDetails; -import org.springframework.lang.Nullable; - -/** - * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization - * header. Although it is not a standard FHIR query, but it uses the FHIR server to construct the - * response. In this example, it inspects the JWT and depending on its claims, constructs the list - * of Patient IDs that the user has access to. - * - *

The two types of tokens resemble {@link com.google.fhir.gateway.plugin.ListAccessChecker} and - * {@link com.google.fhir.gateway.plugin.PatientAccessChecker} expected tokens. But those are just - * picked as examples and this custom endpoint is independent of any {@link - * com.google.fhir.gateway.interfaces.AccessChecker}. - */ -@WebServlet("/PractitionerDetail") -public class PractitionerDetailEndpoint extends HttpServlet { - - private static final Logger logger = LoggerFactory.getLogger(PractitionerDetailEndpoint.class); - private final TokenVerifier tokenVerifier; - - private final HttpFhirClient fhirClient; - - public static final String PRACTITIONER_GROUP_CODE = "405623001"; - public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; - - public static final String CODE_URL_VALUE_SEPARATOR = "|"; - - public static final String HTTP_URL_SEPARATOR = "/"; - - public static final String PARAM_VALUES_SEPARATOR = ","; - public static final Bundle EMPTY_BUNDLE = new Bundle(); - - String PROXY_TO_ENV = "PROXY_TO"; - - private FhirContext fhirR4Context = FhirContext.forR4(); - private IGenericClient r4FhirClient = - fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); - - private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); - private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; - private LocationHierarchyEndpoint locationHierarchyEndpoint; - - public PractitionerDetailEndpoint() throws IOException { - this.tokenVerifier = TokenVerifier.createFromEnvVars(); - this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); - this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); - } - - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // Check the Bearer token to be a valid JWT with required claims. - String authHeader = request.getHeader("Authorization"); - if (authHeader == null) { - throw new ServletException("No Authorization header provided!"); - } - List patientIds = new ArrayList<>(); - // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: - DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); - String keycloakUuid = request.getParameter("keycloak-uuid"); - // PractitionerDetails practitionerDetails = - // practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); - - PractitionerDetails practitionerDetails = new PractitionerDetails(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - practitionerDetails.setId(PRACTITIONER_NOT_FOUND); - } - String resultContent = fhirR4JsonParser.encodeResourceToString(practitionerDetails); - - response.getOutputStream().print(resultContent); - response.setStatus(HttpStatus.SC_OK); - } - - public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { - Bundle bundle = new Bundle(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - } - - return bundle; - } - - private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { - Bundle responseBundle = new Bundle(); - List attributedPractitioners = new ArrayList<>(); - PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); - // Get other guys. - - List careTeamManagingOrganizationIds = - getManagingOrganizationsOfCareTeamIds(careTeamList); - List supervisorCareTeamOrganizationLocationIds = - getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); - List officialLocationIds = - getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); - List locationHierarchies = - getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); - List attributedLocationsList = getAttributedLocations(locationHierarchies); - List attributedOrganizationIds = - getOrganizationIdsByLocationIds(attributedLocationsList); - - // Get care teams by organization Ids - List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); - - for (CareTeam careTeam : careTeamList) { - attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); - } - - careTeamList.addAll(attributedCareTeams); - - for (CareTeam careTeam : careTeamList) { - // Add current supervisor practitioners - attributedPractitioners.addAll( - careTeam.getParticipant().stream() - .filter( - it -> - it.hasMember() - && it.getMember() - .getReference() - .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) - .map( - it -> - getPractitionerByIdentifier( - getReferenceIDPart(it.getMember().getReference()))) - .collect(Collectors.toList())); - } - - List bundleEntryComponentList = new ArrayList<>(); - - for (Practitioner attributedPractitioner : attributedPractitioners) { - bundleEntryComponentList.add( - new Bundle.BundleEntryComponent() - .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); - } - - responseBundle.setEntry(bundleEntryComponentList); - responseBundle.setTotal(bundleEntryComponentList.size()); - return responseBundle; - } - - @NotNull - public static List getAttributedLocations(List locationHierarchies) { - List parentChildrenList = - locationHierarchies.stream() - .flatMap( - locationHierarchy -> - locationHierarchy - .getLocationHierarchyTree() - .getLocationsHierarchy() - .getParentChildren() - .stream()) - .collect(Collectors.toList()); - List attributedLocationsList = - parentChildrenList.stream() - .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) - .map(it -> getReferenceIDPart(it.toString())) - .collect(Collectors.toList()); - return attributedLocationsList; - } - - private List getOrganizationIdsByLocationIds(List attributedLocationsList) { - if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { - return new ArrayList<>(); - } - - Bundle organizationAffiliationsBundle = - getFhirClientForR4() - .search() - .forResource(OrganizationAffiliation.class) - .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) - .returnBundle(Bundle.class) - .execute(); - - return organizationAffiliationsBundle.getEntry().stream() - .map( - bundleEntryComponent -> - getReferenceIDPart( - ((OrganizationAffiliation) bundleEntryComponent.getResource()) - .getOrganization() - .getReference())) - .distinct() - .collect(Collectors.toList()); - } - - private String getPractitionerIdentifier(Practitioner practitioner) { - String practitionerId = EMPTY_STRING; - if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { - practitionerId = practitioner.getIdElement().getIdPart(); - } - return practitionerId; - } - - private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { - - PractitionerDetails practitionerDetails = new PractitionerDetails(); - FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); - String practitionerId = getPractitionerIdentifier(practitioner); - - logger.info("Searching for care teams for practitioner with id: " + practitioner); - Bundle careTeams = getCareTeams(practitionerId); - List careTeamsList = mapBundleToCareTeams(careTeams); - fhirPractitionerDetails.setCareTeams(careTeamsList); - fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); - - logger.info("Searching for Organizations tied with CareTeams: "); - List careTeamManagingOrganizationIds = - getManagingOrganizationsOfCareTeamIds(careTeamsList); - - Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); - logger.info("Managing Organization are fetched"); - - List managingOrganizationTeams = - mapBundleToOrganizations(careTeamManagingOrganizations); - - logger.info("Searching for organizations of practitioner with id: " + practitioner); - - List practitionerRoleList = - getPractitionerRolesByPractitionerId(practitionerId); - logger.info("Practitioner Roles are fetched"); - - List practitionerOrganizationIds = - getOrganizationIdsByPractitionerRoles(practitionerRoleList); - - Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); - - List teams = mapBundleToOrganizations(practitionerOrganizations); - // TODO Fix Distinct - List bothOrganizations = - Stream.concat(managingOrganizationTeams.stream(), teams.stream()) - .distinct() - .collect(Collectors.toList()); - - fhirPractitionerDetails.setOrganizations(bothOrganizations); - fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); - - Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); - logger.info("Groups are fetched"); - - List groupsList = mapBundleToGroups(groupsBundle); - fhirPractitionerDetails.setGroups(groupsList); - fhirPractitionerDetails.setId(practitionerId); - - logger.info("Searching for locations by organizations"); - - Bundle organizationAffiliationsBundle = - getOrganizationAffiliationsByOrganizationIdsBundle( - Stream.concat( - careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) - .distinct() - .collect(Collectors.toList())); - - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - - fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); - - List locationIds = - getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - - List locationsIdentifiers = - getOfficialLocationIdentifiersByLocationIds( - locationIds); // TODO Investigate why the Location ID and official identifiers are - // different - - logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = - getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); - fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); - - logger.info("Searching for locations by ids"); - List locationsList = getLocationsByIds(locationIds); - fhirPractitionerDetails.setLocations(locationsList); - - practitionerDetails.setId(practitionerId); - practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); - - return practitionerDetails; - } - - private List mapBundleToOrganizations(Bundle organizationBundle) { - return organizationBundle.getEntry().stream() - .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private Bundle getGroupsAssignedToPractitioner(String practitionerId) { - return getFhirClientForR4() - .search() - .forResource(Group.class) - .where(Group.MEMBER.hasId(practitionerId)) - .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) - .returnBundle(Bundle.class) - .execute(); - } - - public static Predicate distinctByKey(Function keyExtractor) { - Set seen = ConcurrentHashMap.newKeySet(); - return t -> seen.add(keyExtractor.apply(t)); - } - - private List getPractitionerRolesByPractitionerId(String practitionerId) { - Bundle practitionerRoles = getPractitionerRoles(practitionerId); - return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); - } - - private List getOrganizationIdsByPractitionerRoles( - List practitionerRoles) { - return practitionerRoles.stream() - .filter(practitionerRole -> practitionerRole.hasOrganization()) - .map(it -> getReferenceIDPart(it.getOrganization().getReference())) - .collect(Collectors.toList()); - } - - private Practitioner getPractitionerByIdentifier(String identifier) { - Bundle resultBundle = - getFhirClientForR4() - .search() - .forResource(Practitioner.class) - .where(Practitioner.IDENTIFIER.exactly().identifier(identifier)) - .returnBundle(Bundle.class) - .execute(); - - return resultBundle != null - ? (Practitioner) resultBundle.getEntryFirstRep().getResource() - : null; - } - - private List getCareTeamsByOrganizationIds(List organizationIds) { - if (organizationIds.isEmpty()) return new ArrayList<>(); - - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(CareTeam.class) - .where( - CareTeam.PARTICIPANT.hasAnyOfIds( - organizationIds.stream() - .map( - it -> - Enumerations.ResourceType.ORGANIZATION.toCode() - + FORWARD_SLASH - + it) - .collect(Collectors.toList()))) - .returnBundle(Bundle.class) - .execute(); - - return bundle.getEntry().stream() - .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) - .map(it -> ((CareTeam) it.getResource())) - .collect(Collectors.toList()); - } - - private Bundle getCareTeams(String practitionerId) { - logger.info("Searching for Care Teams with practitioner id :" + practitionerId); - - return getFhirClientForR4() - .search() - .forResource(CareTeam.class) - .where( - CareTeam.PARTICIPANT.hasId( - Enumerations.ResourceType.PRACTITIONER.toCode() + FORWARD_SLASH + practitionerId)) - .returnBundle(Bundle.class) - .execute(); - } - - private Bundle getPractitionerRoles(String practitionerId) { - logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); - return getFhirClientForR4() - .search() - .forResource(PractitionerRole.class) - .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) - .returnBundle(Bundle.class) - .execute(); - } - - private static String getReferenceIDPart(String reference) { - return reference.substring(reference.indexOf(FORWARD_SLASH) + 1); - } - - private Bundle getOrganizationsById(List organizationIds) { - return organizationIds.isEmpty() - ? EMPTY_BUNDLE - : getFhirClientForR4() - .search() - .forResource(Organization.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) - .returnBundle(Bundle.class) - .execute(); - } - - private @Nullable List getLocationsByIds(List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - Bundle locationsBundle = - getFhirClientForR4() - .search() - .forResource(Location.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) - .returnBundle(Bundle.class) - .execute(); - - return locationsBundle.getEntry().stream() - .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) - .collect(Collectors.toList()); - } - - private @Nullable List getOfficialLocationIdentifiersByLocationIds( - List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - List locations = getLocationsByIds(locationIds); - - return locations.stream() - .map( - it -> - it.getIdentifier().stream() - .filter( - id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) - .map(it2 -> it2.getValue()) - .collect(Collectors.toList())) - .flatMap(it3 -> it3.stream()) - .collect(Collectors.toList()); - } - - private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { - if (organizationIds == null || organizationIds.isEmpty()) { - return new ArrayList<>(); - } - Bundle organizationAffiliationsBundle = - getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - } - - private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { - return organizationIds.isEmpty() - ? EMPTY_BUNDLE - : getFhirClientForR4() - .search() - .forResource(OrganizationAffiliation.class) - .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) - .returnBundle(Bundle.class) - .execute(); - } - - private List getLocationIdentifiersByOrganizationAffiliations( - List organizationAffiliations) { - - return organizationAffiliations.stream() - .map( - organizationAffiliation -> - getReferenceIDPart( - organizationAffiliation.getLocation().stream() - .findFirst() - .get() - .getReference())) - .collect(Collectors.toList()); - } - - private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { - logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); - return careTeamsList.stream() - .filter(careTeam -> careTeam.hasManagingOrganization()) - .flatMap(it -> it.getManagingOrganization().stream()) - .map(it -> getReferenceIDPart(it.getReference())) - .collect(Collectors.toList()); - } - - private List mapBundleToCareTeams(Bundle careTeams) { - return careTeams.getEntry().stream() - .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToPractitionerRolesWithOrganization( - Bundle practitionerRoles) { - return practitionerRoles.getEntry().stream() - .map(it -> (PractitionerRole) it.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToGroups(Bundle groupsBundle) { - return groupsBundle.getEntry().stream() - .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToOrganizationAffiliation( - Bundle organizationAffiliationBundle) { - return organizationAffiliationBundle.getEntry().stream() - .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List getLocationsHierarchyByOfficialLocationIdentifiers( - List officialLocationIdentifiers) { - if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); - - List locationHierarchyList = new ArrayList<>(); - for (String officialLocationIdentifier : officialLocationIdentifiers) { - LocationHierarchy locationHierarchy = - locationHierarchyEndpointHelper.getLocationHierarchy(officialLocationIdentifier); - locationHierarchyList.add(locationHierarchy); - } - return locationHierarchyList; - } - - public static String createSearchTagValues(Map.Entry entry) { - return entry.getKey() - + CODE_URL_VALUE_SEPARATOR - + StringUtils.join( - entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); - } - - private IGenericClient getFhirClientForR4() { - return r4FhirClient; - } -} diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java b/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java deleted file mode 100644 index ea91e3b8..00000000 --- a/exec/src/main/java/org/smartregister/fhir/gateway/PractitionerDetailsEndpointHelper.java +++ /dev/null @@ -1,536 +0,0 @@ -/* - * Copyright 2021-2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.gateway; - -import static org.smartregister.fhir.gateway.PractitionerDetailEndpoint.CODE_URL_VALUE_SEPARATOR; -import static org.smartregister.fhir.gateway.PractitionerDetailEndpoint.PARAM_VALUES_SEPARATOR; -import static org.smartregister.utils.Constants.EMPTY_STRING; - -import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.r4.model.*; -import org.jetbrains.annotations.NotNull; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.smartregister.model.location.LocationHierarchy; -import org.smartregister.model.location.ParentChildrenMap; -import org.smartregister.model.practitioner.FhirPractitionerDetails; -import org.smartregister.model.practitioner.PractitionerDetails; -import org.smartregister.utils.Constants; -import org.springframework.lang.Nullable; - -public class PractitionerDetailsEndpointHelper { - private static final Logger logger = - LoggerFactory.getLogger(PractitionerDetailsEndpointHelper.class); - public static final String PRACTITIONER_GROUP_CODE = "405623001"; - public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; - public static final Bundle EMPTY_BUNDLE = new Bundle(); - private IGenericClient r4FhirClient; - - public PractitionerDetailsEndpointHelper(IGenericClient fhirClient) { - this.r4FhirClient = fhirClient; - } - - private IGenericClient getFhirClientForR4() { - return r4FhirClient; - } - - public PractitionerDetails getPractitionerDetailsByKeycloakId(String keycloakUuid) { - PractitionerDetails practitionerDetails = new PractitionerDetails(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); - } - - return practitionerDetails; - } - - public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { - Bundle bundle = new Bundle(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - } - - return bundle; - } - - private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { - Bundle responseBundle = new Bundle(); - List attributedPractitioners = new ArrayList<>(); - PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); - // Get other guys. - - List careTeamManagingOrganizationIds = - getManagingOrganizationsOfCareTeamIds(careTeamList); - List supervisorCareTeamOrganizationLocationIds = - getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); - List officialLocationIds = - getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); - List locationHierarchies = - getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); - List attributedLocationsList = getAttributedLocations(locationHierarchies); - List attributedOrganizationIds = - getOrganizationIdsByLocationIds(attributedLocationsList); - - // Get care teams by organization Ids - List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); - - for (CareTeam careTeam : careTeamList) { - attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); - } - - careTeamList.addAll(attributedCareTeams); - - for (CareTeam careTeam : careTeamList) { - // Add current supervisor practitioners - attributedPractitioners.addAll( - careTeam.getParticipant().stream() - .filter( - it -> - it.hasMember() - && it.getMember() - .getReference() - .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) - .map( - it -> - getPractitionerByIdentifier( - getReferenceIDPart(it.getMember().getReference()))) - .collect(Collectors.toList())); - } - - List bundleEntryComponentList = new ArrayList<>(); - - for (Practitioner attributedPractitioner : attributedPractitioners) { - bundleEntryComponentList.add( - new Bundle.BundleEntryComponent() - .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); - } - - responseBundle.setEntry(bundleEntryComponentList); - responseBundle.setTotal(bundleEntryComponentList.size()); - return responseBundle; - } - - @NotNull - public static List getAttributedLocations(List locationHierarchies) { - List parentChildrenList = - locationHierarchies.stream() - .flatMap( - locationHierarchy -> - locationHierarchy - .getLocationHierarchyTree() - .getLocationsHierarchy() - .getParentChildren() - .stream()) - .collect(Collectors.toList()); - List attributedLocationsList = - parentChildrenList.stream() - .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) - .map(it -> getReferenceIDPart(it.toString())) - .collect(Collectors.toList()); - return attributedLocationsList; - } - - private List getOrganizationIdsByLocationIds(List attributedLocationsList) { - if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { - return new ArrayList<>(); - } - - Bundle organizationAffiliationsBundle = - getFhirClientForR4() - .search() - .forResource(OrganizationAffiliation.class) - .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) - .returnBundle(Bundle.class) - .execute(); - - return organizationAffiliationsBundle.getEntry().stream() - .map( - bundleEntryComponent -> - getReferenceIDPart( - ((OrganizationAffiliation) bundleEntryComponent.getResource()) - .getOrganization() - .getReference())) - .distinct() - .collect(Collectors.toList()); - } - - private String getPractitionerIdentifier(Practitioner practitioner) { - String practitionerId = EMPTY_STRING; - if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { - practitionerId = practitioner.getIdElement().getIdPart(); - } - return practitionerId; - } - - private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { - - PractitionerDetails practitionerDetails = new PractitionerDetails(); - FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); - String practitionerId = getPractitionerIdentifier(practitioner); - - logger.info("Searching for care teams for practitioner with id: " + practitioner); - Bundle careTeams = getCareTeams(practitionerId); - List careTeamsList = mapBundleToCareTeams(careTeams); - fhirPractitionerDetails.setCareTeams(careTeamsList); - fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); - - logger.info("Searching for Organizations tied with CareTeams: "); - List careTeamManagingOrganizationIds = - getManagingOrganizationsOfCareTeamIds(careTeamsList); - - Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); - logger.info("Managing Organization are fetched"); - - List managingOrganizationTeams = - mapBundleToOrganizations(careTeamManagingOrganizations); - - logger.info("Searching for organizations of practitioner with id: " + practitioner); - - List practitionerRoleList = - getPractitionerRolesByPractitionerId(practitionerId); - logger.info("Practitioner Roles are fetched"); - - List practitionerOrganizationIds = - getOrganizationIdsByPractitionerRoles(practitionerRoleList); - - Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); - - List teams = mapBundleToOrganizations(practitionerOrganizations); - // TODO Fix Distinct - List bothOrganizations = - Stream.concat(managingOrganizationTeams.stream(), teams.stream()) - .distinct() - .collect(Collectors.toList()); - - fhirPractitionerDetails.setOrganizations(bothOrganizations); - fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); - - Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); - logger.info("Groups are fetched"); - - List groupsList = mapBundleToGroups(groupsBundle); - fhirPractitionerDetails.setGroups(groupsList); - fhirPractitionerDetails.setId(practitionerId); - - logger.info("Searching for locations by organizations"); - - Bundle organizationAffiliationsBundle = - getOrganizationAffiliationsByOrganizationIdsBundle( - Stream.concat( - careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) - .distinct() - .collect(Collectors.toList())); - - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - - fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); - - List locationIds = - getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - - List locationsIdentifiers = - getOfficialLocationIdentifiersByLocationIds( - locationIds); // TODO Investigate why the Location ID and official identifiers are - // different - - logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = - getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); - fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); - - logger.info("Searching for locations by ids"); - List locationsList = getLocationsByIds(locationIds); - fhirPractitionerDetails.setLocations(locationsList); - - practitionerDetails.setId(practitionerId); - practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); - - return practitionerDetails; - } - - private List mapBundleToOrganizations(Bundle organizationBundle) { - return organizationBundle.getEntry().stream() - .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private Bundle getGroupsAssignedToPractitioner(String practitionerId) { - return getFhirClientForR4() - .search() - .forResource(Group.class) - .where(Group.MEMBER.hasId(practitionerId)) - .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) - .returnBundle(Bundle.class) - .execute(); - } - - public static Predicate distinctByKey(Function keyExtractor) { - Set seen = ConcurrentHashMap.newKeySet(); - return t -> seen.add(keyExtractor.apply(t)); - } - - private List getPractitionerRolesByPractitionerId(String practitionerId) { - Bundle practitionerRoles = getPractitionerRoles(practitionerId); - return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); - } - - private List getOrganizationIdsByPractitionerRoles( - List practitionerRoles) { - return practitionerRoles.stream() - .filter(practitionerRole -> practitionerRole.hasOrganization()) - .map(it -> getReferenceIDPart(it.getOrganization().getReference())) - .collect(Collectors.toList()); - } - - private Practitioner getPractitionerByIdentifier(String identifier) { - Bundle resultBundle = - getFhirClientForR4() - .search() - .forResource(Practitioner.class) - .where(Practitioner.IDENTIFIER.exactly().identifier(identifier)) - .returnBundle(Bundle.class) - .execute(); - - return resultBundle != null - ? (Practitioner) resultBundle.getEntryFirstRep().getResource() - : null; - } - - private List getCareTeamsByOrganizationIds(List organizationIds) { - if (organizationIds.isEmpty()) return new ArrayList<>(); - - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(CareTeam.class) - .where( - CareTeam.PARTICIPANT.hasAnyOfIds( - organizationIds.stream() - .map( - it -> - Enumerations.ResourceType.ORGANIZATION.toCode() - + Constants.FORWARD_SLASH - + it) - .collect(Collectors.toList()))) - .returnBundle(Bundle.class) - .execute(); - - return bundle.getEntry().stream() - .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) - .map(it -> ((CareTeam) it.getResource())) - .collect(Collectors.toList()); - } - - private Bundle getCareTeams(String practitionerId) { - logger.info("Searching for Care Teams with practitioner id :" + practitionerId); - - return getFhirClientForR4() - .search() - .forResource(CareTeam.class) - .where( - CareTeam.PARTICIPANT.hasId( - Enumerations.ResourceType.PRACTITIONER.toCode() - + Constants.FORWARD_SLASH - + practitionerId)) - .returnBundle(Bundle.class) - .execute(); - } - - private Bundle getPractitionerRoles(String practitionerId) { - logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); - return getFhirClientForR4() - .search() - .forResource(PractitionerRole.class) - .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) - .returnBundle(Bundle.class) - .execute(); - } - - private static String getReferenceIDPart(String reference) { - return reference.substring(reference.indexOf(Constants.FORWARD_SLASH) + 1); - } - - private Bundle getOrganizationsById(List organizationIds) { - return organizationIds.isEmpty() - ? EMPTY_BUNDLE - : getFhirClientForR4() - .search() - .forResource(Organization.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) - .returnBundle(Bundle.class) - .execute(); - } - - private @Nullable List getLocationsByIds(List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - Bundle locationsBundle = - getFhirClientForR4() - .search() - .forResource(Location.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) - .returnBundle(Bundle.class) - .execute(); - - return locationsBundle.getEntry().stream() - .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) - .collect(Collectors.toList()); - } - - private @Nullable List getOfficialLocationIdentifiersByLocationIds( - List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - List locations = getLocationsByIds(locationIds); - - return locations.stream() - .map( - it -> - it.getIdentifier().stream() - .filter( - id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) - .map(it2 -> it2.getValue()) - .collect(Collectors.toList())) - .flatMap(it3 -> it3.stream()) - .collect(Collectors.toList()); - } - - private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { - if (organizationIds == null || organizationIds.isEmpty()) { - return new ArrayList<>(); - } - Bundle organizationAffiliationsBundle = - getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - } - - private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { - return organizationIds.isEmpty() - ? EMPTY_BUNDLE - : getFhirClientForR4() - .search() - .forResource(OrganizationAffiliation.class) - .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) - .returnBundle(Bundle.class) - .execute(); - } - - private List getLocationIdentifiersByOrganizationAffiliations( - List organizationAffiliations) { - - return organizationAffiliations.stream() - .map( - organizationAffiliation -> - getReferenceIDPart( - organizationAffiliation.getLocation().stream() - .findFirst() - .get() - .getReference())) - .collect(Collectors.toList()); - } - - private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { - logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); - return careTeamsList.stream() - .filter(careTeam -> careTeam.hasManagingOrganization()) - .flatMap(it -> it.getManagingOrganization().stream()) - .map(it -> getReferenceIDPart(it.getReference())) - .collect(Collectors.toList()); - } - - private List mapBundleToCareTeams(Bundle careTeams) { - return careTeams.getEntry().stream() - .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToPractitionerRolesWithOrganization( - Bundle practitionerRoles) { - return practitionerRoles.getEntry().stream() - .map(it -> (PractitionerRole) it.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToGroups(Bundle groupsBundle) { - return groupsBundle.getEntry().stream() - .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToOrganizationAffiliation( - Bundle organizationAffiliationBundle) { - return organizationAffiliationBundle.getEntry().stream() - .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List getLocationsHierarchyByOfficialLocationIdentifiers( - List officialLocationIdentifiers) { - if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); - - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(LocationHierarchy.class) - .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) - .returnBundle(Bundle.class) - .execute(); - - return bundle.getEntry().stream() - .map(it -> ((LocationHierarchy) it.getResource())) - .collect(Collectors.toList()); - } - - public static String createSearchTagValues(Map.Entry entry) { - return entry.getKey() - + CODE_URL_VALUE_SEPARATOR - + StringUtils.join( - entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); - } -} diff --git a/plugins/pom.xml b/plugins/pom.xml index df112427..6c45a92f 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -40,12 +40,12 @@ com.google.fhir.gateway server - 0.3.0 + 0.3.1 com.google.fhir.gateway plugins - 0.3.0 + 0.3.1 diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java new file mode 100644 index 00000000..992057e9 --- /dev/null +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java @@ -0,0 +1,98 @@ +package org.smartregister.fhir.gateway.plugins;/// * +// * Copyright 2021-2023 Google LLC +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// package org.smartregister.fhir.gateway; +// +// import ca.uhn.fhir.context.FhirContext; +// import ca.uhn.fhir.parser.IParser; +// import ca.uhn.fhir.rest.client.api.IGenericClient; +// import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +// import com.auth0.jwt.interfaces.DecodedJWT; +// import com.google.fhir.gateway.ExceptionUtil; +// import com.google.fhir.gateway.FhirClientFactory; +// import com.google.fhir.gateway.HttpFhirClient; +// import com.google.fhir.gateway.TokenVerifier; +// import java.io.IOException; +// import java.util.ArrayList; +// import java.util.List; +// import javax.servlet.annotation.WebServlet; +// import javax.servlet.http.HttpServlet; +// import javax.servlet.http.HttpServletRequest; +// import javax.servlet.http.HttpServletResponse; +// import org.apache.http.HttpStatus; +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// import org.smartregister.model.location.LocationHierarchy; +// +// @WebServlet("/LocationHierarchy") +// public class LocationHierarchyEndpoint extends HttpServlet { +// +// private static final Logger logger = LoggerFactory.getLogger(LocationHierarchyEndpoint.class); +// private final TokenVerifier tokenVerifier; +// +// private final HttpFhirClient fhirClient; +// String PROXY_TO_ENV = "PROXY_TO"; +// +// private FhirContext fhirR4Context = FhirContext.forR4(); +// private IGenericClient r4FhirClient = +// fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); +// +// private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); +// +// private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; +// +// public LocationHierarchyEndpoint() throws IOException { +// this.tokenVerifier = TokenVerifier.createFromEnvVars(); +// this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); +// this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); +// } +// +// @Override +// protected void doGet(HttpServletRequest request, HttpServletResponse response) +// throws IOException { +// // Check the Bearer token to be a valid JWT with required claims. +// try { +// +// String authHeader = request.getHeader("Authorization"); +// if (authHeader == null) { +// ExceptionUtil.throwRuntimeExceptionAndLog( +// logger, "No Authorization header provided!", new AuthenticationException()); +// } +// List patientIds = new ArrayList<>(); +// // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: +// DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); +// String identifier = request.getParameter("identifier"); +// +// LocationHierarchy locationHierarchy = +// locationHierarchyEndpointHelper.getLocationHierarchy(identifier); +// String resultContent = fhirR4JsonParser.encodeResourceToString(locationHierarchy); +// response.setContentType("application/json"); +// response.getOutputStream().print(resultContent); +// response.setStatus(HttpStatus.SC_OK); +// } catch (AuthenticationException authenticationException) { +// response.setContentType("application/json"); +// response.getOutputStream().print(authenticationException.getMessage()); +// response.setStatus(authenticationException.getStatusCode()); +// } catch (Exception exception) { +// response.setContentType("application/json"); +// response.getOutputStream().print(exception.getMessage()); +// response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); +// } +// } +// +// private IGenericClient getFhirClientForR4() { +// return r4FhirClient; +// } +// } 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 new file mode 100644 index 00000000..eacb140e --- /dev/null +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java @@ -0,0 +1,140 @@ +package org.smartregister.fhir.gateway.plugins;/// * +// * Copyright 2021-2023 Google LLC +// * +// * Licensed under the Apache License, Version 2.0 (the "License"); +// * you may not use this file except in compliance with the License. +// * You may obtain a copy of the License at +// * +// * http://www.apache.org/licenses/LICENSE-2.0 +// * +// * Unless required by applicable law or agreed to in writing, software +// * distributed under the License is distributed on an "AS IS" BASIS, +// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// * See the License for the specific language governing permissions and +// * limitations under the License. +// */ +// package org.smartregister.fhir.gateway; +// +// import static org.smartregister.utils.Constants.*; +// +// import ca.uhn.fhir.context.FhirContext; +// import ca.uhn.fhir.rest.client.api.IGenericClient; +// import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +// import ca.uhn.fhir.rest.gclient.TokenClientParam; +// import java.util.ArrayList; +// import java.util.List; +// import java.util.stream.Collectors; +// import javax.annotation.Nullable; +// import org.apache.commons.lang3.StringUtils; +// import org.hl7.fhir.r4.model.BaseResource; +// import org.hl7.fhir.r4.model.Bundle; +// import org.hl7.fhir.r4.model.Location; +// import org.hl7.fhir.r4.model.StringType; +// import org.slf4j.Logger; +// import org.slf4j.LoggerFactory; +// import org.smartregister.model.location.LocationHierarchy; +// import org.smartregister.model.location.LocationHierarchyTree; +// +// public class LocationHierarchyEndpointHelper { +// +// private static final Logger logger = +// LoggerFactory.getLogger(LocationHierarchyEndpointHelper.class); +// String PROXY_TO_ENV = "PROXY_TO"; +// +// private FhirContext fhirR4Context = FhirContext.forR4(); +// private IGenericClient r4FhirClient = +// fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); +// +// public LocationHierarchy getLocationHierarchy(String identifier) { +// Location location = getLocationsByIdentifier(identifier); +// String locationId = EMPTY_STRING; +// if (location != null && location.getIdElement() != null) { +// locationId = location.getIdElement().getIdPart(); +// } +// +// LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); +// LocationHierarchy locationHierarchy = new LocationHierarchy(); +// if (StringUtils.isNotBlank(locationId) && location != null) { +// logger.info("Building Location Hierarchy of Location Id : " + locationId); +// locationHierarchyTree.buildTreeFromList(getLocationHierarchy(locationId, location)); +// StringType locationIdString = new StringType().setId(locationId).getIdElement(); +// locationHierarchy.setLocationId(locationIdString); +// locationHierarchy.setId(LOCATION_RESOURCE + locationId); +// +// locationHierarchy.setLocationHierarchyTree(locationHierarchyTree); +// } else { +// locationHierarchy.setId(LOCATION_RESOURCE_NOT_FOUND); +// } +// return locationHierarchy; +// } +// +// private List getLocationHierarchy(String locationId, Location parentLocation) { +// return descendants(locationId, parentLocation); +// } +// +// public List descendants(String locationId, Location parentLocation) { +// +// Bundle childLocationBundle = +// getFhirClientForR4() +// .search() +// .forResource(Location.class) +// .where(new ReferenceClientParam(Location.SP_PARTOF).hasAnyOfIds(locationId)) +// .returnBundle(Bundle.class) +// .execute(); +// +// List allLocations = new ArrayList<>(); +// if (parentLocation != null) { +// allLocations.add((Location) parentLocation); +// } +// +// if (childLocationBundle != null) { +// for (Bundle.BundleEntryComponent childLocation : childLocationBundle.getEntry()) { +// Location childLocationEntity = (Location) childLocation.getResource(); +// allLocations.add(childLocationEntity); +// allLocations.addAll(descendants(childLocationEntity.getIdElement().getIdPart(), null)); +// } +// } +// +// return allLocations; +// } +// +// private @Nullable List getLocationsByIds(List locationIds) { +// if (locationIds == null || locationIds.isEmpty()) { +// return new ArrayList<>(); +// } +// +// Bundle locationsBundle = +// getFhirClientForR4() +// .search() +// .forResource(Location.class) +// .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) +// .returnBundle(Bundle.class) +// .execute(); +// +// return locationsBundle.getEntry().stream() +// .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) +// .collect(Collectors.toList()); +// } +// +// private @Nullable Location getLocationsByIdentifier(String identifier) { +// Bundle locationsBundle = +// getFhirClientForR4() +// .search() +// .forResource(Location.class) +// .where(new TokenClientParam(Location.SP_IDENTIFIER).exactly().identifier(identifier)) +// .returnBundle(Bundle.class) +// .execute(); +// +// List locationsList = new ArrayList<>(); +// if (locationsBundle != null) +// locationsList = +// locationsBundle.getEntry().stream() +// .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) +// .collect(Collectors.toList()); +// return locationsList.size() > 0 ? locationsList.get(0) : new Location(); +// } +// +// private IGenericClient getFhirClientForR4() { +// return r4FhirClient; +// } +// } 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 7141d2d9..d1f78ca6 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -15,7 +15,7 @@ */ package org.smartregister.fhir.gateway.plugins; -import static com.google.fhir.gateway.ProxyConstants.SYNC_STRATEGY; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.SYNC_STRATEGY; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.model.api.IQueryParameterType; @@ -30,30 +30,23 @@ import com.google.fhir.gateway.FhirProxyServer; import com.google.fhir.gateway.HttpFhirClient; import com.google.fhir.gateway.JwtUtil; -import com.google.fhir.gateway.interfaces.RequestDetailsReader; -import com.google.fhir.gateway.interfaces.ResourceFinder; -import com.google.fhir.gateway.interfaces.AccessChecker; -import com.google.fhir.gateway.interfaces.AccessDecision; -import com.google.fhir.gateway.interfaces.NoOpAccessDecision; +import com.google.fhir.gateway.interfaces.*; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; - -import java.util.List; -import java.util.ArrayList; -import java.util.Map; -import java.util.HashMap; +import java.util.*; import java.util.stream.Collectors; import javax.inject.Named; import org.apache.commons.lang3.StringUtils; 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.CareTeam; +import org.hl7.fhir.r4.model.Composition; import org.hl7.fhir.r4.model.Organization; +import org.hl7.fhir.r4.model.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.smartregister.fhir.gateway.plugins.interfaces.ResourceFinder; import org.smartregister.model.practitioner.PractitionerDetails; import org.smartregister.utils.Constants; diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java index 25de5326..75bebf41 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java @@ -22,7 +22,9 @@ import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.fhir.gateway.ExceptionUtil; import com.google.fhir.gateway.FhirClientFactory; import com.google.fhir.gateway.HttpFhirClient; import com.google.fhir.gateway.TokenVerifier; @@ -83,33 +85,46 @@ public PractitionerDetailEndpoint() throws IOException { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - // Check the Bearer token to be a valid JWT with required claims. - String authHeader = request.getHeader("Authorization"); - if (authHeader == null) { - throw new ServletException("No Authorization header provided!"); + try { + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "No Authorization header provided!", new AuthenticationException()); + } + List patientIds = new ArrayList<>(); + // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: + DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); + String keycloakUuid = request.getParameter("keycloak-uuid"); + // PractitionerDetails practitionerDetails = + // practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); + + PractitionerDetails practitionerDetails = new PractitionerDetails(); + + logger.info("Searching for practitioner with identifier: " + keycloakUuid); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + + if (practitioner != null) { + + practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); + + } else { + logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + practitionerDetails.setId(PRACTITIONER_NOT_FOUND); + } + response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); + response.setStatus(HttpStatus.SC_OK); } - List patientIds = new ArrayList<>(); - // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: - DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); - String keycloakUuid = request.getParameter("keycloak-uuid"); - // PractitionerDetails practitionerDetails = - // practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); - - PractitionerDetails practitionerDetails = new PractitionerDetails(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - practitionerDetails.setId(PRACTITIONER_NOT_FOUND); + catch (AuthenticationException authenticationException) { + response.setContentType("application/json"); + response.getOutputStream().print(authenticationException.getMessage()); + response.setStatus(authenticationException.getStatusCode()); + } + catch (Exception exception) { + response.setContentType("application/json"); + response.getOutputStream().print(exception.getMessage()); + response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); } - response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); - response.setStatus(HttpStatus.SC_OK); + } public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index 0927a51c..ee5eb0b0 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -15,11 +15,12 @@ */ package org.smartregister.fhir.gateway.plugins; +import static org.smartregister.fhir.gateway.plugins.Constants.CODE_URL_VALUE_SEPARATOR; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.PARAM_VALUES_SEPARATOR; import static org.smartregister.utils.Constants.EMPTY_STRING; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -import com.google.fhir.gateway.ProxyConstants; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; @@ -538,11 +539,8 @@ private List getLocationsHierarchyByOfficialLocationIdentifie public static String createSearchTagValues(Map.Entry entry) { return entry.getKey() - + com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR + + CODE_URL_VALUE_SEPARATOR + StringUtils.join( - entry.getValue(), - com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR - + entry.getKey() - + ProxyConstants.CODE_URL_VALUE_SEPARATOR); + entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); } } diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java index 8b056027..f8c9d854 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ResourceFinderImp.java @@ -19,10 +19,8 @@ import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; -import com.google.fhir.gateway.BundleResources; import com.google.fhir.gateway.ExceptionUtil; import com.google.fhir.gateway.interfaces.RequestDetailsReader; -import com.google.fhir.gateway.interfaces.ResourceFinder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; @@ -32,6 +30,7 @@ import org.hl7.fhir.r4.model.Bundle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.smartregister.fhir.gateway.plugins.interfaces.ResourceFinder; public final class ResourceFinderImp implements ResourceFinder { private static final Logger logger = LoggerFactory.getLogger(ResourceFinderImp.class); diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java deleted file mode 100644 index 8b633ade..00000000 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/RestUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2021-2023 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.smartregister.fhir.gateway.plugins; - -import com.google.fhir.gateway.ResourceFinderImp; - -public class RestUtil { - - private static ResourceFinderImp instance = null; - - // private final FhirContext fhirContext; - // - // public static synchronized ResourceFinderImp getInstance(FhirContext fhirContext) { - // if (instance != null) { - // return instance; - // } else { - // instance = new ResourceFinderImp(fhirContext); - // return instance; - // } - // } -} diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java index 0e3b24f5..db541321 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java @@ -15,7 +15,8 @@ */ package org.smartregister.fhir.gateway.plugins; -import static org.smartregister.fhir.gateway.plugins.ProxyConstants.PARAM_VALUES_SEPARATOR; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.*; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.Literals.EQUALS; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; @@ -130,7 +131,7 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea RequestMutation.builder() .queryParams( Map.of( - com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM, + TAG_SEARCH_PARAM, Arrays.asList(StringUtils.join(syncFilterParameterValues, ",")))) .build(); } @@ -302,12 +303,12 @@ private Map getSyncTags( StringBuilder sb = new StringBuilder(); Map map = new HashMap<>(); - sb.append(com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM); - sb.append(com.google.fhir.gateway.ProxyConstants.Literals.EQUALS); + sb.append(TAG_SEARCH_PARAM); + sb.append(EQUALS); - addTags(com.google.fhir.gateway.ProxyConstants.LOCATION_TAG_URL, locationIds, map, sb); - addTags(com.google.fhir.gateway.ProxyConstants.ORGANISATION_TAG_URL, organizationIds, map, sb); - addTags(com.google.fhir.gateway.ProxyConstants.CARE_TEAM_TAG_URL, careTeamIds, map, sb); + addTags(LOCATION_TAG_URL, locationIds, map, sb); + addTags(ORGANISATION_TAG_URL, organizationIds, map, sb); + addTags(CARE_TEAM_TAG_URL, careTeamIds, map, sb); return map; } @@ -319,10 +320,7 @@ private void addTags( StringBuilder urlStringBuilder) { int len = values.size(); if (len > 0) { - if (urlStringBuilder.length() - != (com.google.fhir.gateway.ProxyConstants.TAG_SEARCH_PARAM - + com.google.fhir.gateway.ProxyConstants.Literals.EQUALS) - .length()) { + if (urlStringBuilder.length() != (TAG_SEARCH_PARAM + EQUALS).length()) { urlStringBuilder.append(PARAM_VALUES_SEPARATOR); } @@ -331,11 +329,11 @@ private void addTags( int i = 0; for (String tagValue : values) { urlStringBuilder.append(tagUrl); - urlStringBuilder.append(com.google.fhir.gateway.ProxyConstants.CODE_URL_VALUE_SEPARATOR); + urlStringBuilder.append(CODE_URL_VALUE_SEPARATOR); urlStringBuilder.append(tagValue); if (i != len - 1) { - urlStringBuilder.append(com.google.fhir.gateway.ProxyConstants.PARAM_VALUES_SEPARATOR); + urlStringBuilder.append(PARAM_VALUES_SEPARATOR); } i++; } diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java index ab1af4b1..41b2d9f3 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/interfaces/ResourceFinder.java @@ -15,9 +15,9 @@ */ package org.smartregister.fhir.gateway.plugins.interfaces; -import com.google.fhir.gateway.BundleResources; import com.google.fhir.gateway.interfaces.RequestDetailsReader; import java.util.List; +import org.smartregister.fhir.gateway.plugins.BundleResources; public interface ResourceFinder { diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java index a52b40dc..d9442c05 100755 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java @@ -17,6 +17,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.smartregister.fhir.gateway.plugins.ProxyConstants.*; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -28,7 +29,6 @@ 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.ProxyConstants; import com.google.fhir.gateway.interfaces.RequestDetailsReader; import com.google.fhir.gateway.interfaces.RequestMutation; import java.io.IOException; @@ -96,7 +96,7 @@ public class SyncAccessDecisionTest { Assert.assertFalse(requestDetails.getCompleteUrl().contains(locationId)); Assert.assertFalse(requestDetails.getRequestPath().contains(locationId)); - locationTagToValuesList.add(ProxyConstants.LOCATION_TAG_URL + "|" + locationId); + locationTagToValuesList.add(LOCATION_TAG_URL + "|" + locationId); } Assert.assertTrue( @@ -111,7 +111,7 @@ public class SyncAccessDecisionTest { for (String careTeamId : careTeamIds) { Assert.assertFalse(requestDetails.getCompleteUrl().contains(careTeamId)); Assert.assertFalse(requestDetails.getRequestPath().contains(careTeamId)); - careteamTagToValuesList.add(ProxyConstants.LOCATION_TAG_URL + "|" + careTeamId); + careteamTagToValuesList.add(LOCATION_TAG_URL + "|" + careTeamId); } Assert.assertTrue( @@ -131,9 +131,7 @@ public class SyncAccessDecisionTest { .getQueryParams() .get("_tag") .get(0) - .contains( - StringUtils.join( - organisationIds, "," + ProxyConstants.ORGANISATION_TAG_URL + "|"))); + .contains(StringUtils.join(organisationIds, "," + ORGANISATION_TAG_URL + "|"))); } @Test @@ -163,11 +161,11 @@ public void preProcessShouldAddLocationIdFiltersWhenUserIsAssignedToLocationsOnl .getQueryParams() .get("_tag") .get(0) - .contains(StringUtils.join(locationIds, "," + ProxyConstants.LOCATION_TAG_URL + "|"))); + .contains(StringUtils.join(locationIds, "," + LOCATION_TAG_URL + "|"))); for (String param : mutatedRequest.getQueryParams().get("_tag")) { - Assert.assertFalse(param.contains(ProxyConstants.CARE_TEAM_TAG_URL)); - Assert.assertFalse(param.contains(ProxyConstants.ORGANISATION_TAG_URL)); + Assert.assertFalse(param.contains(CARE_TEAM_TAG_URL)); + Assert.assertFalse(param.contains(ORGANISATION_TAG_URL)); } } @@ -199,11 +197,11 @@ public void preProcessShouldAddCareTeamIdFiltersWhenUserIsAssignedToCareTeamsOnl .getQueryParams() .get("_tag") .get(0) - .contains(StringUtils.join(careTeamIds, "," + ProxyConstants.CARE_TEAM_TAG_URL + "|"))); + .contains(StringUtils.join(careTeamIds, "," + CARE_TEAM_TAG_URL + "|"))); for (String param : mutatedRequest.getQueryParams().get("_tag")) { - Assert.assertFalse(param.contains(ProxyConstants.LOCATION_TAG_URL)); - Assert.assertFalse(param.contains(ProxyConstants.ORGANISATION_TAG_URL)); + Assert.assertFalse(param.contains(LOCATION_TAG_URL)); + Assert.assertFalse(param.contains(ORGANISATION_TAG_URL)); } } @@ -232,12 +230,12 @@ public void preProcessShouldAddOrganisationIdFiltersWhenUserIsAssignedToOrganisa mutatedRequest .getQueryParams() .get("_tag") - .contains(ProxyConstants.ORGANISATION_TAG_URL + "|" + locationId)); + .contains(ORGANISATION_TAG_URL + "|" + locationId)); } for (String param : mutatedRequest.getQueryParams().get("_tag")) { - Assert.assertFalse(param.contains(ProxyConstants.LOCATION_TAG_URL)); - Assert.assertFalse(param.contains(ProxyConstants.CARE_TEAM_TAG_URL)); + Assert.assertFalse(param.contains(LOCATION_TAG_URL)); + Assert.assertFalse(param.contains(CARE_TEAM_TAG_URL)); } } @@ -268,9 +266,7 @@ public void preProcessShouldAddFiltersWhenResourceNotInSyncFilterIgnoredResource .getQueryParams() .get("_tag") .get(0) - .contains( - StringUtils.join( - organisationIds, "," + ProxyConstants.ORGANISATION_TAG_URL + "|"))); + .contains(StringUtils.join(organisationIds, "," + ORGANISATION_TAG_URL + "|"))); } @Test @@ -356,16 +352,13 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso RequestMutation mutatedRequest = testInstance.getRequestMutation(new TestRequestDetailsToReader(requestDetails)); - List searchParamArrays = - mutatedRequest.getQueryParams().get(ProxyConstants.TAG_SEARCH_PARAM); + List searchParamArrays = mutatedRequest.getQueryParams().get(TAG_SEARCH_PARAM); Assert.assertNotNull(searchParamArrays); Assert.assertTrue( searchParamArrays .get(0) - .contains( - StringUtils.join( - organisationIds, "," + ProxyConstants.ORGANISATION_TAG_URL + "|"))); + .contains(StringUtils.join(organisationIds, "," + ORGANISATION_TAG_URL + "|"))); } @Test(expected = RuntimeException.class) diff --git a/pom.xml b/pom.xml index a7593b28..16bddc64 100755 --- a/pom.xml +++ b/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.2.1-SNAPSHOT + 0.3.1 org.smartregister From 9b4f8f013ecae047bdda22d7d1b4f77c65054ac9 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Fri, 13 Oct 2023 15:29:05 +0500 Subject: [PATCH 10/22] Code refactoring, addressing PR feedback, package and module restructuring --- .../plugins/LocationHierarchyEndpoint.java | 181 ++++++------- .../LocationHierarchyEndpointHelper.java | 252 +++++++++--------- .../plugins/PractitionerDetailEndpoint.java | 62 +++-- .../PractitionerDetailsEndpointHelper.java | 50 ++-- 4 files changed, 265 insertions(+), 280 deletions(-) diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java index 992057e9..eba4451c 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java @@ -1,98 +1,83 @@ -package org.smartregister.fhir.gateway.plugins;/// * -// * Copyright 2021-2023 Google LLC -// * -// * Licensed under the Apache License, Version 2.0 (the "License"); -// * you may not use this file except in compliance with the License. -// * You may obtain a copy of the License at -// * -// * http://www.apache.org/licenses/LICENSE-2.0 -// * -// * Unless required by applicable law or agreed to in writing, software -// * distributed under the License is distributed on an "AS IS" BASIS, -// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// * See the License for the specific language governing permissions and -// * limitations under the License. -// */ -// package org.smartregister.fhir.gateway; -// -// import ca.uhn.fhir.context.FhirContext; -// import ca.uhn.fhir.parser.IParser; -// import ca.uhn.fhir.rest.client.api.IGenericClient; -// import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; -// import com.auth0.jwt.interfaces.DecodedJWT; -// import com.google.fhir.gateway.ExceptionUtil; -// import com.google.fhir.gateway.FhirClientFactory; -// import com.google.fhir.gateway.HttpFhirClient; -// import com.google.fhir.gateway.TokenVerifier; -// import java.io.IOException; -// import java.util.ArrayList; -// import java.util.List; -// import javax.servlet.annotation.WebServlet; -// import javax.servlet.http.HttpServlet; -// import javax.servlet.http.HttpServletRequest; -// import javax.servlet.http.HttpServletResponse; -// import org.apache.http.HttpStatus; -// import org.slf4j.Logger; -// import org.slf4j.LoggerFactory; -// import org.smartregister.model.location.LocationHierarchy; -// -// @WebServlet("/LocationHierarchy") -// public class LocationHierarchyEndpoint extends HttpServlet { -// -// private static final Logger logger = LoggerFactory.getLogger(LocationHierarchyEndpoint.class); -// private final TokenVerifier tokenVerifier; -// -// private final HttpFhirClient fhirClient; -// String PROXY_TO_ENV = "PROXY_TO"; -// -// private FhirContext fhirR4Context = FhirContext.forR4(); -// private IGenericClient r4FhirClient = -// fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); -// -// private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); -// -// private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; -// -// public LocationHierarchyEndpoint() throws IOException { -// this.tokenVerifier = TokenVerifier.createFromEnvVars(); -// this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); -// this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); -// } -// -// @Override -// protected void doGet(HttpServletRequest request, HttpServletResponse response) -// throws IOException { -// // Check the Bearer token to be a valid JWT with required claims. -// try { -// -// String authHeader = request.getHeader("Authorization"); -// if (authHeader == null) { -// ExceptionUtil.throwRuntimeExceptionAndLog( -// logger, "No Authorization header provided!", new AuthenticationException()); -// } -// List patientIds = new ArrayList<>(); -// // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: -// DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); -// String identifier = request.getParameter("identifier"); -// -// LocationHierarchy locationHierarchy = -// locationHierarchyEndpointHelper.getLocationHierarchy(identifier); -// String resultContent = fhirR4JsonParser.encodeResourceToString(locationHierarchy); -// response.setContentType("application/json"); -// response.getOutputStream().print(resultContent); -// response.setStatus(HttpStatus.SC_OK); -// } catch (AuthenticationException authenticationException) { -// response.setContentType("application/json"); -// response.getOutputStream().print(authenticationException.getMessage()); -// response.setStatus(authenticationException.getStatusCode()); -// } catch (Exception exception) { -// response.setContentType("application/json"); -// response.getOutputStream().print(exception.getMessage()); -// response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); -// } -// } -// -// private IGenericClient getFhirClientForR4() { -// return r4FhirClient; -// } -// } +package org.smartregister.fhir.gateway.plugins; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import com.auth0.jwt.interfaces.DecodedJWT; +import com.google.fhir.gateway.ExceptionUtil; +import com.google.fhir.gateway.FhirClientFactory; +import com.google.fhir.gateway.HttpFhirClient; +import com.google.fhir.gateway.TokenVerifier; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.servlet.annotation.WebServlet; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; + +@WebServlet("/LocationHierarchy") +public class LocationHierarchyEndpoint extends HttpServlet { + + private static final Logger logger = LoggerFactory.getLogger(LocationHierarchyEndpoint.class); + private final TokenVerifier tokenVerifier; + + private final HttpFhirClient fhirClient; + String PROXY_TO_ENV = "PROXY_TO"; + + private FhirContext fhirR4Context = FhirContext.forR4(); + private IGenericClient r4FhirClient = + fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + + private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); + + private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; + + public LocationHierarchyEndpoint() throws IOException { + this.tokenVerifier = TokenVerifier.createFromEnvVars(); + this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws IOException { + // Check the Bearer token to be a valid JWT with required claims. + try { + + String authHeader = request.getHeader("Authorization"); + if (authHeader == null) { + ExceptionUtil.throwRuntimeExceptionAndLog( + logger, "No Authorization header provided!", new AuthenticationException()); + } + List patientIds = new ArrayList<>(); + // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: + DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); + String identifier = request.getParameter("identifier"); + + LocationHierarchy locationHierarchy = + locationHierarchyEndpointHelper.getLocationHierarchy(identifier); + String resultContent = fhirR4JsonParser.encodeResourceToString(locationHierarchy); + response.setContentType("application/json"); + response.getOutputStream().print(resultContent); + response.setStatus(HttpStatus.SC_OK); + } catch (AuthenticationException authenticationException) { + response.setContentType("application/json"); + response.getOutputStream().print(authenticationException.getMessage()); + response.setStatus(authenticationException.getStatusCode()); + } catch (Exception exception) { + response.setContentType("application/json"); + response.getOutputStream().print(exception.getMessage()); + response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); + } + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } +} 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 eacb140e..78838fc5 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 @@ -1,4 +1,5 @@ -package org.smartregister.fhir.gateway.plugins;/// * +package org.smartregister.fhir.gateway.plugins; +/// * // * Copyright 2021-2023 Google LLC // * // * Licensed under the Apache License, Version 2.0 (the "License"); @@ -13,128 +14,127 @@ // * See the License for the specific language governing permissions and // * limitations under the License. // */ -// package org.smartregister.fhir.gateway; -// -// import static org.smartregister.utils.Constants.*; -// -// import ca.uhn.fhir.context.FhirContext; -// import ca.uhn.fhir.rest.client.api.IGenericClient; -// import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -// import ca.uhn.fhir.rest.gclient.TokenClientParam; -// import java.util.ArrayList; -// import java.util.List; -// import java.util.stream.Collectors; -// import javax.annotation.Nullable; -// import org.apache.commons.lang3.StringUtils; -// import org.hl7.fhir.r4.model.BaseResource; -// import org.hl7.fhir.r4.model.Bundle; -// import org.hl7.fhir.r4.model.Location; -// import org.hl7.fhir.r4.model.StringType; -// import org.slf4j.Logger; -// import org.slf4j.LoggerFactory; -// import org.smartregister.model.location.LocationHierarchy; -// import org.smartregister.model.location.LocationHierarchyTree; -// -// public class LocationHierarchyEndpointHelper { -// -// private static final Logger logger = -// LoggerFactory.getLogger(LocationHierarchyEndpointHelper.class); -// String PROXY_TO_ENV = "PROXY_TO"; -// -// private FhirContext fhirR4Context = FhirContext.forR4(); -// private IGenericClient r4FhirClient = -// fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); -// -// public LocationHierarchy getLocationHierarchy(String identifier) { -// Location location = getLocationsByIdentifier(identifier); -// String locationId = EMPTY_STRING; -// if (location != null && location.getIdElement() != null) { -// locationId = location.getIdElement().getIdPart(); -// } -// -// LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); -// LocationHierarchy locationHierarchy = new LocationHierarchy(); -// if (StringUtils.isNotBlank(locationId) && location != null) { -// logger.info("Building Location Hierarchy of Location Id : " + locationId); -// locationHierarchyTree.buildTreeFromList(getLocationHierarchy(locationId, location)); -// StringType locationIdString = new StringType().setId(locationId).getIdElement(); -// locationHierarchy.setLocationId(locationIdString); -// locationHierarchy.setId(LOCATION_RESOURCE + locationId); -// -// locationHierarchy.setLocationHierarchyTree(locationHierarchyTree); -// } else { -// locationHierarchy.setId(LOCATION_RESOURCE_NOT_FOUND); -// } -// return locationHierarchy; -// } -// -// private List getLocationHierarchy(String locationId, Location parentLocation) { -// return descendants(locationId, parentLocation); -// } -// -// public List descendants(String locationId, Location parentLocation) { -// -// Bundle childLocationBundle = -// getFhirClientForR4() -// .search() -// .forResource(Location.class) -// .where(new ReferenceClientParam(Location.SP_PARTOF).hasAnyOfIds(locationId)) -// .returnBundle(Bundle.class) -// .execute(); -// -// List allLocations = new ArrayList<>(); -// if (parentLocation != null) { -// allLocations.add((Location) parentLocation); -// } -// -// if (childLocationBundle != null) { -// for (Bundle.BundleEntryComponent childLocation : childLocationBundle.getEntry()) { -// Location childLocationEntity = (Location) childLocation.getResource(); -// allLocations.add(childLocationEntity); -// allLocations.addAll(descendants(childLocationEntity.getIdElement().getIdPart(), null)); -// } -// } -// -// return allLocations; -// } -// -// private @Nullable List getLocationsByIds(List locationIds) { -// if (locationIds == null || locationIds.isEmpty()) { -// return new ArrayList<>(); -// } -// -// Bundle locationsBundle = -// getFhirClientForR4() -// .search() -// .forResource(Location.class) -// .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) -// .returnBundle(Bundle.class) -// .execute(); -// -// return locationsBundle.getEntry().stream() -// .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) -// .collect(Collectors.toList()); -// } -// -// private @Nullable Location getLocationsByIdentifier(String identifier) { -// Bundle locationsBundle = -// getFhirClientForR4() -// .search() -// .forResource(Location.class) -// .where(new TokenClientParam(Location.SP_IDENTIFIER).exactly().identifier(identifier)) -// .returnBundle(Bundle.class) -// .execute(); -// -// List locationsList = new ArrayList<>(); -// if (locationsBundle != null) -// locationsList = -// locationsBundle.getEntry().stream() -// .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) -// .collect(Collectors.toList()); -// return locationsList.size() > 0 ? locationsList.get(0) : new Location(); -// } -// -// private IGenericClient getFhirClientForR4() { -// return r4FhirClient; -// } -// } + +import static org.smartregister.utils.Constants.*; + +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.rest.client.api.IGenericClient; +import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import ca.uhn.fhir.rest.gclient.TokenClientParam; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +import org.hl7.fhir.r4.model.BaseResource; +import org.hl7.fhir.r4.model.Bundle; +import org.hl7.fhir.r4.model.Location; +import org.hl7.fhir.r4.model.StringType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.smartregister.model.location.LocationHierarchy; +import org.smartregister.model.location.LocationHierarchyTree; + +public class LocationHierarchyEndpointHelper { + + private static final Logger logger = + LoggerFactory.getLogger(LocationHierarchyEndpointHelper.class); + String PROXY_TO_ENV = "PROXY_TO"; + + private FhirContext fhirR4Context = FhirContext.forR4(); + private IGenericClient r4FhirClient = + fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + + public LocationHierarchy getLocationHierarchy(String identifier) { + Location location = getLocationsByIdentifier(identifier); + String locationId = EMPTY_STRING; + if (location != null && location.getIdElement() != null) { + locationId = location.getIdElement().getIdPart(); + } + + LocationHierarchyTree locationHierarchyTree = new LocationHierarchyTree(); + LocationHierarchy locationHierarchy = new LocationHierarchy(); + if (StringUtils.isNotBlank(locationId) && location != null) { + logger.info("Building Location Hierarchy of Location Id : " + locationId); + locationHierarchyTree.buildTreeFromList(getLocationHierarchy(locationId, location)); + StringType locationIdString = new StringType().setId(locationId).getIdElement(); + locationHierarchy.setLocationId(locationIdString); + locationHierarchy.setId(LOCATION_RESOURCE + locationId); + + locationHierarchy.setLocationHierarchyTree(locationHierarchyTree); + } else { + locationHierarchy.setId(LOCATION_RESOURCE_NOT_FOUND); + } + return locationHierarchy; + } + + private List getLocationHierarchy(String locationId, Location parentLocation) { + return descendants(locationId, parentLocation); + } + + public List descendants(String locationId, Location parentLocation) { + + Bundle childLocationBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(Location.SP_PARTOF).hasAnyOfIds(locationId)) + .returnBundle(Bundle.class) + .execute(); + + List allLocations = new ArrayList<>(); + if (parentLocation != null) { + allLocations.add((Location) parentLocation); + } + + if (childLocationBundle != null) { + for (Bundle.BundleEntryComponent childLocation : childLocationBundle.getEntry()) { + Location childLocationEntity = (Location) childLocation.getResource(); + allLocations.add(childLocationEntity); + allLocations.addAll(descendants(childLocationEntity.getIdElement().getIdPart(), null)); + } + } + + return allLocations; + } + + private @Nullable List getLocationsByIds(List locationIds) { + if (locationIds == null || locationIds.isEmpty()) { + return new ArrayList<>(); + } + + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) + .returnBundle(Bundle.class) + .execute(); + + return locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + } + + private @Nullable Location getLocationsByIdentifier(String identifier) { + Bundle locationsBundle = + getFhirClientForR4() + .search() + .forResource(Location.class) + .where(new TokenClientParam(Location.SP_IDENTIFIER).exactly().identifier(identifier)) + .returnBundle(Bundle.class) + .execute(); + + List locationsList = new ArrayList<>(); + if (locationsBundle != null) + locationsList = + locationsBundle.getEntry().stream() + .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) + .collect(Collectors.toList()); + return locationsList.size() > 0 ? locationsList.get(0) : new Location(); + } + + private IGenericClient getFhirClientForR4() { + return r4FhirClient; + } +} diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java index 75bebf41..f5bca8c5 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java @@ -16,12 +16,16 @@ package org.smartregister.fhir.gateway.plugins; import static org.smartregister.fhir.gateway.plugins.Constants.CODE_URL_VALUE_SEPARATOR; +import static org.smartregister.fhir.gateway.plugins.Constants.PROXY_TO_ENV; import static org.smartregister.fhir.gateway.plugins.ProxyConstants.PARAM_VALUES_SEPARATOR; import static org.smartregister.utils.Constants.*; import static org.smartregister.utils.Constants.EMPTY_STRING; +import ca.uhn.fhir.context.FhirContext; +import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; +import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.ExceptionUtil; @@ -74,12 +78,21 @@ public class PractitionerDetailEndpoint extends HttpServlet { public static final String PRACTITIONER_GROUP_CODE = "405623001"; public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; public static final Bundle EMPTY_BUNDLE = new Bundle(); - private IGenericClient r4FhirClient; + + private FhirContext fhirR4Context = FhirContext.forR4(); + private IGenericClient r4FhirClient = + fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + + private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); + private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; + private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; + public PractitionerDetailEndpoint() throws IOException { this.tokenVerifier = TokenVerifier.createFromEnvVars(); this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); } @Override @@ -89,7 +102,7 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) String authHeader = request.getHeader("Authorization"); if (authHeader == null) { ExceptionUtil.throwRuntimeExceptionAndLog( - logger, "No Authorization header provided!", new AuthenticationException()); + logger, "No Authorization header provided!", new AuthenticationException()); } List patientIds = new ArrayList<>(); // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: @@ -111,20 +124,19 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); practitionerDetails.setId(PRACTITIONER_NOT_FOUND); } - response.getOutputStream().print("Your patient are: " + String.join(" ", patientIds)); + String resultContent = fhirR4JsonParser.encodeResourceToString(practitionerDetails); + response.setContentType("application/json"); + response.getOutputStream().print(resultContent); response.setStatus(HttpStatus.SC_OK); - } - catch (AuthenticationException authenticationException) { + } catch (AuthenticationException authenticationException) { response.setContentType("application/json"); response.getOutputStream().print(authenticationException.getMessage()); response.setStatus(authenticationException.getStatusCode()); - } - catch (Exception exception) { + } catch (Exception exception) { response.setContentType("application/json"); response.getOutputStream().print(exception.getMessage()); response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); } - } public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { @@ -158,8 +170,7 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); List officialLocationIds = getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); - List locationHierarchies = - getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); + List locationHierarchies = getLocationsHierarchy(officialLocationIds); List attributedLocationsList = getAttributedLocations(locationHierarchies); List attributedOrganizationIds = getOrganizationIdsByLocationIds(attributedLocationsList); @@ -328,8 +339,7 @@ private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner pr // different logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = - getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); + List locationHierarchyList = getLocationsHierarchy(locationsIdentifiers); fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); logger.info("Searching for locations by ids"); @@ -562,22 +572,7 @@ private List mapBundleToOrganizationAffiliation( .collect(Collectors.toList()); } - private List getLocationsHierarchyByOfficialLocationIdentifiers( - List officialLocationIdentifiers) { - if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); - - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(LocationHierarchy.class) - .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) - .returnBundle(Bundle.class) - .execute(); - - return bundle.getEntry().stream() - .map(it -> ((LocationHierarchy) it.getResource())) - .collect(Collectors.toList()); - } + // a public static String createSearchTagValues(Map.Entry entry) { return entry.getKey() @@ -586,6 +581,17 @@ public static String createSearchTagValues(Map.Entry entry) { entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); } + private List getLocationsHierarchy(List locationsIdentifiers) { + List locationHierarchyList = new ArrayList<>(); + TokenParam identifier; + LocationHierarchy locationHierarchy; + for (String locationsIdentifier : locationsIdentifiers) { + locationHierarchy = locationHierarchyEndpointHelper.getLocationHierarchy(locationsIdentifier); + locationHierarchyList.add(locationHierarchy); + } + return locationHierarchyList; + } + private IGenericClient getFhirClientForR4() { return r4FhirClient; } diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index ee5eb0b0..51a6aec0 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -65,18 +65,18 @@ private IGenericClient getFhirClientForR4() { return r4FhirClient; } - public PractitionerDetails getPractitionerDetailsByKeycloakId(String keycloakUuid) { + public PractitionerDetails getPractitionerDetailsByKeycloakId(String keycloakUUID) { PractitionerDetails practitionerDetails = new PractitionerDetails(); - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); + logger.info("Searching for practitioner with identifier: " + keycloakUUID); + Practitioner practitioner = getPractitionerByIdentifier(keycloakUUID); if (practitioner != null) { practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); + logger.error("Practitioner with KC identifier: " + keycloakUUID + " not found"); practitionerDetails.setId(Constants.PRACTITIONER_NOT_FOUND); } @@ -110,12 +110,15 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract List careTeamManagingOrganizationIds = getManagingOrganizationsOfCareTeamIds(careTeamList); - List supervisorCareTeamOrganizationLocationIds = + List organizationAffiliations = getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); + + List supervisorCareTeamOrganizationLocationIds = + getLocationIdsByOrganizationAffiliations(organizationAffiliations); List officialLocationIds = getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); List locationHierarchies = - getLocationsHierarchyByOfficialLocationIdentifiers(officialLocationIds); + getLocationsHierarchyByLocationIds(supervisorCareTeamOrganizationLocationIds); List attributedLocationsList = getAttributedLocations(locationHierarchies); List attributedOrganizationIds = getOrganizationIdsByLocationIds(attributedLocationsList); @@ -248,7 +251,7 @@ private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner pr // TODO Fix Distinct List bothOrganizations = Stream.concat(managingOrganizationTeams.stream(), teams.stream()) - .distinct() + .filter(distinctByKey(Organization::getId)) .collect(Collectors.toList()); fhirPractitionerDetails.setOrganizations(bothOrganizations); @@ -275,17 +278,11 @@ private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner pr fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); - List locationIds = - getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - - List locationsIdentifiers = - getOfficialLocationIdentifiersByLocationIds( - locationIds); // TODO Investigate why the Location ID and official identifiers are - // different + List locationIds = getLocationIdsByOrganizationAffiliations(organizationAffiliations); logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = - getLocationsHierarchyByOfficialLocationIdentifiers(locationsIdentifiers); + List locationHierarchyList = getLocationsHierarchyByLocationIds(locationIds); + fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); logger.info("Searching for locations by ids"); @@ -314,9 +311,9 @@ private Bundle getGroupsAssignedToPractitioner(String practitionerId) { .execute(); } - public static Predicate distinctByKey(Function keyExtractor) { + public static Predicate distinctByKey(Function uniqueKeyExtractor) { Set seen = ConcurrentHashMap.newKeySet(); - return t -> seen.add(keyExtractor.apply(t)); + return t -> seen.add(uniqueKeyExtractor.apply(t)); } private List getPractitionerRolesByPractitionerId(String practitionerId) { @@ -449,15 +446,14 @@ private Bundle getOrganizationsById(List organizationIds) { .collect(Collectors.toList()); } - private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { + private List getOrganizationAffiliationsByOrganizationIds( + List organizationIds) { if (organizationIds == null || organizationIds.isEmpty()) { return new ArrayList<>(); } Bundle organizationAffiliationsBundle = getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); + return mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); } private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { @@ -471,7 +467,7 @@ private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List o .execute(); } - private List getLocationIdentifiersByOrganizationAffiliations( + private List getLocationIdsByOrganizationAffiliations( List organizationAffiliations) { return organizationAffiliations.stream() @@ -520,18 +516,16 @@ private List mapBundleToOrganizationAffiliation( .collect(Collectors.toList()); } - private List getLocationsHierarchyByOfficialLocationIdentifiers( - List officialLocationIdentifiers) { - if (officialLocationIdentifiers.isEmpty()) return new ArrayList<>(); + private List getLocationsHierarchyByLocationIds(List locationIds) { + if (locationIds.isEmpty()) return new ArrayList<>(); Bundle bundle = getFhirClientForR4() .search() .forResource(LocationHierarchy.class) - .where(LocationHierarchy.IDENTIFIER.exactly().codes(officialLocationIdentifiers)) + .where(LocationHierarchy.RES_ID.exactly().codes(locationIds)) .returnBundle(Bundle.class) .execute(); - return bundle.getEntry().stream() .map(it -> ((LocationHierarchy) it.getResource())) .collect(Collectors.toList()); From 887efc0d35dadc744f04a2230e3b3e943060a2f3 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 18 Oct 2023 13:18:28 +0500 Subject: [PATCH 11/22] Locate missing changes from the PR https://github.com/opensrp/fhir-gateway/pull/45/files# --- plugins/pom.xml | 2 +- .../hapi_page_url_allowed_queries.json | 14 ++--- .../plugins/PermissionAccessChecker.java | 54 ++++++++++--------- .../PractitionerDetailsEndpointHelper.java | 32 ++--------- .../gateway/plugins/SyncAccessDecision.java | 15 +++--- .../plugins/SyncAccessDecisionTest.java | 9 ++-- 6 files changed, 57 insertions(+), 69 deletions(-) diff --git a/plugins/pom.xml b/plugins/pom.xml index 6c45a92f..b46a2cfa 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -62,7 +62,7 @@ org.smartregister fhir-common-utils - 0.0.9-SNAPSHOT + 0.0.10-SNAPSHOT compile diff --git a/plugins/resources/hapi_page_url_allowed_queries.json b/plugins/resources/hapi_page_url_allowed_queries.json index 927b2c33..3f47e740 100644 --- a/plugins/resources/hapi_page_url_allowed_queries.json +++ b/plugins/resources/hapi_page_url_allowed_queries.json @@ -6,25 +6,27 @@ "_getpages": "ANY_VALUE" }, "allowExtraParams": true, - "allParamsRequired": false + "allParamsRequired": false, + "requestType": "GET" }, { "path": "Composition", - "methodType": "GET", "queryParams": { "identifier":"ANY_VALUE" }, "allowExtraParams": true, - "allParamsRequired": true + "allParamsRequired": true, + "requestType": "GET" }, { "path": "Binary", - "methodType": "GET", "queryParams": { "_id":"ANY_VALUE" }, "allowExtraParams": true, - "allParamsRequired": true + "allParamsRequired": true, + "requestType": "GET" } + ] -} +} \ No newline at end of file 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 d1f78ca6..d38b2062 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -57,6 +57,7 @@ public class PermissionAccessChecker implements AccessChecker { private SyncAccessDecision syncAccessDecision; private PermissionAccessChecker( + FhirContext fhirContext, String keycloakUUID, List userRoles, ResourceFinderImp resourceFinder, @@ -76,6 +77,7 @@ private PermissionAccessChecker( this.userRoles = userRoles; this.syncAccessDecision = new SyncAccessDecision( + fhirContext, keycloakUUID, applicationId, true, @@ -206,15 +208,14 @@ private String getApplicationIdFromJWT(DecodedJWT jwt) { return JwtUtil.getClaimOrDie(jwt, FHIR_CORE_APPLICATION_ID_CLAIM); } - private IGenericClient createFhirClientForR4() { + private IGenericClient createFhirClientForR4(FhirContext fhirContext) { String fhirServer = System.getenv(PROXY_TO_ENV); - FhirContext ctx = FhirContext.forR4(); - IGenericClient client = ctx.newRestfulGenericClient(fhirServer); + IGenericClient client = fhirContext.newRestfulGenericClient(fhirServer); return client; } - private Composition readCompositionResource(String applicationId) { - IGenericClient client = createFhirClientForR4(); + private Composition readCompositionResource(String applicationId, FhirContext fhirContext) { + IGenericClient client = createFhirClientForR4(fhirContext); Bundle compositionBundle = client .search() @@ -235,13 +236,12 @@ private String getBinaryResourceReference(Composition composition) { List indexes = new ArrayList<>(); String id = ""; if (composition != null && composition.getSection() != null) { - indexes = - composition.getSection().stream() - .filter(v -> v.getFocus().getIdentifier() != null) - .filter(v -> v.getFocus().getIdentifier().getValue() != null) - .filter(v -> v.getFocus().getIdentifier().getValue().equals("application")) - .map(v -> composition.getSection().indexOf(v)) - .collect(Collectors.toList()); + composition.getSection().stream() + .filter(v -> v.getFocus().getIdentifier() != null) + .filter(v -> v.getFocus().getIdentifier().getValue() != null) + .filter(v -> v.getFocus().getIdentifier().getValue().equals("application")) + .map(v -> composition.getSection().indexOf(v)) + .collect(Collectors.toList()); Composition.SectionComponent sectionComponent = composition.getSection().get(0); Reference focus = sectionComponent != null ? sectionComponent.getFocus() : null; id = focus != null ? focus.getReference() : null; @@ -249,8 +249,9 @@ private String getBinaryResourceReference(Composition composition) { return id; } - private Binary findApplicationConfigBinaryResource(String binaryResourceId) { - IGenericClient client = createFhirClientForR4(); + private Binary findApplicationConfigBinaryResource( + String binaryResourceId, FhirContext fhirContext) { + IGenericClient client = createFhirClientForR4(fhirContext); Binary binary = null; if (!binaryResourceId.isBlank()) { binary = client.read().resource(Binary.class).withId(binaryResourceId).execute(); @@ -274,9 +275,9 @@ private String findSyncStrategy(Binary binary) { return syncStrategy; } - private PractitionerDetails readPractitionerDetails(String keycloakUUID) { - IGenericClient client = createFhirClientForR4(); - // Map<> + private PractitionerDetails readPractitionerDetails( + String keycloakUUID, FhirContext fhirContext) { + IGenericClient client = createFhirClientForR4(fhirContext); Bundle practitionerDetailsBundle = client .search() @@ -317,18 +318,19 @@ public AccessChecker create( throws AuthenticationException { List userRoles = getUserRolesFromJWT(jwt); String applicationId = getApplicationIdFromJWT(jwt); - Composition composition = readCompositionResource(applicationId); + Composition composition = readCompositionResource(applicationId, fhirContext); String binaryResourceReference = getBinaryResourceReference(composition); - Binary binary = findApplicationConfigBinaryResource(binaryResourceReference); + Binary binary = findApplicationConfigBinaryResource(binaryResourceReference, fhirContext); String syncStrategy = findSyncStrategy(binary); - PractitionerDetails practitionerDetails = readPractitionerDetails(jwt.getSubject()); + PractitionerDetails practitionerDetails = + readPractitionerDetails(jwt.getSubject(), fhirContext); List careTeams; List organizations; List careTeamIds = new ArrayList<>(); List organizationIds = new ArrayList<>(); List locationIds = new ArrayList<>(); if (StringUtils.isNotBlank(syncStrategy)) { - if (syncStrategy.equals(Constants.CARE_TEAM)) { + if (Constants.CARE_TEAM.equalsIgnoreCase(syncStrategy)) { careTeams = practitionerDetails != null && practitionerDetails.getFhirPractitionerDetails() != null @@ -339,7 +341,7 @@ public AccessChecker create( careTeamIds.add(careTeam.getIdElement().getIdPart()); } } - } else if (syncStrategy.equals(Constants.ORGANIZATION)) { + } else if (Constants.ORGANIZATION.equalsIgnoreCase(syncStrategy)) { organizations = practitionerDetails != null && practitionerDetails.getFhirPractitionerDetails() != null @@ -350,16 +352,20 @@ public AccessChecker create( organizationIds.add(organization.getIdElement().getIdPart()); } } - } else if (syncStrategy.equals(Constants.LOCATION)) { + } else if (Constants.LOCATION.equalsIgnoreCase(syncStrategy)) { locationIds = practitionerDetails != null && practitionerDetails.getFhirPractitionerDetails() != null ? PractitionerDetailsEndpointHelper.getAttributedLocations( practitionerDetails.getFhirPractitionerDetails().getLocationHierarchyList()) : locationIds; - } + } else + throw new IllegalStateException( + "Sync strategy not configured. Please confirm Keycloak fhir_core_app_id attribute for" + + " the user matches the Composition.json config official identifier value"); } return new PermissionAccessChecker( + fhirContext, jwt.getSubject(), userRoles, ResourceFinderImp.getInstance(fhirContext), diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index 51a6aec0..bcba27d2 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -33,7 +33,6 @@ import org.hl7.fhir.r4.model.CareTeam; import org.hl7.fhir.r4.model.Enumerations; import org.hl7.fhir.r4.model.Group; -import org.hl7.fhir.r4.model.Identifier; import org.hl7.fhir.r4.model.Location; import org.hl7.fhir.r4.model.Organization; import org.hl7.fhir.r4.model.OrganizationAffiliation; @@ -55,14 +54,14 @@ public class PractitionerDetailsEndpointHelper { public static final String PRACTITIONER_GROUP_CODE = "405623001"; public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; public static final Bundle EMPTY_BUNDLE = new Bundle(); - private IGenericClient r4FhirClient; + private IGenericClient r4FHIRClient; public PractitionerDetailsEndpointHelper(IGenericClient fhirClient) { - this.r4FhirClient = fhirClient; + this.r4FHIRClient = fhirClient; } private IGenericClient getFhirClientForR4() { - return r4FhirClient; + return r4FHIRClient; } public PractitionerDetails getPractitionerDetailsByKeycloakId(String keycloakUUID) { @@ -115,8 +114,7 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract List supervisorCareTeamOrganizationLocationIds = getLocationIdsByOrganizationAffiliations(organizationAffiliations); - List officialLocationIds = - getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); + List locationHierarchies = getLocationsHierarchyByLocationIds(supervisorCareTeamOrganizationLocationIds); List attributedLocationsList = getAttributedLocations(locationHierarchies); @@ -248,7 +246,7 @@ private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner pr Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); List teams = mapBundleToOrganizations(practitionerOrganizations); - // TODO Fix Distinct + List bothOrganizations = Stream.concat(managingOrganizationTeams.stream(), teams.stream()) .filter(distinctByKey(Organization::getId)) @@ -426,26 +424,6 @@ private Bundle getOrganizationsById(List organizationIds) { .collect(Collectors.toList()); } - private @Nullable List getOfficialLocationIdentifiersByLocationIds( - List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - List locations = getLocationsByIds(locationIds); - - return locations.stream() - .map( - it -> - it.getIdentifier().stream() - .filter( - id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) - .map(it2 -> it2.getValue()) - .collect(Collectors.toList())) - .flatMap(it3 -> it3.stream()) - .collect(Collectors.toList()); - } - private List getOrganizationAffiliationsByOrganizationIds( List organizationIds) { if (organizationIds == null || organizationIds.isEmpty()) { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java index db541321..3a28a40b 100755 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecision.java @@ -54,7 +54,6 @@ public class SyncAccessDecision implements AccessDecision { "SYNC_FILTER_IGNORE_RESOURCES_FILE"; public static final String MATCHES_ANY_VALUE = "ANY_VALUE"; private static final Logger logger = LoggerFactory.getLogger(SyncAccessDecision.class); - private static final int LENGTH_OF_SEARCH_PARAM_AND_EQUALS = 5; private final String syncStrategy; private final String applicationId; private final boolean accessGranted; @@ -65,13 +64,14 @@ public class SyncAccessDecision implements AccessDecision { private IgnoredResourcesConfig config; private String keycloakUUID; private Gson gson = new Gson(); - private FhirContext fhirR4Context = FhirContext.forR4(); - private IParser fhirR4JsonParser = fhirR4Context.newJsonParser(); + private FhirContext fhirR4Context; + private IParser fhirR4JsonParser; private IGenericClient fhirR4Client; private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; public SyncAccessDecision( + FhirContext fhirContext, String keycloakUUID, String applicationId, boolean accessGranted, @@ -80,6 +80,7 @@ public SyncAccessDecision( List organizationIds, String syncStrategy, List roles) { + this.fhirR4Context = fhirContext; this.keycloakUUID = keycloakUUID; this.applicationId = applicationId; this.accessGranted = accessGranted; @@ -96,7 +97,7 @@ public SyncAccessDecision( } catch (NullPointerException e) { logger.error(e.getMessage()); } - + this.fhirR4JsonParser = fhirR4Context.newJsonParser(); this.practitionerDetailsEndpointHelper = new PractitionerDetailsEndpointHelper(fhirR4Client); } @@ -169,7 +170,7 @@ public String postProcess(RequestDetailsReader request, HttpResponse response) if (StringUtils.isNotBlank(gatewayMode)) { resultContent = new BasicResponseHandler().handleResponse(response); - IBaseResource responseResource = fhirR4JsonParser.parseResource(resultContent); + IBaseResource responseResource = this.fhirR4JsonParser.parseResource(resultContent); switch (gatewayMode) { case Constants.LIST_ENTRIES: @@ -187,14 +188,14 @@ public String postProcess(RequestDetailsReader request, HttpResponse response) } if (resultContentBundle != null) - resultContent = fhirR4JsonParser.encodeResourceToString(resultContentBundle); + resultContent = this.fhirR4JsonParser.encodeResourceToString(resultContentBundle); } if (includeAttributedPractitioners(request.getRequestPath())) { Bundle practitionerDetailsBundle = this.practitionerDetailsEndpointHelper.getSupervisorPractitionerDetailsByKeycloakId( keycloakUUID); - resultContent = fhirR4JsonParser.encodeResourceToString(practitionerDetailsBundle); + resultContent = this.fhirR4JsonParser.encodeResourceToString(practitionerDetailsBundle); } return resultContent; diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java index d9442c05..a2d9856d 100755 --- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java +++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/SyncAccessDecisionTest.java @@ -487,9 +487,8 @@ public void testPostProcessWithListModeHeaderSearchByTagShouldFetchListEntriesBu 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); + (ListResource) FhirContext.forR4().newJsonParser().parseResource(testListJson); Bundle bundle = new Bundle(); Bundle.BundleEntryComponent bundleEntryComponent = new Bundle.BundleEntryComponent(); @@ -500,10 +499,10 @@ public void testPostProcessWithListModeHeaderSearchByTagShouldFetchListEntriesBu HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS); TestUtil.setUpFhirResponseMock( - fhirResponseMock, realFhirContext.newJsonParser().encodeResourceToString(bundle)); + fhirResponseMock, FhirContext.forR4().newJsonParser().encodeResourceToString(bundle)); testInstance.setFhirR4Client(iGenericClient); - testInstance.setFhirR4Context(fhirR4Context); + testInstance.setFhirR4Context(FhirContext.forR4()); String resultContent = testInstance.postProcess(requestDetailsSpy, fhirResponseMock); Mockito.verify(iTransaction).withBundle(bundleArgumentCaptor.capture()); @@ -538,8 +537,10 @@ public void cleanUp() { } private SyncAccessDecision createSyncAccessDecisionTestInstance() { + FhirContext fhirR4Context = FhirContext.forR4(); SyncAccessDecision accessDecision = new SyncAccessDecision( + fhirR4Context, "sample-keycloak-id", "sample-application-id", true, From ab1ea14fc801895e0ee8a5ba9d93d1706311051b Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 18 Oct 2023 13:56:21 +0500 Subject: [PATCH 12/22] Locate missing changes from the PR https://github.com/opensrp/fhir-gateway/pull/45/files# --- .../plugins/PractitionerDetailEndpoint.java | 490 +----------------- .../PractitionerDetailsEndpointHelper.java | 30 +- 2 files changed, 18 insertions(+), 502 deletions(-) diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java index f5bca8c5..a7254427 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailEndpoint.java @@ -15,46 +15,27 @@ */ package org.smartregister.fhir.gateway.plugins; -import static org.smartregister.fhir.gateway.plugins.Constants.CODE_URL_VALUE_SEPARATOR; import static org.smartregister.fhir.gateway.plugins.Constants.PROXY_TO_ENV; -import static org.smartregister.fhir.gateway.plugins.ProxyConstants.PARAM_VALUES_SEPARATOR; import static org.smartregister.utils.Constants.*; -import static org.smartregister.utils.Constants.EMPTY_STRING; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.parser.IParser; import ca.uhn.fhir.rest.client.api.IGenericClient; -import ca.uhn.fhir.rest.gclient.ReferenceClientParam; -import ca.uhn.fhir.rest.param.TokenParam; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.google.fhir.gateway.ExceptionUtil; -import com.google.fhir.gateway.FhirClientFactory; -import com.google.fhir.gateway.HttpFhirClient; import com.google.fhir.gateway.TokenVerifier; import java.io.IOException; import java.util.*; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; -import java.util.stream.Stream; -import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpStatus; import org.hl7.fhir.r4.model.*; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.smartregister.model.location.LocationHierarchy; -import org.smartregister.model.location.ParentChildrenMap; -import org.smartregister.model.practitioner.FhirPractitionerDetails; import org.smartregister.model.practitioner.PractitionerDetails; -import org.springframework.lang.Nullable; /** * This is an example servlet that requires a valid JWT to be present as the Bearer Authorization @@ -72,13 +53,6 @@ public class PractitionerDetailEndpoint extends HttpServlet { private static final Logger logger = LoggerFactory.getLogger(PractitionerDetailEndpoint.class); private final TokenVerifier tokenVerifier; - - private final HttpFhirClient fhirClient; - - public static final String PRACTITIONER_GROUP_CODE = "405623001"; - public static final String HTTP_SNOMED_INFO_SCT = "http://snomed.info/sct"; - public static final Bundle EMPTY_BUNDLE = new Bundle(); - private FhirContext fhirR4Context = FhirContext.forR4(); private IGenericClient r4FhirClient = fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); @@ -87,17 +61,14 @@ public class PractitionerDetailEndpoint extends HttpServlet { private PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper; - private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; - public PractitionerDetailEndpoint() throws IOException { this.tokenVerifier = TokenVerifier.createFromEnvVars(); - this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); - this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); + this.practitionerDetailsEndpointHelper = new PractitionerDetailsEndpointHelper(r4FhirClient); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { + throws IOException { try { String authHeader = request.getHeader("Authorization"); if (authHeader == null) { @@ -108,22 +79,8 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) // Note for a more meaningful HTTP status code, we can catch AuthenticationException in: DecodedJWT jwt = tokenVerifier.decodeAndVerifyBearerToken(authHeader); String keycloakUuid = request.getParameter("keycloak-uuid"); - // PractitionerDetails practitionerDetails = - // practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); - - PractitionerDetails practitionerDetails = new PractitionerDetails(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - practitionerDetails.setId(PRACTITIONER_NOT_FOUND); - } + PractitionerDetails practitionerDetails = + practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(keycloakUuid); String resultContent = fhirR4JsonParser.encodeResourceToString(practitionerDetails); response.setContentType("application/json"); response.getOutputStream().print(resultContent); @@ -139,253 +96,6 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } } - public Bundle getSupervisorPractitionerDetailsByKeycloakId(String keycloakUuid) { - Bundle bundle = new Bundle(); - - logger.info("Searching for practitioner with identifier: " + keycloakUuid); - Practitioner practitioner = getPractitionerByIdentifier(keycloakUuid); - - if (practitioner != null) { - - bundle = getAttributedPractitionerDetailsByPractitioner(practitioner); - - } else { - logger.error("Practitioner with KC identifier: " + keycloakUuid + " not found"); - } - - return bundle; - } - - private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner practitioner) { - Bundle responseBundle = new Bundle(); - List attributedPractitioners = new ArrayList<>(); - PractitionerDetails practitionerDetails = getPractitionerDetailsByPractitioner(practitioner); - - List careTeamList = practitionerDetails.getFhirPractitionerDetails().getCareTeams(); - // Get other guys. - - List careTeamManagingOrganizationIds = - getManagingOrganizationsOfCareTeamIds(careTeamList); - List supervisorCareTeamOrganizationLocationIds = - getOrganizationAffiliationsByOrganizationIds(careTeamManagingOrganizationIds); - List officialLocationIds = - getOfficialLocationIdentifiersByLocationIds(supervisorCareTeamOrganizationLocationIds); - List locationHierarchies = getLocationsHierarchy(officialLocationIds); - List attributedLocationsList = getAttributedLocations(locationHierarchies); - List attributedOrganizationIds = - getOrganizationIdsByLocationIds(attributedLocationsList); - - // Get care teams by organization Ids - List attributedCareTeams = getCareTeamsByOrganizationIds(attributedOrganizationIds); - - for (CareTeam careTeam : careTeamList) { - attributedCareTeams.removeIf(it -> it.getId().equals(careTeam.getId())); - } - - careTeamList.addAll(attributedCareTeams); - - for (CareTeam careTeam : careTeamList) { - // Add current supervisor practitioners - attributedPractitioners.addAll( - careTeam.getParticipant().stream() - .filter( - it -> - it.hasMember() - && it.getMember() - .getReference() - .startsWith(Enumerations.ResourceType.PRACTITIONER.toCode())) - .map( - it -> - getPractitionerByIdentifier( - getReferenceIDPart(it.getMember().getReference()))) - .collect(Collectors.toList())); - } - - List bundleEntryComponentList = new ArrayList<>(); - - for (Practitioner attributedPractitioner : attributedPractitioners) { - bundleEntryComponentList.add( - new Bundle.BundleEntryComponent() - .setResource(getPractitionerDetailsByPractitioner(attributedPractitioner))); - } - - responseBundle.setEntry(bundleEntryComponentList); - responseBundle.setTotal(bundleEntryComponentList.size()); - return responseBundle; - } - - @NotNull - public static List getAttributedLocations(List locationHierarchies) { - List parentChildrenList = - locationHierarchies.stream() - .flatMap( - locationHierarchy -> - locationHierarchy - .getLocationHierarchyTree() - .getLocationsHierarchy() - .getParentChildren() - .stream()) - .collect(Collectors.toList()); - List attributedLocationsList = - parentChildrenList.stream() - .flatMap(parentChildren -> parentChildren.getChildIdentifiers().stream()) - .map(it -> getReferenceIDPart(it.toString())) - .collect(Collectors.toList()); - return attributedLocationsList; - } - - private List getOrganizationIdsByLocationIds(List attributedLocationsList) { - if (attributedLocationsList == null || attributedLocationsList.isEmpty()) { - return new ArrayList<>(); - } - - Bundle organizationAffiliationsBundle = - getFhirClientForR4() - .search() - .forResource(OrganizationAffiliation.class) - .where(OrganizationAffiliation.LOCATION.hasAnyOfIds(attributedLocationsList)) - .returnBundle(Bundle.class) - .execute(); - - return organizationAffiliationsBundle.getEntry().stream() - .map( - bundleEntryComponent -> - getReferenceIDPart( - ((OrganizationAffiliation) bundleEntryComponent.getResource()) - .getOrganization() - .getReference())) - .distinct() - .collect(Collectors.toList()); - } - - private String getPractitionerIdentifier(Practitioner practitioner) { - String practitionerId = EMPTY_STRING; - if (practitioner.getIdElement() != null && practitioner.getIdElement().getIdPart() != null) { - practitionerId = practitioner.getIdElement().getIdPart(); - } - return practitionerId; - } - - private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { - - PractitionerDetails practitionerDetails = new PractitionerDetails(); - FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); - String practitionerId = getPractitionerIdentifier(practitioner); - - logger.info("Searching for care teams for practitioner with id: " + practitioner); - Bundle careTeams = getCareTeams(practitionerId); - List careTeamsList = mapBundleToCareTeams(careTeams); - fhirPractitionerDetails.setCareTeams(careTeamsList); - fhirPractitionerDetails.setPractitioners(Arrays.asList(practitioner)); - - logger.info("Searching for Organizations tied with CareTeams: "); - List careTeamManagingOrganizationIds = - getManagingOrganizationsOfCareTeamIds(careTeamsList); - - Bundle careTeamManagingOrganizations = getOrganizationsById(careTeamManagingOrganizationIds); - logger.info("Managing Organization are fetched"); - - List managingOrganizationTeams = - mapBundleToOrganizations(careTeamManagingOrganizations); - - logger.info("Searching for organizations of practitioner with id: " + practitioner); - - List practitionerRoleList = - getPractitionerRolesByPractitionerId(practitionerId); - logger.info("Practitioner Roles are fetched"); - - List practitionerOrganizationIds = - getOrganizationIdsByPractitionerRoles(practitionerRoleList); - - Bundle practitionerOrganizations = getOrganizationsById(practitionerOrganizationIds); - - List teams = mapBundleToOrganizations(practitionerOrganizations); - // TODO Fix Distinct - List bothOrganizations = - Stream.concat(managingOrganizationTeams.stream(), teams.stream()) - .distinct() - .collect(Collectors.toList()); - - fhirPractitionerDetails.setOrganizations(bothOrganizations); - fhirPractitionerDetails.setPractitionerRoles(practitionerRoleList); - - Bundle groupsBundle = getGroupsAssignedToPractitioner(practitionerId); - logger.info("Groups are fetched"); - - List groupsList = mapBundleToGroups(groupsBundle); - fhirPractitionerDetails.setGroups(groupsList); - fhirPractitionerDetails.setId(practitionerId); - - logger.info("Searching for locations by organizations"); - - Bundle organizationAffiliationsBundle = - getOrganizationAffiliationsByOrganizationIdsBundle( - Stream.concat( - careTeamManagingOrganizationIds.stream(), practitionerOrganizationIds.stream()) - .distinct() - .collect(Collectors.toList())); - - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - - fhirPractitionerDetails.setOrganizationAffiliations(organizationAffiliations); - - List locationIds = - getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - - List locationsIdentifiers = - getOfficialLocationIdentifiersByLocationIds( - locationIds); // TODO Investigate why the Location ID and official identifiers are - // different - - logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = getLocationsHierarchy(locationsIdentifiers); - fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); - - logger.info("Searching for locations by ids"); - List locationsList = getLocationsByIds(locationIds); - fhirPractitionerDetails.setLocations(locationsList); - - practitionerDetails.setId(practitionerId); - practitionerDetails.setFhirPractitionerDetails(fhirPractitionerDetails); - - return practitionerDetails; - } - - private List mapBundleToOrganizations(Bundle organizationBundle) { - return organizationBundle.getEntry().stream() - .map(bundleEntryComponent -> (Organization) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private Bundle getGroupsAssignedToPractitioner(String practitionerId) { - return getFhirClientForR4() - .search() - .forResource(Group.class) - .where(Group.MEMBER.hasId(practitionerId)) - .where(Group.CODE.exactly().systemAndCode(HTTP_SNOMED_INFO_SCT, PRACTITIONER_GROUP_CODE)) - .returnBundle(Bundle.class) - .execute(); - } - - public static Predicate distinctByKey(Function keyExtractor) { - Set seen = ConcurrentHashMap.newKeySet(); - return t -> seen.add(keyExtractor.apply(t)); - } - - private List getPractitionerRolesByPractitionerId(String practitionerId) { - Bundle practitionerRoles = getPractitionerRoles(practitionerId); - return mapBundleToPractitionerRolesWithOrganization(practitionerRoles); - } - - private List getOrganizationIdsByPractitionerRoles( - List practitionerRoles) { - return practitionerRoles.stream() - .filter(practitionerRole -> practitionerRole.hasOrganization()) - .map(it -> getReferenceIDPart(it.getOrganization().getReference())) - .collect(Collectors.toList()); - } - private Practitioner getPractitionerByIdentifier(String identifier) { Bundle resultBundle = getFhirClientForR4() @@ -400,198 +110,6 @@ private Practitioner getPractitionerByIdentifier(String identifier) { : null; } - private List getCareTeamsByOrganizationIds(List organizationIds) { - if (organizationIds.isEmpty()) return new ArrayList<>(); - - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(CareTeam.class) - .where( - CareTeam.PARTICIPANT.hasAnyOfIds( - organizationIds.stream() - .map( - it -> - Enumerations.ResourceType.ORGANIZATION.toCode() - + FORWARD_SLASH - + it) - .collect(Collectors.toList()))) - .returnBundle(Bundle.class) - .execute(); - - return bundle.getEntry().stream() - .filter(it -> ((CareTeam) it.getResource()).hasManagingOrganization()) - .map(it -> ((CareTeam) it.getResource())) - .collect(Collectors.toList()); - } - - private Bundle getCareTeams(String practitionerId) { - logger.info("Searching for Care Teams with practitioner id :" + practitionerId); - - return getFhirClientForR4() - .search() - .forResource(CareTeam.class) - .where( - CareTeam.PARTICIPANT.hasId( - Enumerations.ResourceType.PRACTITIONER.toCode() + FORWARD_SLASH + practitionerId)) - .returnBundle(Bundle.class) - .execute(); - } - - private Bundle getPractitionerRoles(String practitionerId) { - logger.info("Searching for Practitioner roles with practitioner id :" + practitionerId); - return getFhirClientForR4() - .search() - .forResource(PractitionerRole.class) - .where(PractitionerRole.PRACTITIONER.hasId(practitionerId)) - .returnBundle(Bundle.class) - .execute(); - } - - private static String getReferenceIDPart(String reference) { - return reference.substring(reference.indexOf(FORWARD_SLASH) + 1); - } - - private Bundle getOrganizationsById(List organizationIds) { - return organizationIds.isEmpty() - ? EMPTY_BUNDLE - : getFhirClientForR4() - .search() - .forResource(Organization.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(organizationIds)) - .returnBundle(Bundle.class) - .execute(); - } - - private @Nullable List getLocationsByIds(List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - Bundle locationsBundle = - getFhirClientForR4() - .search() - .forResource(Location.class) - .where(new ReferenceClientParam(BaseResource.SP_RES_ID).hasAnyOfIds(locationIds)) - .returnBundle(Bundle.class) - .execute(); - - return locationsBundle.getEntry().stream() - .map(bundleEntryComponent -> ((Location) bundleEntryComponent.getResource())) - .collect(Collectors.toList()); - } - - private @Nullable List getOfficialLocationIdentifiersByLocationIds( - List locationIds) { - if (locationIds == null || locationIds.isEmpty()) { - return new ArrayList<>(); - } - - List locations = getLocationsByIds(locationIds); - - return locations.stream() - .map( - it -> - it.getIdentifier().stream() - .filter( - id -> id.hasUse() && id.getUse().equals(Identifier.IdentifierUse.OFFICIAL)) - .map(it2 -> it2.getValue()) - .collect(Collectors.toList())) - .flatMap(it3 -> it3.stream()) - .collect(Collectors.toList()); - } - - private List getOrganizationAffiliationsByOrganizationIds(List organizationIds) { - if (organizationIds == null || organizationIds.isEmpty()) { - return new ArrayList<>(); - } - Bundle organizationAffiliationsBundle = - getOrganizationAffiliationsByOrganizationIdsBundle(organizationIds); - List organizationAffiliations = - mapBundleToOrganizationAffiliation(organizationAffiliationsBundle); - return getLocationIdentifiersByOrganizationAffiliations(organizationAffiliations); - } - - private Bundle getOrganizationAffiliationsByOrganizationIdsBundle(List organizationIds) { - return organizationIds.isEmpty() - ? EMPTY_BUNDLE - : getFhirClientForR4() - .search() - .forResource(OrganizationAffiliation.class) - .where(OrganizationAffiliation.PRIMARY_ORGANIZATION.hasAnyOfIds(organizationIds)) - .returnBundle(Bundle.class) - .execute(); - } - - private List getLocationIdentifiersByOrganizationAffiliations( - List organizationAffiliations) { - - return organizationAffiliations.stream() - .map( - organizationAffiliation -> - getReferenceIDPart( - organizationAffiliation.getLocation().stream() - .findFirst() - .get() - .getReference())) - .collect(Collectors.toList()); - } - - private List getManagingOrganizationsOfCareTeamIds(List careTeamsList) { - logger.info("Searching for Organizations with care teams list of size:" + careTeamsList.size()); - return careTeamsList.stream() - .filter(careTeam -> careTeam.hasManagingOrganization()) - .flatMap(it -> it.getManagingOrganization().stream()) - .map(it -> getReferenceIDPart(it.getReference())) - .collect(Collectors.toList()); - } - - private List mapBundleToCareTeams(Bundle careTeams) { - return careTeams.getEntry().stream() - .map(bundleEntryComponent -> (CareTeam) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToPractitionerRolesWithOrganization( - Bundle practitionerRoles) { - return practitionerRoles.getEntry().stream() - .map(it -> (PractitionerRole) it.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToGroups(Bundle groupsBundle) { - return groupsBundle.getEntry().stream() - .map(bundleEntryComponent -> (Group) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - private List mapBundleToOrganizationAffiliation( - Bundle organizationAffiliationBundle) { - return organizationAffiliationBundle.getEntry().stream() - .map(bundleEntryComponent -> (OrganizationAffiliation) bundleEntryComponent.getResource()) - .collect(Collectors.toList()); - } - - // a - - public static String createSearchTagValues(Map.Entry entry) { - return entry.getKey() - + CODE_URL_VALUE_SEPARATOR - + StringUtils.join( - entry.getValue(), PARAM_VALUES_SEPARATOR + entry.getKey() + CODE_URL_VALUE_SEPARATOR); - } - - private List getLocationsHierarchy(List locationsIdentifiers) { - List locationHierarchyList = new ArrayList<>(); - TokenParam identifier; - LocationHierarchy locationHierarchy; - for (String locationsIdentifier : locationsIdentifiers) { - locationHierarchy = locationHierarchyEndpointHelper.getLocationHierarchy(locationsIdentifier); - locationHierarchyList.add(locationHierarchy); - } - return locationHierarchyList; - } - private IGenericClient getFhirClientForR4() { return r4FhirClient; } diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index bcba27d2..3417469e 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -56,8 +56,11 @@ public class PractitionerDetailsEndpointHelper { public static final Bundle EMPTY_BUNDLE = new Bundle(); private IGenericClient r4FHIRClient; + private LocationHierarchyEndpointHelper locationHierarchyEndpointHelper; + public PractitionerDetailsEndpointHelper(IGenericClient fhirClient) { this.r4FHIRClient = fhirClient; + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); } private IGenericClient getFhirClientForR4() { @@ -116,7 +119,7 @@ private Bundle getAttributedPractitionerDetailsByPractitioner(Practitioner pract getLocationIdsByOrganizationAffiliations(organizationAffiliations); List locationHierarchies = - getLocationsHierarchyByLocationIds(supervisorCareTeamOrganizationLocationIds); + getLocationsHierarchy(supervisorCareTeamOrganizationLocationIds); List attributedLocationsList = getAttributedLocations(locationHierarchies); List attributedOrganizationIds = getOrganizationIdsByLocationIds(attributedLocationsList); @@ -212,7 +215,7 @@ private String getPractitionerIdentifier(Practitioner practitioner) { return practitionerId; } - private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { + public PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner practitioner) { PractitionerDetails practitionerDetails = new PractitionerDetails(); FhirPractitionerDetails fhirPractitionerDetails = new FhirPractitionerDetails(); @@ -279,7 +282,7 @@ private PractitionerDetails getPractitionerDetailsByPractitioner(Practitioner pr List locationIds = getLocationIdsByOrganizationAffiliations(organizationAffiliations); logger.info("Searching for location hierarchy list by locations identifiers"); - List locationHierarchyList = getLocationsHierarchyByLocationIds(locationIds); + List locationHierarchyList = getLocationsHierarchy(locationIds); fhirPractitionerDetails.setLocationHierarchyList(locationHierarchyList); @@ -494,19 +497,14 @@ private List mapBundleToOrganizationAffiliation( .collect(Collectors.toList()); } - private List getLocationsHierarchyByLocationIds(List locationIds) { - if (locationIds.isEmpty()) return new ArrayList<>(); - - Bundle bundle = - getFhirClientForR4() - .search() - .forResource(LocationHierarchy.class) - .where(LocationHierarchy.RES_ID.exactly().codes(locationIds)) - .returnBundle(Bundle.class) - .execute(); - return bundle.getEntry().stream() - .map(it -> ((LocationHierarchy) it.getResource())) - .collect(Collectors.toList()); + private List getLocationsHierarchy(List locationsIdentifiers) { + List locationHierarchyList = new ArrayList<>(); + LocationHierarchy locationHierarchy; + for (String locationsIdentifier : locationsIdentifiers) { + locationHierarchy = locationHierarchyEndpointHelper.getLocationHierarchy(locationsIdentifier); + locationHierarchyList.add(locationHierarchy); + } + return locationHierarchyList; } public static String createSearchTagValues(Map.Entry entry) { From 88609ce372a1c8ca31bc78f7de48feacab503b8d Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 18 Oct 2023 16:54:05 +0500 Subject: [PATCH 13/22] Minor fixes and code refactoring --- exec/pom.xml | 8 ++++++++ .../smartregister/fhir/gateway/MainApp.java | 1 - .../plugins/LocationHierarchyEndpoint.java | 6 +++--- .../LocationHierarchyEndpointHelper.java | 18 +++++++++--------- .../plugins/PermissionAccessChecker.java | 11 ++++++++--- .../PractitionerDetailsEndpointHelper.java | 2 +- .../fhir/gateway/plugins/ProxyConstants.java | 2 ++ 7 files changed, 31 insertions(+), 17 deletions(-) diff --git a/exec/pom.xml b/exec/pom.xml index 817888fd..3e1f9469 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -51,6 +51,14 @@ + + + + ca.uhn.hapi.fhir + hapi-fhir-server + ${hapifhir_version} + + com.google.fhir.gateway diff --git a/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java b/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java index cc952f89..c51dbdae 100644 --- a/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java +++ b/exec/src/main/java/org/smartregister/fhir/gateway/MainApp.java @@ -29,7 +29,6 @@ public class MainApp { public static void main(String[] args) { - System.out.println("Running exec module"); SpringApplication.run(MainApp.class, args); } } diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java index eba4451c..a8572a8d 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpoint.java @@ -31,7 +31,7 @@ public class LocationHierarchyEndpoint extends HttpServlet { String PROXY_TO_ENV = "PROXY_TO"; private FhirContext fhirR4Context = FhirContext.forR4(); - private IGenericClient r4FhirClient = + private IGenericClient r4FHIRClient = fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); private IParser fhirR4JsonParser = fhirR4Context.newJsonParser().setPrettyPrint(true); @@ -41,7 +41,7 @@ public class LocationHierarchyEndpoint extends HttpServlet { public LocationHierarchyEndpoint() throws IOException { this.tokenVerifier = TokenVerifier.createFromEnvVars(); this.fhirClient = FhirClientFactory.createFhirClientFromEnvVars(); - this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(r4FHIRClient); } @Override @@ -78,6 +78,6 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) } private IGenericClient getFhirClientForR4() { - return r4FhirClient; + return r4FHIRClient; } } 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 78838fc5..7845c096 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 @@ -17,7 +17,6 @@ import static org.smartregister.utils.Constants.*; -import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.rest.client.api.IGenericClient; import ca.uhn.fhir.rest.gclient.ReferenceClientParam; import ca.uhn.fhir.rest.gclient.TokenClientParam; @@ -39,11 +38,16 @@ public class LocationHierarchyEndpointHelper { private static final Logger logger = LoggerFactory.getLogger(LocationHierarchyEndpointHelper.class); - String PROXY_TO_ENV = "PROXY_TO"; - private FhirContext fhirR4Context = FhirContext.forR4(); - private IGenericClient r4FhirClient = - fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)); + private IGenericClient r4FHIRClient; + + public LocationHierarchyEndpointHelper(IGenericClient fhirClient) { + this.r4FHIRClient = fhirClient; + } + + private IGenericClient getFhirClientForR4() { + return r4FHIRClient; + } public LocationHierarchy getLocationHierarchy(String identifier) { Location location = getLocationsByIdentifier(identifier); @@ -133,8 +137,4 @@ public List descendants(String locationId, Location parentLocation) { .collect(Collectors.toList()); return locationsList.size() > 0 ? locationsList.get(0) : new Location(); } - - private IGenericClient getFhirClientForR4() { - return r4FhirClient; - } } 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 d38b2062..2fcf03b3 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -237,9 +237,14 @@ private String getBinaryResourceReference(Composition composition) { String id = ""; if (composition != null && composition.getSection() != null) { composition.getSection().stream() - .filter(v -> v.getFocus().getIdentifier() != null) - .filter(v -> v.getFocus().getIdentifier().getValue() != null) - .filter(v -> v.getFocus().getIdentifier().getValue().equals("application")) + .filter( + v -> + v.getFocus().getIdentifier() != null + && v.getFocus().getIdentifier().getValue() != null + && v.getFocus() + .getIdentifier() + .getValue() + .equals(ProxyConstants.APPLICATION)) .map(v -> composition.getSection().indexOf(v)) .collect(Collectors.toList()); Composition.SectionComponent sectionComponent = composition.getSection().get(0); diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java index 3417469e..9ee0ed88 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PractitionerDetailsEndpointHelper.java @@ -60,7 +60,7 @@ public class PractitionerDetailsEndpointHelper { public PractitionerDetailsEndpointHelper(IGenericClient fhirClient) { this.r4FHIRClient = fhirClient; - this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(); + this.locationHierarchyEndpointHelper = new LocationHierarchyEndpointHelper(r4FHIRClient); } private IGenericClient getFhirClientForR4() { diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java index 3488bc8c..2b28bb2d 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/ProxyConstants.java @@ -39,6 +39,8 @@ public class ProxyConstants { public static final String SYNC_STRATEGY = "syncStrategy"; public static final String REALM_ACCESS = "realm_access"; + public static final String APPLICATION = "application"; + public interface Literals { String EQUALS = "="; } From 95ae3db6543d81335d3cc1da16fea623d454c948 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 18 Oct 2023 17:01:44 +0500 Subject: [PATCH 14/22] Address PR feedback --- .../fhir/gateway/plugins/PermissionAccessChecker.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 2fcf03b3..4ffbe046 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -233,10 +233,10 @@ private Composition readCompositionResource(String applicationId, FhirContext fh } private String getBinaryResourceReference(Composition composition) { - List indexes = new ArrayList<>(); + String id = ""; if (composition != null && composition.getSection() != null) { - composition.getSection().stream() + Optional firstIndex = composition.getSection().stream() .filter( v -> v.getFocus().getIdentifier() != null @@ -246,8 +246,10 @@ private String getBinaryResourceReference(Composition composition) { .getValue() .equals(ProxyConstants.APPLICATION)) .map(v -> composition.getSection().indexOf(v)) - .collect(Collectors.toList()); - Composition.SectionComponent sectionComponent = composition.getSection().get(0); + .findFirst(); + + Integer result = firstIndex.orElse(-1); + Composition.SectionComponent sectionComponent = composition.getSection().get(result); Reference focus = sectionComponent != null ? sectionComponent.getFocus() : null; id = focus != null ? focus.getReference() : null; } From b03066ac23de4a9c652ddd221be3b1a64dc84016 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 18 Oct 2023 17:39:47 +0500 Subject: [PATCH 15/22] Address PR feedback --- plugins/resources/README.md | 14 +---------- .../hapi_page_url_allowed_queries.json | 8 +++---- .../plugins/PermissionAccessChecker.java | 24 +++++++++---------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/plugins/resources/README.md b/plugins/resources/README.md index f6125144..bf33049a 100644 --- a/plugins/resources/README.md +++ b/plugins/resources/README.md @@ -1,13 +1 @@ -# Files description - -- `patient-list-example.json`: This is a sample list of patient IDs that can be - uploaded to a FHIR store and used as authorization list: - ```shell - $ curl --request PUT \ - -H "Authorization: Bearer $(gcloud auth print-access-token)" \ - -H "Content-Type: application/fhir+json; charset=utf-8" \ - "https://healthcare.googleapis.com/v1/projects/fhir-sdk/locations/us/datasets/synthea-sample-data/fhirStores/gcs-data/fhir/List/patient-list-example" \ - -d @patient-list-example.json - ``` - The test user that is configured on the default test Keycloak IDP has the ID - of this list as its `patient_list` claim. +# FHIR Gateway Plugin diff --git a/plugins/resources/hapi_page_url_allowed_queries.json b/plugins/resources/hapi_page_url_allowed_queries.json index 3f47e740..e7912fee 100644 --- a/plugins/resources/hapi_page_url_allowed_queries.json +++ b/plugins/resources/hapi_page_url_allowed_queries.json @@ -10,21 +10,21 @@ "requestType": "GET" }, { - "path": "Composition", + "path": "Composition/", "queryParams": { "identifier":"ANY_VALUE" }, "allowExtraParams": true, - "allParamsRequired": true, + "allParamsRequired": false, "requestType": "GET" }, { - "path": "Binary", + "path": "Binary/", "queryParams": { "_id":"ANY_VALUE" }, "allowExtraParams": true, - "allParamsRequired": true, + "allParamsRequired": false, "requestType": "GET" } 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 4ffbe046..50184e67 100644 --- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java +++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java @@ -35,7 +35,6 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; import java.util.*; -import java.util.stream.Collectors; import javax.inject.Named; import org.apache.commons.lang3.StringUtils; import org.hl7.fhir.r4.model.Binary; @@ -236,17 +235,18 @@ private String getBinaryResourceReference(Composition composition) { String id = ""; if (composition != null && composition.getSection() != null) { - Optional firstIndex = composition.getSection().stream() - .filter( - v -> - v.getFocus().getIdentifier() != null - && v.getFocus().getIdentifier().getValue() != null - && v.getFocus() - .getIdentifier() - .getValue() - .equals(ProxyConstants.APPLICATION)) - .map(v -> composition.getSection().indexOf(v)) - .findFirst(); + Optional firstIndex = + composition.getSection().stream() + .filter( + v -> + v.getFocus().getIdentifier() != null + && v.getFocus().getIdentifier().getValue() != null + && v.getFocus() + .getIdentifier() + .getValue() + .equals(ProxyConstants.APPLICATION)) + .map(v -> composition.getSection().indexOf(v)) + .findFirst(); Integer result = firstIndex.orElse(-1); Composition.SectionComponent sectionComponent = composition.getSection().get(result); From 921d526b52250a418633e498b9e7b69c715ae8f1 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Wed, 18 Oct 2023 17:42:23 +0500 Subject: [PATCH 16/22] Update configurations --- plugins/resources/hapi_page_url_allowed_queries.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugins/resources/hapi_page_url_allowed_queries.json b/plugins/resources/hapi_page_url_allowed_queries.json index e7912fee..a2aa8c2b 100644 --- a/plugins/resources/hapi_page_url_allowed_queries.json +++ b/plugins/resources/hapi_page_url_allowed_queries.json @@ -14,6 +14,7 @@ "queryParams": { "identifier":"ANY_VALUE" }, + "allowUnauthenticatedRequests": true, "allowExtraParams": true, "allParamsRequired": false, "requestType": "GET" @@ -23,6 +24,7 @@ "queryParams": { "_id":"ANY_VALUE" }, + "allowUnauthenticatedRequests": true, "allowExtraParams": true, "allParamsRequired": false, "requestType": "GET" From 7b5fb7e5fadfd1cd67fb167b6f788e35b55f040e Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Mon, 23 Oct 2023 15:42:12 +0500 Subject: [PATCH 17/22] Exemption of Composition and Binary resource from the authentication workflow --- .../hapi_page_url_allowed_queries.json | 4 +-- resources/hapi_page_url_allowed_queries.json | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 resources/hapi_page_url_allowed_queries.json diff --git a/plugins/resources/hapi_page_url_allowed_queries.json b/plugins/resources/hapi_page_url_allowed_queries.json index a2aa8c2b..7f0152a4 100644 --- a/plugins/resources/hapi_page_url_allowed_queries.json +++ b/plugins/resources/hapi_page_url_allowed_queries.json @@ -10,7 +10,7 @@ "requestType": "GET" }, { - "path": "Composition/", + "path": "Composition/ANY_VALUE", "queryParams": { "identifier":"ANY_VALUE" }, @@ -20,7 +20,7 @@ "requestType": "GET" }, { - "path": "Binary/", + "path": "Binary/ANY_VALUE", "queryParams": { "_id":"ANY_VALUE" }, diff --git a/resources/hapi_page_url_allowed_queries.json b/resources/hapi_page_url_allowed_queries.json new file mode 100644 index 00000000..7f0152a4 --- /dev/null +++ b/resources/hapi_page_url_allowed_queries.json @@ -0,0 +1,34 @@ +{ + "entries": [ + { + "path": "", + "queryParams": { + "_getpages": "ANY_VALUE" + }, + "allowExtraParams": true, + "allParamsRequired": false, + "requestType": "GET" + }, + { + "path": "Composition/ANY_VALUE", + "queryParams": { + "identifier":"ANY_VALUE" + }, + "allowUnauthenticatedRequests": true, + "allowExtraParams": true, + "allParamsRequired": false, + "requestType": "GET" + }, + { + "path": "Binary/ANY_VALUE", + "queryParams": { + "_id":"ANY_VALUE" + }, + "allowUnauthenticatedRequests": true, + "allowExtraParams": true, + "allParamsRequired": false, + "requestType": "GET" + } + + ] +} \ No newline at end of file From 5d4d419af73909145690ef7ebd28e6a856520d56 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Mon, 23 Oct 2023 16:21:35 +0500 Subject: [PATCH 18/22] Remove extra resources/ folder --- plugins/resources/README.md | 1 - .../hapi_page_url_allowed_queries.json | 34 ----------- .../hapi_sync_filter_ignored_queries.json | 60 ------------------- 3 files changed, 95 deletions(-) delete mode 100644 plugins/resources/README.md delete mode 100644 plugins/resources/hapi_page_url_allowed_queries.json delete mode 100644 plugins/resources/hapi_sync_filter_ignored_queries.json diff --git a/plugins/resources/README.md b/plugins/resources/README.md deleted file mode 100644 index bf33049a..00000000 --- a/plugins/resources/README.md +++ /dev/null @@ -1 +0,0 @@ -# FHIR Gateway Plugin diff --git a/plugins/resources/hapi_page_url_allowed_queries.json b/plugins/resources/hapi_page_url_allowed_queries.json deleted file mode 100644 index 7f0152a4..00000000 --- a/plugins/resources/hapi_page_url_allowed_queries.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "entries": [ - { - "path": "", - "queryParams": { - "_getpages": "ANY_VALUE" - }, - "allowExtraParams": true, - "allParamsRequired": false, - "requestType": "GET" - }, - { - "path": "Composition/ANY_VALUE", - "queryParams": { - "identifier":"ANY_VALUE" - }, - "allowUnauthenticatedRequests": true, - "allowExtraParams": true, - "allParamsRequired": false, - "requestType": "GET" - }, - { - "path": "Binary/ANY_VALUE", - "queryParams": { - "_id":"ANY_VALUE" - }, - "allowUnauthenticatedRequests": true, - "allowExtraParams": true, - "allParamsRequired": false, - "requestType": "GET" - } - - ] -} \ No newline at end of file diff --git a/plugins/resources/hapi_sync_filter_ignored_queries.json b/plugins/resources/hapi_sync_filter_ignored_queries.json deleted file mode 100644 index b71ffedc..00000000 --- a/plugins/resources/hapi_sync_filter_ignored_queries.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "entries": [ - { - "path": "Questionnaire", - "methodType": "GET", - "queryParams": { - "_id": "ANY_VALUE" - } - }, - { - "path": "StructureMap", - "methodType": "GET", - "queryParams": { - "_id": "ANY_VALUE" - } - }, - { - "path": "List", - "methodType": "GET", - "queryParams": { - "_id": "ANY_VALUE" - } - }, - { - "path": "PlanDefinition", - "methodType": "GET", - "queryParams": { - "_id": "ANY_VALUE" - } - }, - { - "path": "Library", - "methodType": "GET", - "queryParams": { - "_id": "ANY_VALUE" - } - }, - { - "path": "Measure", - "methodType": "GET", - "queryParams": { - "_id": "ANY_VALUE" - } - }, - { - "path": "LocationHierarchy/", - "methodType": "GET", - "queryParams": { - "identifier": "ANY_VALUE" - } - }, - { - "path": "PractitionerDetails", - "methodType": "GET", - "queryParams": { - "keycloak-uuid": "ANY_VALUE" - } - } - ] -} From 7e2e5a13296d422bb23d4b93ba1d3225add55e53 Mon Sep 17 00:00:00 2001 From: Reham Muzzamil Date: Mon, 23 Oct 2023 17:51:51 +0500 Subject: [PATCH 19/22] Rename artifact id to gateway --- exec/pom.xml | 2 +- plugins/pom.xml | 2 +- pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exec/pom.xml b/exec/pom.xml index 3e1f9469..31ecbc4e 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.smartregister - opensrp-fhir-proxy-plugin + gateway 1.0.0 diff --git a/plugins/pom.xml b/plugins/pom.xml index b46a2cfa..93f54386 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.smartregister - opensrp-fhir-proxy-plugin + gateway 1.0.0 diff --git a/pom.xml b/pom.xml index 16bddc64..95de02cb 100755 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.smartregister - opensrp-fhir-proxy-plugin + gateway 1.0.0 pom From e34b5dc20dea64a3c1c0588775cf92e30d6f88b5 Mon Sep 17 00:00:00 2001 From: Martin Ndegwa Date: Mon, 23 Oct 2023 16:28:07 +0300 Subject: [PATCH 20/22] Update the artifact versioning --- exec/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exec/pom.xml b/exec/pom.xml index 31ecbc4e..7d08c9e5 100755 --- a/exec/pom.xml +++ b/exec/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.smartregister - gateway + opensrp-gateway-plugin 1.0.0 From a19b3d0862e9d56db7a37fa2f775fd434ecb4749 Mon Sep 17 00:00:00 2001 From: Martin Ndegwa Date: Mon, 23 Oct 2023 16:28:57 +0300 Subject: [PATCH 21/22] Update pom.xml --- plugins/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/pom.xml b/plugins/pom.xml index 93f54386..b81bcba8 100644 --- a/plugins/pom.xml +++ b/plugins/pom.xml @@ -20,7 +20,7 @@ 4.0.0 org.smartregister - gateway + opensrp-gateway-plugin 1.0.0 From 99cdd57b464bae248ab3da2f4dde10e8f6f081d2 Mon Sep 17 00:00:00 2001 From: Martin Ndegwa Date: Mon, 23 Oct 2023 16:29:18 +0300 Subject: [PATCH 22/22] Update pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 95de02cb..018acd4d 100755 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ org.smartregister - gateway + opensrp-gateway-plugin 1.0.0 pom