diff --git a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java index b5f5df07615..64867a7be70 100644 --- a/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java +++ b/hapi-fhir-server/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.java @@ -49,6 +49,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.*; import java.util.function.Consumer; +import java.util.stream.Collectors; /** * This interceptor can be used to automatically narrow the scope of searches in order to @@ -245,21 +246,7 @@ public class SearchNarrowingInterceptor { } else if (theAreCompartments) { - List searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName); - if (searchParams.size() > 0) { - - // Resources like Observation have several fields that add the resource to - // the compartment. In the case of Observation, it's subject, patient and performer. - // For this kind of thing, we'll prefer the one called "patient". - RuntimeSearchParam searchParam = - searchParams - .stream() - .filter(t -> t.getName().equalsIgnoreCase(compartmentName)) - .findFirst() - .orElse(searchParams.get(0)); - searchParamName = searchParam.getName(); - - } + searchParamName = selectBestSearchParameterForCompartment(theRequestDetails, theResDef, compartmentName); } lastCompartmentName = compartmentName; @@ -274,4 +261,70 @@ public class SearchNarrowingInterceptor { } } + private String selectBestSearchParameterForCompartment(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, String compartmentName) { + String searchParamName = null; + + Set queryParameters = theRequestDetails.getParameters().keySet(); + + List searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName); + if (searchParams.size() > 0) { + + // Resources like Observation have several fields that add the resource to + // the compartment. In the case of Observation, it's subject, patient and performer. + // For this kind of thing, we'll prefer the one that matches the compartment name. + Optional primarySearchParam = + searchParams + .stream() + .filter(t -> t.getName().equalsIgnoreCase(compartmentName)) + .findFirst(); + + if (primarySearchParam.isPresent()) { + String primarySearchParamName = primarySearchParam.get().getName(); + // If the primary search parameter is actually in use in the query, use it. + if (queryParameters.contains(primarySearchParamName)) { + searchParamName = primarySearchParamName; + } else { + // If the primary search parameter itself isn't in use, check to see whether any of its synonyms are. + List synonyms = findSynonyms(searchParams, primarySearchParam.get()); + Optional synonymInUse = synonyms + .stream() + .filter(t -> queryParameters.contains(t.getName())) + .findFirst(); + if (synonymInUse.isPresent()) { + // if so, use one of those + searchParamName = synonymInUse.get().getName(); + } else { + // if not, i.e., the original query is not filtering on this field at all, use the primary + searchParamName = primarySearchParamName; + } + } + } else { + // Otherwise, fall back to whatever is available + searchParamName = searchParams.get(0).getName(); + } + + } + return searchParamName; + } + + private List findSynonyms(List searchParams, RuntimeSearchParam primarySearchParam) { + // We define two search parameters in a compartment as synonyms if they refer to the same field in the model, ignoring any qualifiers + + String primaryBasePath = getBasePath(primarySearchParam); + + return searchParams + .stream() + .filter(t -> primaryBasePath.equals(getBasePath(t))) + .collect(Collectors.toList()); + } + + private String getBasePath(RuntimeSearchParam searchParam) { + int qualifierIndex = searchParam.getPath().indexOf(".where"); + if (qualifierIndex == -1) { + return searchParam.getPath(); + } else { + return searchParam.getPath().substring(0, qualifierIndex); + } + } + } diff --git a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java index 5d7682a17e4..f66de116829 100644 --- a/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java +++ b/hapi-fhir-structures-r4/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptorTest.java @@ -224,6 +224,26 @@ public class SearchNarrowingInterceptorTest { assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/456", "Patient/456")); } + @Test + public void testNarrowObservationsByPatientContext_ClientRequestedSomeOverlap_UseSynonym() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + ourClient + .search() + .forResource("Observation") + .where(Observation.SUBJECT.hasAnyOfIds("Patient/456", "Patient/777")) + .and(Observation.SUBJECT.hasAnyOfIds("Patient/456", "Patient/888")) + .execute(); + + assertEquals("Observation.search", ourLastHitMethod); + assertNull(ourLastIdParam); + assertNull(ourLastCodeParam); + assertThat(toStrings(ourLastSubjectParam), Matchers.contains("Patient/456", "Patient/456")); + assertNull(ourLastPerformerParam); + assertNull(ourLastPatientParam); + } + @Test public void testNarrowObservationsByPatientContext_ClientRequestedNoOverlap() { @@ -245,6 +265,27 @@ public class SearchNarrowingInterceptorTest { assertNull(ourLastHitMethod); } + @Test + public void testNarrowObservationsByPatientContext_ClientRequestedNoOverlap_UseSynonym() { + + ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456"); + + try { + ourClient + .search() + .forResource("Observation") + .where(Observation.SUBJECT.hasAnyOfIds("Patient/111", "Patient/777")) + .and(Observation.SUBJECT.hasAnyOfIds("Patient/111", "Patient/888")) + .execute(); + + fail("Expected a 403 error"); + } catch (ForbiddenOperationException e) { + assertEquals(Constants.STATUS_HTTP_403_FORBIDDEN, e.getStatusCode()); + } + + assertNull(ourLastHitMethod); + } + private List toStrings(BaseAndListParam> theParams) { List> valuesAsQueryTokens = theParams.getValuesAsQueryTokens();