diff --git a/fhir-config/src/main/java/com/ibm/fhir/config/ResourcesConfigAdapter.java b/fhir-config/src/main/java/com/ibm/fhir/config/ResourcesConfigAdapter.java index 56ba11213b1..129007621f7 100644 --- a/fhir-config/src/main/java/com/ibm/fhir/config/ResourcesConfigAdapter.java +++ b/fhir-config/src/main/java/com/ibm/fhir/config/ResourcesConfigAdapter.java @@ -63,6 +63,22 @@ public ResourcesConfigAdapter(PropertyGroup resourcesConfig) throws Exception { } } + /** + * @return whether the server is configured to prevent searches for one or more resource types + */ + public boolean isSearchRestricted() { + Set searchableResourceTypes = typesByInteraction.get(Interaction.SEARCH); + return searchableResourceTypes == null || searchableResourceTypes.size() < ALL_CONCRETE_TYPES.size(); + } + + /** + * @return whether the server is configured to prevent history interactions for one or more resource types + */ + public boolean isHistoryRestricted() { + Set resourceTypesSupportingHistory = typesByInteraction.get(Interaction.HISTORY); + return resourceTypesSupportingHistory == null || resourceTypesSupportingHistory.size() < ALL_CONCRETE_TYPES.size(); + } + /** * @return an immutable, non-null set of concrete supported resource types * @throws Exception diff --git a/fhir-persistence/src/main/java/com/ibm/fhir/persistence/util/FHIRPersistenceUtil.java b/fhir-persistence/src/main/java/com/ibm/fhir/persistence/util/FHIRPersistenceUtil.java index 16babd5eb4a..785171f171b 100644 --- a/fhir-persistence/src/main/java/com/ibm/fhir/persistence/util/FHIRPersistenceUtil.java +++ b/fhir-persistence/src/main/java/com/ibm/fhir/persistence/util/FHIRPersistenceUtil.java @@ -15,6 +15,8 @@ import java.util.Set; import java.util.logging.Logger; +import org.owasp.encoder.Encode; + import com.ibm.fhir.config.FHIRConfigHelper; import com.ibm.fhir.config.FHIRConfiguration; import com.ibm.fhir.config.FHIRRequestContext; @@ -39,8 +41,6 @@ import com.ibm.fhir.persistence.context.impl.FHIRSystemHistoryContextImpl; import com.ibm.fhir.persistence.exception.FHIRPersistenceException; -import org.owasp.encoder.Encode; - public class FHIRPersistenceUtil { private static final Logger log = Logger.getLogger(FHIRPersistenceUtil.class.getName()); @@ -104,6 +104,10 @@ public static FHIRSystemHistoryContext parseSystemHistoryParameters(Map typesSupportingHistory = config.getSupportedResourceTypes(Interaction.HISTORY); + for (String name : queryParameters.keySet()) { List values = queryParameters.get(name); String first = values.get(0); @@ -116,14 +120,6 @@ public static FHIRSystemHistoryContext parseSystemHistoryParameters(Map typesSupportingHistory = config.getSupportedResourceTypes(Interaction.HISTORY); - for (String v: values) { List resourceTypes = Arrays.asList(v.split("\\s*,\\s*")); for (String resourceType: resourceTypes) { @@ -187,6 +183,12 @@ public static FHIRSystemHistoryContext parseSystemHistoryParameters(Map createDeletedResourceResultMarker(String public static com.ibm.fhir.model.type.Instant getUpdateTime() { return com.ibm.fhir.model.type.Instant.now(ZoneOffset.UTC); } - + /** * Creates and returns a copy of the passed resource with the {@code Resource.id} * {@code Resource.meta.versionId}, and {@code Resource.meta.lastUpdated} elements replaced. diff --git a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchHelper.java b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchHelper.java index 2a180849cdf..eb616cac8fa 100644 --- a/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchHelper.java +++ b/fhir-search/src/main/java/com/ibm/fhir/search/util/SearchHelper.java @@ -27,13 +27,15 @@ import java.util.logging.Logger; import java.util.stream.Collectors; +import org.owasp.encoder.Encode; + import com.ibm.fhir.config.FHIRConfigHelper; import com.ibm.fhir.config.FHIRConfiguration; import com.ibm.fhir.config.FHIRRequestContext; import com.ibm.fhir.config.Interaction; import com.ibm.fhir.config.PropertyGroup; -import com.ibm.fhir.config.ResourcesConfigAdapter; import com.ibm.fhir.config.PropertyGroup.PropertyEntry; +import com.ibm.fhir.config.ResourcesConfigAdapter; import com.ibm.fhir.core.FHIRConstants; import com.ibm.fhir.model.resource.CodeSystem; import com.ibm.fhir.model.resource.CodeSystem.Concept; @@ -83,8 +85,6 @@ import com.ibm.fhir.term.util.CodeSystemSupport; import com.ibm.fhir.term.util.ValueSetSupport; -import org.owasp.encoder.Encode; - /** * A helper class with methods for working with HL7 FHIR search. */ @@ -404,17 +404,21 @@ public FHIRSearchContext parseQueryParameters(Class resourceType, throw SearchExceptionUtil.buildNewInvalidSearchException("system search not supported with _include or _revinclude."); } + // _type parameter is only supported for whole system searches + boolean isSystemSearch = Resource.class.equals(resourceType); + if (queryParameters.containsKey(SearchConstants.RESOURCE_TYPE) && !isSystemSearch) { + manageException("_type parameter is only supported for whole-system search", IssueType.NOT_SUPPORTED, context, false); + } + // Process the _type parameter(s) - if (queryParameters.containsKey(SearchConstants.RESOURCE_TYPE)) { - if (!Resource.class.equals(resourceType)) { - manageException("_type search parameter is only supported with system search", IssueType.NOT_SUPPORTED, context, false); - } else { + if (isSystemSearch) { + PropertyGroup pg = FHIRConfigHelper.getPropertyGroup(FHIRConfiguration.PROPERTY_RESOURCES); + ResourcesConfigAdapter config = new ResourcesConfigAdapter(pg); + Set searchableTypes = config.getSupportedResourceTypes(Interaction.SEARCH); + + if (queryParameters.containsKey(SearchConstants.RESOURCE_TYPE)) { List values = queryParameters.get(SearchConstants.RESOURCE_TYPE); - PropertyGroup pg = FHIRConfigHelper.getPropertyGroup(FHIRConfiguration.PROPERTY_RESOURCES); - ResourcesConfigAdapter config = new ResourcesConfigAdapter(pg); - Set searchableTypes = config.getSupportedResourceTypes(Interaction.SEARCH); - for (String v: values) { List tmpResourceTypes = Arrays.asList(v.split("\\s*,\\s*")); for (String tmpResourceType: tmpResourceTypes) { @@ -430,6 +434,12 @@ public FHIRSearchContext parseQueryParameters(Class resourceType, } } } + + // if no _type parameter was passed but the search interaction is only supported for some subset of types + // then we need to set the supported resource types in the context + if (resourceTypes.isEmpty() && config.isSearchRestricted()) { + resourceTypes.addAll(config.getSupportedResourceTypes(Interaction.SEARCH)); + } } queryParameters.remove(SearchConstants.RESOURCE_TYPE); diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchAllTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchAllTest.java index a95b375a4b9..847c7381883 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchAllTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SearchAllTest.java @@ -878,6 +878,50 @@ public void testSearchAll_UrlReflexsivityUsingLastUpdated() throws Exception { } } + @Test(groups = { "server-search-all" }, dependsOnMethods = { "testCreatePatient" }) + public void testSearchAllUsingId_tenant1() throws Exception { + // tenant1 is configured to support search on only a subset of resource types + FHIRRequestHeader altTenantHeader = new FHIRRequestHeader("X-FHIR-TENANT-ID", "tenant1"); + FHIRRequestHeader altDatastoreHeader = new FHIRRequestHeader("X-FHIR-DSID", "profile"); + + FHIRParameters parameters = new FHIRParameters(); + parameters.searchParam("_id", patientId); + FHIRResponse response = client.searchAll(parameters, false, altTenantHeader, altDatastoreHeader); + assertResponse(response.getResponse(), Response.Status.OK.getStatusCode()); + Bundle bundle = response.getResource(Bundle.class); + assertNotNull(bundle); + + boolean validSelf = false; + for (Link link : bundle.getLink()) { + String type = link.getRelation().getValue(); + String uri = link.getUrl().getValue(); + if ("self".equals(type)) { + assertTrue(uri.contains("_type"), "self link should contain the implicitly add _type parameter"); + validSelf = true; + } + } + + assertTrue(validSelf, "missing self link"); + } + + @Test + public void testSystemHistoryInvalidType_tenant1() throws Exception { + FHIRRequestHeader altTenantHeader = new FHIRRequestHeader("X-FHIR-TENANT-ID", "tenant1"); + FHIRRequestHeader altDatastoreHeader = new FHIRRequestHeader("X-FHIR-DSID", "profile"); + + + FHIRParameters parameters = new FHIRParameters(); + // for tenant1, Patient is configured for search but CompartmentDefinition is not + parameters.searchParam("_type", "Patient,CompartmentDefinition"); + FHIRResponse response = client.searchAll(parameters, false, altTenantHeader, altDatastoreHeader); + assertResponse(response.getResponse(), Response.Status.BAD_REQUEST.getStatusCode()); + + OperationOutcome oo = response.getResource(OperationOutcome.class); + assertNotNull(oo); + assertEquals(oo.getIssue().get(0).getDetails().getText().getValue(), + "Search interaction is not supported for _type parameter value: CompartmentDefinition"); + } + /* * Queries based on the URI the endpoint with the query parameter and value. */ diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SystemHistoryTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SystemHistoryTest.java index 2a16ee085fa..30275db3b36 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SystemHistoryTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/SystemHistoryTest.java @@ -6,6 +6,7 @@ package com.ibm.fhir.server.test; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; import static org.testng.AssertJUnit.assertNotNull; @@ -26,7 +27,9 @@ import com.ibm.fhir.client.FHIRResponse; import com.ibm.fhir.core.FHIRMediaType; import com.ibm.fhir.model.resource.Bundle; +import com.ibm.fhir.model.resource.Bundle.Link; import com.ibm.fhir.model.resource.Observation; +import com.ibm.fhir.model.resource.OperationOutcome; import com.ibm.fhir.model.resource.Patient; import com.ibm.fhir.model.resource.Resource; import com.ibm.fhir.model.test.TestUtil; @@ -38,7 +41,7 @@ * [base]/_history?_count=10 * [base]/_history?_count=10&_since=2021-02-21T03:00:53.878052Z" * [base]/_history?_count=10&_before=2021-02-21T03:00:53.878052Z" - * + * * Error case. Don't mix paging styles * [base]/_history?_count=10&_since=2021-02-21T03:00:53.878052Z&_changeIdMarker=4" */ @@ -47,7 +50,7 @@ public class SystemHistoryTest extends FHIRServerTestBase { // Create some resources, update, delete and undelete private boolean deleteSupported = false; - + // the id of the test patient we create private String patientId; @@ -130,7 +133,7 @@ public void testSystemHistoryWithTypePatient() throws Exception { // _since filter, we'll probably get back data which has been created by // other tests, so the only assertion we can make is we get back at least // the number of change records we expect. - + // Follow the next links until we get String requestPath = "Patient/_history"; boolean found = false; @@ -143,7 +146,7 @@ public void testSystemHistoryWithTypePatient() throws Exception { assertResponse(historyResponse, Response.Status.OK.getStatusCode()); Bundle bundle = historyResponse.readEntity(Bundle.class); - + // Check the bundle to see if we found the patient for (Bundle.Entry be: bundle.getEntry()) { // simple way to see if our patient has appeared @@ -152,7 +155,7 @@ public void testSystemHistoryWithTypePatient() throws Exception { found = true; } } - + // See if there's more work to do count = bundle.getEntry().size(); if (!found && count > 0) { @@ -161,7 +164,7 @@ public void testSystemHistoryWithTypePatient() throws Exception { assertNotNull(requestPath); } } while (count > 0 && !found); - + assertTrue(found, "Patient id in history"); } @@ -181,7 +184,7 @@ public void testSystemHistoryWithTypeObservation() throws Exception { assertNotNull(bundle.getEntry()); assertTrue(bundle.getEntry().size() >= 1); } - + @Test(dependsOnMethods = {"populateResourcesForHistory"}) public void testSystemHistoryWithMultipleTypes() throws Exception { if (!deleteSupported) { @@ -200,7 +203,7 @@ public void testSystemHistoryWithMultipleTypes() throws Exception { assertNotNull(bundle); assertNotNull(bundle.getEntry()); assertTrue(bundle.getEntry().size() >= 5); - + // Check that the next link was composed correctly: String nextLink = getNextLink(bundle); assertNotNull(nextLink); @@ -236,7 +239,7 @@ public void testSystemHistoryWithBadSort() throws Exception { .queryParam("_sort", "bogus") .request().get(Response.class); assertResponse(historyResponse, Response.Status.BAD_REQUEST.getStatusCode()); - } + } @Test(dependsOnMethods = {"populateResourcesForHistory"}) public void testSystemHistoryWithTypePatientAndOrderNone() throws Exception { @@ -251,8 +254,8 @@ public void testSystemHistoryWithTypePatientAndOrderNone() throws Exception { // _since filter, we'll probably get back data which has been created by // other tests, so the only assertion we can make is we get back at least // the number of change records we expect. - - // Follow the next links until we + + // Follow the next links until we String requestPath = "Patient/_history"; boolean found = false; int count; @@ -272,7 +275,7 @@ public void testSystemHistoryWithTypePatientAndOrderNone() throws Exception { assertResponse(historyResponse, Response.Status.OK.getStatusCode()); Bundle bundle = historyResponse.readEntity(Bundle.class); - + // Check the bundle to see if we found the patient for (Bundle.Entry be: bundle.getEntry()) { // simple way to see if our patient has appeared @@ -280,7 +283,7 @@ public void testSystemHistoryWithTypePatientAndOrderNone() throws Exception { if (fullUrl.contains("Patient/" + patientId)) { found = true; } - + // Check that the resourceId value for the resource is always increasing. For // whole system history interactions, this id goes in the bundle.entry.id field long resourceId = Long.parseLong(be.getId()); @@ -296,7 +299,7 @@ public void testSystemHistoryWithTypePatientAndOrderNone() throws Exception { assertNotNull(nextPath); } } while (count > 0); - + assertTrue(found, "Patient id in history"); } @@ -324,7 +327,7 @@ public void testSystemHistoryWithTypePatientAndOrderASC() throws Exception { int count; Instant prevLastUpdated = null; String nextPath = null; - String prevDigest = null; // to help see if the response changed + String prevDigest = null; // to help see if the response changed Instant since = Instant.now().minus(1, ChronoUnit.HOURS); Instant before = Instant.now().plus(1, ChronoUnit.HOURS); do { @@ -349,7 +352,7 @@ public void testSystemHistoryWithTypePatientAndOrderASC() throws Exception { assertResponse(historyResponse, Response.Status.OK.getStatusCode()); Bundle bundle = historyResponse.readEntity(Bundle.class); - + // Check the bundle to see if we found the patient MessageDigest digest = MessageDigest.getInstance("SHA-256"); for (Bundle.Entry be: bundle.getEntry()) { @@ -387,7 +390,7 @@ public void testSystemHistoryWithTypePatientAndOrderASC() throws Exception { assertNotNull(nextPath); } } while (count > 0); - + assertTrue(found, "Patient id in history"); } @@ -404,14 +407,14 @@ public void testSystemHistoryWithTypePatientAndOrderDESC() throws Exception { // _since filter, we'll probably get back data which has been created by // other tests, so the only assertion we can make is we get back at least // the number of change records we expect. - - // Follow the next links until we + + // Follow the next links until we String requestPath = "Patient/_history"; boolean found = false; int count; Instant prevLastUpdated = null; String nextPath = null; - String prevDigest = null; // to help see if the response changed + String prevDigest = null; // to help see if the response changed do { final Response historyResponse; if (nextPath == null) { @@ -426,7 +429,7 @@ public void testSystemHistoryWithTypePatientAndOrderDESC() throws Exception { assertResponse(historyResponse, Response.Status.OK.getStatusCode()); Bundle bundle = historyResponse.readEntity(Bundle.class); - + // Check the bundle to see if we found the patient MessageDigest digest = MessageDigest.getInstance("SHA-256"); for (Bundle.Entry be: bundle.getEntry()) { @@ -464,10 +467,55 @@ public void testSystemHistoryWithTypePatientAndOrderDESC() throws Exception { assertNotNull(nextPath); } } while (count > 0); - + assertTrue(found, "Patient id in history"); } + @Test + public void testSystemHistoryWithNoType_tenant1() throws Exception { + WebTarget target = getWebTarget(); + + // use tenant1 because that tenant is configured to support history on only a subset of resource types + Response historyResponse = target.path("_history").request() + .header("X-FHIR-TENANT-ID", "tenant1") + .header("X-FHIR-DSID", "profile") + .get(Response.class); + assertResponse(historyResponse, Response.Status.OK.getStatusCode()); + + Bundle bundle = historyResponse.readEntity(Bundle.class); + assertNotNull(bundle); + + boolean validSelf = false; + for (Link link : bundle.getLink()) { + String type = link.getRelation().getValue(); + String uri = link.getUrl().getValue(); + if ("self".equals(type)) { + assertTrue(uri.contains("_type"), "self link should contain the implicitly add _type parameter"); + validSelf = true; + } + } + + assertTrue(validSelf, "missing self link"); + } + + @Test + public void testSystemHistoryInvalidType_tenant1() throws Exception { + WebTarget target = getWebTarget(); + + // Patient is configured for the history interaction but CompartmentDefinition is not + Response historyResponse = target.path("_history").queryParam("_type", "Patient,CompartmentDefinition").request() + // use tenant1 because that tenant is configured to support history on only a subset of resource types + .header("X-FHIR-TENANT-ID", "tenant1") + .header("X-FHIR-DSID", "profile") + .get(Response.class); + assertResponse(historyResponse, Response.Status.BAD_REQUEST.getStatusCode()); + + OperationOutcome oo = historyResponse.readEntity(OperationOutcome.class); + assertNotNull(oo); + assertEquals(oo.getIssue().get(0).getDetails().getText().getValue(), + "history interaction is not supported for _type parameter value: CompartmentDefinition"); + } + /** * Update the resource * @param resourceToUpdate