diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java index 3faf40074cc..da56d09fc73 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/FHIRConfiguration.java @@ -41,6 +41,8 @@ public class FHIRConfiguration { public static final String PROPERTY_FIELD_RESOURCES_OPEN = "open"; public static final String PROPERTY_FIELD_RESOURCES_INTERACTIONS = "interactions"; public static final String PROPERTY_FIELD_RESOURCES_SEARCH_PARAMETERS = "searchParameters"; + public static final String PROPERTY_FIELD_RESOURCES_SEARCH_INCLUDES = "searchIncludes"; + public static final String PROPERTY_FIELD_RESOURCES_SEARCH_REV_INCLUDES = "searchRevIncludes"; // Auth and security properties public static final String PROPERTY_SECURITY_CORS = "fhirServer/security/cors"; diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java index e844908ba6e..44829a1ab86 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchUtil.java @@ -1485,8 +1485,8 @@ public static QueryParameter parseChainedInclusionCriteria(QueryParameter inclus if (parmNames[i].indexOf(SearchConstants.COLON_DELIMITER) != -1) { qualifiedInclusionCriteria = parmNames[i].split(SearchConstants.COLON_DELIMITER_STR); chainedInclusionCriteria = - new QueryParameter(Type.REFERENCE, qualifiedInclusionCriteria[0], null, resourceType, - inclusionCriteriaParm.getValues()); + new QueryParameter(Type.REFERENCE, qualifiedInclusionCriteria[0], null, + resourceType, inclusionCriteriaParm.getValues()); } else { chainedInclusionCriteria = new QueryParameter(Type.REFERENCE, parmNames[i], null, resourceType); @@ -1549,7 +1549,10 @@ private static void parseInclusionParameter(Class resourceType, FHIRSearchCon SearchParameter searchParm; InclusionParameter newInclusionParm; - List newInclusionParms; + List newInclusionParms = null; + + List allowedIncludes = getSearchIncludeRestrictions(resourceType.getSimpleName()); + List allowedRevIncludes = getSearchRevIncludeRestrictions(resourceType.getSimpleName()); for (String inclusionValue : inclusionValues) { @@ -1562,16 +1565,33 @@ private static void parseInclusionParameter(Class resourceType, FHIRSearchCon searchParameterName = inclusionValueParts[1]; searchParameterTargetType = inclusionValueParts.length == 3 ? inclusionValueParts[2] : null; - // For _include parameter, join resource type must match resource type being searched - if (SearchConstants.INCLUDE.equals(inclusionKeyword) && !joinResourceType.equals(resourceType.getSimpleName())) { - throw SearchExceptionUtil.buildNewInvalidSearchException( - "The join resource type must match the resource type being searched."); + if (SearchConstants.INCLUDE.equals(inclusionKeyword)) { + + // For _include parameter, join resource type must match resource type being searched + if (!joinResourceType.equals(resourceType.getSimpleName())) { + throw SearchExceptionUtil.buildNewInvalidSearchException( + "The join resource type must match the resource type being searched."); + } + + // Check allowed _include values + if (allowedIncludes != null && !allowedIncludes.contains(inclusionValue)) { + throw SearchExceptionUtil.buildNewInvalidSearchException("'" + inclusionValue + + "' is not a valid _include parameter value for resource type '" + resourceType.getSimpleName() + "'"); + } } - // For _revinclude parameter, target resource type, if specified, must match resource type being searched - if (SearchConstants.REVINCLUDE.equals(inclusionKeyword) && searchParameterTargetType != null - && !searchParameterTargetType.equals(resourceType.getSimpleName())) { - throw SearchExceptionUtil.buildNewInvalidSearchException("The search parameter target type must match the resource type being searched."); + if (SearchConstants.REVINCLUDE.equals(inclusionKeyword)) { + + // For _revinclude parameter, target resource type, if specified, must match resource type being searched + if (searchParameterTargetType != null && !searchParameterTargetType.equals(resourceType.getSimpleName())) { + throw SearchExceptionUtil.buildNewInvalidSearchException("The search parameter target type must match the resource type being searched."); + } + + // Check allowed _revinclude values + if (allowedRevIncludes != null && !allowedRevIncludes.contains(inclusionValue)) { + throw SearchExceptionUtil.buildNewInvalidSearchException("'" + inclusionValue + + "' is not a valid _revinclude parameter value for resource type '" + resourceType.getSimpleName() + "'"); + } } // Ensure that the Inclusion Parameter being parsed is a valid search parameter of type 'reference'. @@ -1603,6 +1623,7 @@ private static void parseInclusionParameter(Class resourceType, FHIRSearchCon newInclusionParms = buildIncludeParameter(resourceType, joinResourceType, entry.getValue(), entry.getKey(), searchParameterTargetType); context.getIncludeParameters().addAll(newInclusionParms); + } else { newInclusionParm = buildRevIncludeParameter(resourceType, joinResourceType, entry.getValue(), entry.getKey(), searchParameterTargetType); @@ -1612,6 +1633,96 @@ private static void parseInclusionParameter(Class resourceType, FHIRSearchCon } } + /** + * Retrieves the search include restrictions. + * + * @param resourceType + * the resource type + * @return list of allowed search _include values, or null if no restrictions + * @throws Exception + * an exception + */ + private static List getSearchIncludeRestrictions(String resourceType) throws Exception { + + // Retrieve the "resources" config property group. + PropertyGroup rsrcsGroup = FHIRConfigHelper.getPropertyGroup(FHIRConfiguration.PROPERTY_RESOURCES); + if (rsrcsGroup != null) { + List rsrcsEntries = rsrcsGroup.getProperties(); + if (rsrcsEntries != null && !rsrcsEntries.isEmpty()) { + + // Try find search includes property for matching resource type + for (PropertyEntry rsrcsEntry : rsrcsEntries) { + if (resourceType.equals(rsrcsEntry.getName())) { + PropertyGroup resourceTypeGroup = (PropertyGroup) rsrcsEntry.getValue(); + if (resourceTypeGroup != null) { + return resourceTypeGroup.getStringListProperty(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_SEARCH_INCLUDES); + } + } + } + + // Otherwise, try find search includes property for "Resource" resource type + for (PropertyEntry rsrcsEntry : rsrcsEntries) { + + // Check if matching resource type + if (SearchConstants.RESOURCE_RESOURCE.equals(rsrcsEntry.getName())) { + PropertyGroup resourceTypeGroup = (PropertyGroup) rsrcsEntry.getValue(); + if (resourceTypeGroup != null) { + return resourceTypeGroup.getStringListProperty(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_SEARCH_INCLUDES); + } + } + } + + } + } + + return null; + } + + /** + * Retrieves the search revinclude restrictions. + * + * @param resourceType + * the resource type + * @return list of allowed search _revinclude values, or null if no restrictions + * @throws Exception + * an exception + */ + private static List getSearchRevIncludeRestrictions(String resourceType) throws Exception { + + // Retrieve the "resources" config property group. + PropertyGroup rsrcsGroup = FHIRConfigHelper.getPropertyGroup(FHIRConfiguration.PROPERTY_RESOURCES); + if (rsrcsGroup != null) { + List rsrcsEntries = rsrcsGroup.getProperties(); + if (rsrcsEntries != null && !rsrcsEntries.isEmpty()) { + + // Try find search revincludes property for matching resource type + for (PropertyEntry rsrcsEntry : rsrcsEntries) { + if (resourceType.equals(rsrcsEntry.getName())) { + PropertyGroup resourceTypeGroup = (PropertyGroup) rsrcsEntry.getValue(); + if (resourceTypeGroup != null) { + return resourceTypeGroup.getStringListProperty(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_SEARCH_REV_INCLUDES); + } + } + } + + // Otherwise, try find search revincludes property for "Resource" resource type + for (PropertyEntry rsrcsEntry : rsrcsEntries) { + + // Check if matching resource type + if (SearchConstants.RESOURCE_RESOURCE.equals(rsrcsEntry.getName())) { + PropertyGroup resourceTypeGroup = (PropertyGroup) rsrcsEntry.getValue(); + if (resourceTypeGroup != null) { + return resourceTypeGroup.getStringListProperty(FHIRConfiguration.PROPERTY_FIELD_RESOURCES_SEARCH_REV_INCLUDES); + } + } + } + + } + } + + return null; + } + /** * Builds and returns a collection of InclusionParameter objects representing * occurrences of the _include search result parameter in the query string. diff --git a/fhir-search/src/test/java/com/ibm/fhir/search/parameters/SearchParameterRestrictionTest.java b/fhir-search/src/test/java/com/ibm/fhir/search/parameters/SearchParameterRestrictionTest.java index 72594eb811e..d044ae5c245 100644 --- a/fhir-search/src/test/java/com/ibm/fhir/search/parameters/SearchParameterRestrictionTest.java +++ b/fhir-search/src/test/java/com/ibm/fhir/search/parameters/SearchParameterRestrictionTest.java @@ -17,7 +17,12 @@ import com.ibm.fhir.config.FHIRConfiguration; import com.ibm.fhir.config.FHIRRequestContext; +import com.ibm.fhir.model.resource.ExplanationOfBenefit; +import com.ibm.fhir.model.resource.MedicationRequest; +import com.ibm.fhir.model.resource.Organization; import com.ibm.fhir.model.resource.Patient; +import com.ibm.fhir.model.resource.Person; +import com.ibm.fhir.model.resource.RelatedPerson; import com.ibm.fhir.search.exception.FHIRSearchException; import com.ibm.fhir.search.test.BaseSearchTest; import com.ibm.fhir.search.util.SearchUtil; @@ -27,6 +32,7 @@ */ public class SearchParameterRestrictionTest extends BaseSearchTest { + private static final String DEFAULT_TENANT_ID = "default"; private static final String TENANT_ID = "tenant7"; @Override @@ -124,5 +130,124 @@ public void testModifierDisallowed() throws Exception { SearchUtil.parseQueryParameters(Patient.class, queryParameters); } + + @Test + public void testIncludeAllowedByDefault() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(DEFAULT_TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("Person:organization")); + + SearchUtil.parseQueryParameters(Person.class, queryParameters); + } + + @Test + public void testIncludeAllowed() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("Patient:general-practitioner")); + + SearchUtil.parseQueryParameters(Patient.class, queryParameters); + } + + @Test + public void testIncludeWildcardAllowed() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("ExplanationOfBenefit:*")); + + SearchUtil.parseQueryParameters(ExplanationOfBenefit.class, queryParameters); + } + + @Test(expectedExceptions = { FHIRSearchException.class }) + public void testIncludeWildcardNotAllowed() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("Patient:*")); + + SearchUtil.parseQueryParameters(Patient.class, queryParameters); + } + + @Test + public void testIncludeAllowedByBaseResource() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("MedicationRequest:patient")); + + SearchUtil.parseQueryParameters(MedicationRequest.class, queryParameters); + } + + @Test(expectedExceptions = { FHIRSearchException.class }) + public void testIncludeDisallowed() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("Patient:organization")); + + SearchUtil.parseQueryParameters(Patient.class, queryParameters); + } + + @Test(expectedExceptions = { FHIRSearchException.class }) + public void testIncludeDisallowedByBaseResource() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_include", Collections.singletonList("Person:organization")); + + SearchUtil.parseQueryParameters(Person.class, queryParameters); + } + + @Test + public void testRevIncludeAllowedByDefault() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(DEFAULT_TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_revinclude", Collections.singletonList("Person:organization")); + + SearchUtil.parseQueryParameters(Organization.class, queryParameters); + } + + @Test + public void testRevIncludeAllowed() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + Map> queryParameters = new HashMap<>(); + queryParameters.put("_revinclude", Collections.singletonList("MedicationRequest:intended-performer")); + + SearchUtil.parseQueryParameters(Patient.class, queryParameters); + } + + @Test + public void testRevIncludeAllowedByBaseResource() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_revinclude", Collections.singletonList("Provenance:target")); + + SearchUtil.parseQueryParameters(Person.class, queryParameters); + } + + @Test(expectedExceptions = { FHIRSearchException.class }) + public void testRevIncludeDisallowed() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_revinclude", Collections.singletonList("MedicationRequest:requester")); + + SearchUtil.parseQueryParameters(Patient.class, queryParameters); + } + + @Test(expectedExceptions = { FHIRSearchException.class }) + public void testRevIncludeDisallowedByBaseResource() throws Exception { + FHIRRequestContext.set(new FHIRRequestContext(TENANT_ID)); + + Map> queryParameters = new HashMap<>(); + queryParameters.put("_revinclude", Collections.singletonList("MedicationRequest:intended-performer")); + + SearchUtil.parseQueryParameters(RelatedPerson.class, queryParameters); + } } diff --git a/fhir-search/src/test/resources/config/tenant7/fhir-server-config.json b/fhir-search/src/test/resources/config/tenant7/fhir-server-config.json index 65ce598f9e5..59e1a133b77 100644 --- a/fhir-search/src/test/resources/config/tenant7/fhir-server-config.json +++ b/fhir-search/src/test/resources/config/tenant7/fhir-server-config.json @@ -15,7 +15,8 @@ "ExplanationOfBenefit:provider", "ExplanationOfBenefit:care-team", "ExplanationOfBenefit:coverage", - "ExplanationOfBenefit:insurer" + "ExplanationOfBenefit:insurer", + "ExplanationOfBenefit:*" ], "searchRevIncludes": [], "searchParameters": { @@ -26,6 +27,14 @@ "identifier": "http://hl7.org/fhir/us/carin-bb/SearchParameter/explanationofbenefit-identifier", "service-date": "http://hl7.org/fhir/us/carin-bb/SearchParameter/explanationofbenefit-service-date" } + }, + "Patient": { + "searchIncludes": ["Patient:general-practitioner"], + "searchRevIncludes": ["MedicationRequest:intended-performer"] + }, + "Resource": { + "searchIncludes": ["MedicationRequest:patient"], + "searchRevIncludes": ["Provenance:target"] } } }