diff --git a/.gitignore b/.gitignore index 8a2feaee..c8f3188c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ __pycache__/ # MacOS .DS_Store + +out/ diff --git a/plugins/src/main/java/com/google/fhir/proxy/plugin/DataAccessChecker.java b/plugins/src/main/java/com/google/fhir/proxy/plugin/DataAccessChecker.java index a281f8c7..861fb8d8 100644 --- a/plugins/src/main/java/com/google/fhir/proxy/plugin/DataAccessChecker.java +++ b/plugins/src/main/java/com/google/fhir/proxy/plugin/DataAccessChecker.java @@ -57,7 +57,7 @@ private DataAccessChecker(String applicationId, List careTeamIds, List locationIds = new ArrayList<>(); + locationIds.add("msf"); + List organisationIds = new ArrayList<>(); + organisationIds.add("P0001"); + List careTeamIds = new ArrayList<>(); + return new OpenSRPSyncAccessDecision(true, locationIds, careTeamIds, organisationIds); + } + + @Named(value = "OPENSRP_TEST_ACCESS_CHECKER") + public static class Factory implements AccessCheckerFactory { + + @VisibleForTesting static final String PATIENT_LIST_CLAIM = "patient_list"; + + @Override + public AccessChecker create( + DecodedJWT jwt, + HttpFhirClient httpFhirClient, + FhirContext fhirContext, + PatientFinder patientFinder) { + return new OpenSRPAccessTestChecker(); + } + } +} diff --git a/plugins/src/main/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java b/plugins/src/main/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java index 0bf3292b..f73f90b2 100644 --- a/plugins/src/main/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java +++ b/plugins/src/main/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecision.java @@ -1,59 +1,162 @@ +/* + * Copyright 2021-2022 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 com.google.fhir.proxy.plugin; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import com.google.fhir.proxy.interfaces.AccessDecision; +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; import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; public class OpenSRPSyncAccessDecision implements AccessDecision { - private AccessDecision accessDecision; + public static final String CARE_TEAM_TAG_URL = "http://smartregister.org/fhir/care-team-tag"; - public OpenSRPSyncAccessDecision(AccessDecision accessDecision) { - this.accessDecision = accessDecision; + public static final String LOCATION_TAG_URL = "http://smartregister.org/fhir/location-id"; + + public static final String ORGANISATION_TAG_URL = "http://smartregister.org/organisation-tag"; + + public static final String SEARCH_PARAM_TAG = "_tag"; + + private boolean accessGranted; + + private List careTeamIds; + + private List locationIds; + + private List organizationIds; + + public OpenSRPSyncAccessDecision(boolean accessGranted, List locationIds, List careTeamIds, + List organizationIds) { + this.accessGranted = accessGranted; + this.careTeamIds = careTeamIds; + this.locationIds = locationIds; + this.organizationIds = organizationIds; } @Override public boolean canAccess() { - return accessDecision.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)) { - addSyncTags(servletRequestDetails, getSyncTags()); + // 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"); + } + addSyncTags(servletRequestDetails, getSyncTags(locationIds, careTeamIds, organizationIds)); } } private void addSyncTags(ServletRequestDetails servletRequestDetails, Pair> syncTags) { - String syncTagsString = getSyncTags().getKey(); - if (servletRequestDetails.getParameters().size() == 0) { - syncTagsString = "?" + syncTagsString; - } - servletRequestDetails.setCompleteUrl(servletRequestDetails.getCompleteUrl() + syncTagsString); - servletRequestDetails.setRequestPath(servletRequestDetails.getRequestPath() + syncTagsString); + List params = new ArrayList<>(); + + for (Map.Entry entry : syncTags.getValue().entrySet()) { + String tagName = entry.getKey(); + for (String tagValue : entry.getValue()) { + StringBuilder sb = new StringBuilder(tagName.length() + tagValue.length() + 2); + sb.append(tagName); + sb.append("|"); + sb.append(tagValue); + params.add(sb.toString()); + } + } - for (Map.Entry entry: syncTags.getValue().entrySet()) { - servletRequestDetails.addParameter(entry.getKey(), entry.getValue()); + String[] prevTagFilters = servletRequestDetails.getParameters().get(SEARCH_PARAM_TAG); + if (prevTagFilters != null && prevTagFilters.length > 1) { + Collections.addAll(params, prevTagFilters); } + + servletRequestDetails.addParameter(SEARCH_PARAM_TAG, params.toArray(new String[0])); } @Override public String postProcess(HttpResponse response) throws IOException { - return accessDecision.postProcess(response); + return null; } - private Pair> getSyncTags() { + private Pair> getSyncTags(List locationIds, List careTeamIds, + List organizationIds) { + StringBuilder sb = new StringBuilder(); + Map map = new HashMap<>(); + + sb.append(SEARCH_PARAM_TAG); + sb.append("="); + addTags(LOCATION_TAG_URL, locationIds, map, sb); + addTags(ORGANISATION_TAG_URL, organizationIds, map, sb); + addTags(CARE_TEAM_TAG_URL, careTeamIds, map, sb); + return new ImmutablePair<>(sb.toString(), map); + } + + private void addTags(String tagUrl, List values, Map map, StringBuilder sb) { + int len = values.size(); + if (len > 0) { + if (sb.length() != (SEARCH_PARAM_TAG + "=").length()) { + sb.append(","); + } + + map.put(tagUrl, values.toArray(new String[0])); + + int i = 0; + for (String tagValue : values) { + sb.append(tagUrl); + sb.append(":"); + sb.append(tagValue); + + if (i != len - 1) { + sb.append(","); + } + } + } } private boolean isSyncUrl(ServletRequestDetails servletRequestDetails) { - return (servletRequestDetails.getRequestType() == RequestTypeEnum.GET && servletRequestDetails.getRestOperationType() - .isTypeLevel()); + 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/plugins/src/test/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java b/plugins/src/test/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java new file mode 100644 index 00000000..ab7c9472 --- /dev/null +++ b/plugins/src/test/java/com/google/fhir/proxy/plugin/OpenSRPSyncAccessDecisionTest.java @@ -0,0 +1,176 @@ +/* + * Copyright 2021-2022 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 com.google.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 org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@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(OpenSRPSyncAccessDecision.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(OpenSRPSyncAccessDecision.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(OpenSRPSyncAccessDecision.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(OpenSRPSyncAccessDecision.LOCATION_TAG_URL + "|" + locationId)); + } + + for (String param : requestDetails.getParameters().get("_tag")) { + Assert.assertFalse(param.contains(OpenSRPSyncAccessDecision.CARE_TEAM_TAG_URL)); + Assert.assertFalse(param.contains(OpenSRPSyncAccessDecision.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(OpenSRPSyncAccessDecision.CARE_TEAM_TAG_URL + "|" + locationId)); + } + + for (String param : requestDetails.getParameters().get("_tag")) { + Assert.assertFalse(param.contains(OpenSRPSyncAccessDecision.LOCATION_TAG_URL)); + Assert.assertFalse(param.contains(OpenSRPSyncAccessDecision.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(OpenSRPSyncAccessDecision.ORGANISATION_TAG_URL + "|" + locationId)); + } + + for (String param : requestDetails.getParameters().get("_tag")) { + Assert.assertFalse(param.contains(OpenSRPSyncAccessDecision.LOCATION_TAG_URL)); + Assert.assertFalse(param.contains(OpenSRPSyncAccessDecision.CARE_TEAM_TAG_URL)); + } + } + + private OpenSRPSyncAccessDecision createOpenSRPSyncAccessDecisionTestInstance() { + return new OpenSRPSyncAccessDecision(true, locationIds, careTeamIds, organisationIds); + } + +}