From 0e54275e33b9c598c203132b84ed26754d78c607 Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Wed, 26 May 2021 15:05:53 -0500 Subject: [PATCH 1/3] Issue #896 - Add checking for MIME-type parameter fhirVersion Signed-off-by: Troy Biesterfeld --- docs/src/pages/Conformance.md | 4 +- .../java/com/ibm/fhir/core/FHIRMediaType.java | 17 ++- .../ibm/fhir/server/test/BasicServerTest.java | 125 +++++++++++++++++- .../server/test/PrettyServerFormatTest.java | 12 +- .../filter/rest/FHIRRestServletFilter.java | 46 ++++++- 5 files changed, 191 insertions(+), 13 deletions(-) diff --git a/docs/src/pages/Conformance.md b/docs/src/pages/Conformance.md index a12fac10db4..08ad60520fc 100644 --- a/docs/src/pages/Conformance.md +++ b/docs/src/pages/Conformance.md @@ -2,7 +2,7 @@ layout: post title: Conformance description: Notes on the Conformance of the IBM FHIR Server -date: 2021-05-19 +date: 2021-05-26 permalink: /conformance/ --- @@ -12,7 +12,7 @@ The IBM FHIR Server aims to be a conformant implementation of the HL7 FHIR speci ## Capability statement The HL7 FHIR specification defines [an interaction](https://www.hl7.org/fhir/R4/http.html#capabilities) for retrieving a machine-readable description of the server's capabilities via the `[base]/metadata` endpoint. The IBM FHIR Server implements this interaction and generates a `CapabilityStatement` resource based on the current server configuration. While the `CapabilityStatement` resource is ideal for certain uses, this markdown document provides a human-readable summary of important details, with a special focus on limitations of the current implementation and deviations from the specification. -The IBM FHIR Server supports only version 4.0.1 of the specification and ignores the optional MIME-type parameter `fhirVersion`. +The IBM FHIR Server supports only version 4.0.1 of the specification. ## FHIR HTTP API The HL7 FHIR specification is more than just a data format. It defines an [HTTP API](https://www.hl7.org/fhir/R4/http.html) for creating, reading, updating, deleting, and searching over FHIR resources. The IBM FHIR Server implements the full API for every resource defined in the specification, with the following exceptions: diff --git a/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java b/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java index cf733298f58..03cb63796cc 100644 --- a/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java +++ b/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java @@ -1,17 +1,23 @@ /* - * (C) Copyright IBM Corp. 2019 + * (C) Copyright IBM Corp. 2019, 2021 * * SPDX-License-Identifier: Apache-2.0 */ package com.ibm.fhir.core; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + import javax.ws.rs.core.MediaType; /** * This class contains definitions of some non-standard media types. */ public class FHIRMediaType extends MediaType { + public final static String SUBTYPE_FHIR_JSON = "fhir+json"; public final static String APPLICATION_FHIR_JSON = "application/" + SUBTYPE_FHIR_JSON; public final static MediaType APPLICATION_FHIR_JSON_TYPE = new MediaType("application", SUBTYPE_FHIR_JSON); @@ -29,6 +35,13 @@ public class FHIRMediaType extends MediaType { public final static MediaType APPLICATION_FHIR_NDJSON_TYPE = new MediaType("application", SUBTYPE_FHIR_NDJSON); public final static String SUBTYPE_FHIR_PARQUET = "fhir+parquet"; - public static final String APPLICATION_PARQUET = "application/" + SUBTYPE_FHIR_PARQUET; + public static final String APPLICATION_PARQUET = "application/" + SUBTYPE_FHIR_PARQUET; public final static MediaType APPLICATION_FHIR_PARQUET_TYPE = new MediaType("application", SUBTYPE_FHIR_PARQUET); + + // Supported values for the MIME-type parameter fhirVersion. + // https://www.hl7.org/fhir/http.html#version-parameter + // The value of this parameter is the publication and major version number for the specification. + public static final String FHIR_VERSION_PARAMETER = "fhirVersion"; + public static final Set SUPPORTED_FHIR_VERSIONS = + Collections.unmodifiableSet(new HashSet<>(Arrays.asList("4.0"))); } diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java index b3142c1b8ad..f62268c673f 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java @@ -125,6 +125,38 @@ public void testMetadataAPI_XML() { assertNotNull(conf.getName()); } + /** + * Verify the 'metadata' API with valid fhirVersion in Accept header. + */ + @Test(groups = { "server-basic" }) + public void testMetadataAPI_validFhirVersion() { + WebTarget target = getWebTarget(); + MediaType mediaType = new MediaType("application", "fhir+json", + Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "4.0")); + Response response = target.path("metadata").request(mediaType).get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + assertEquals(mediaType, response.getMediaType()); + + CapabilityStatement conf = response.readEntity(CapabilityStatement.class); + assertNotNull(conf); + assertNotNull(conf.getFormat()); + assertEquals(6, conf.getFormat().size()); + assertNotNull(conf.getVersion()); + assertNotNull(conf.getName()); + } + + /** + * Verify the 'metadata' API with invalid fhirVersion in Accept header. + */ + @Test(groups = { "server-basic" }) + public void testMetadataAPI_invalidFhirVersion() { + WebTarget target = getWebTarget(); + MediaType mediaType = new MediaType("application", "fhir+json", + Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "3.0")); + Response response = target.path("metadata").request(mediaType).get(); + assertResponse(response, Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + /** * Create a Patient, then make sure we can retrieve it. */ @@ -175,6 +207,50 @@ public void testCreatePatient_minimal() throws Exception { TestUtil.assertResourceEquals(patient, responsePatient); } + /** + * Create a minimal Patient with valid fhirVersion in Content-Type header, then make sure we can retrieve it. + */ + @Test(groups = { "server-basic" }) + public void testCreatePatient_minimal_validFhirVersion() throws Exception { + WebTarget target = getWebTarget(); + + // Build a new Patient and then call the 'create' API. + Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json"); + + MediaType mediaType = new MediaType("application", "fhir+json", + Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "4.0")); + Entity entity = Entity.entity(patient, mediaType); + Response response = target.path("Patient").request().post(entity, Response.class); + assertResponse(response, Response.Status.CREATED.getStatusCode()); + + // Get the patient's logical id value. + String patientId = getLocationLogicalId(response); + + // Next, call the 'read' API to retrieve the new patient and verify it. + response = target.path("Patient/" + patientId).request(mediaType).get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Patient responsePatient = response.readEntity(Patient.class); + + TestUtil.assertResourceEquals(patient, responsePatient); + } + + /** + * Attempt to create a minimal Patient with invalid fhirVersion in Content-Type header. + */ + @Test( groups = { "server-basic" }) + public void testCreatePatient_minimal_invalidFhirVersion() throws Exception { + WebTarget target = getWebTarget(); + + // Build a new Patient and then call the 'create' API. + Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json"); + + MediaType mediaType = new MediaType("application", "fhir+json", + Collections.singletonMap(FHIRMediaType.FHIR_VERSION_PARAMETER, "3.0")); + Entity entity = Entity.entity(patient, mediaType); + Response response = target.path("Patient").request().post(entity, Response.class); + assertResponse(response, Response.Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode()); + } + /** * Create a minimal Patient, then make sure we can retrieve it with varying format */ @@ -193,13 +269,60 @@ public void testCreatePatientMinimalWithFormat() throws Exception { String patientId = getLocationLogicalId(response); // Next, call the 'read' API to retrieve the new patient and verify it. - response = target.path("Patient/" + patientId).request(FHIRMediaType.APPLICATION_FHIR_JSON).header("_format", "application/fhir+json").get(); + response = target.path("Patient/" + patientId).queryParam("_format", "application/fhir+json").request(FHIRMediaType.APPLICATION_FHIR_JSON).get(); assertResponse(response, Response.Status.OK.getStatusCode()); Patient responsePatient = response.readEntity(Patient.class); TestUtil.assertResourceEquals(patient, responsePatient); } + /** + * Create a minimal Patient, then make sure we can retrieve it with varying format with valid FHIR version + */ + @Test(groups = { "server-basic" }) + public void testCreatePatientMinimalWithFormat_validFhirVersion() throws Exception { + WebTarget target = getWebTarget(); + + // Build a new Patient and then call the 'create' API. + Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json"); + + Entity entity = Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON); + Response response = target.path("Patient").request().post(entity, Response.class); + assertResponse(response, Response.Status.CREATED.getStatusCode()); + + // Get the patient's logical id value. + String patientId = getLocationLogicalId(response); + + // Next, call the 'read' API to retrieve the new patient and verify it. + response = target.path("Patient/" + patientId).queryParam("_format", "application/fhir+json;fhirVersion=4.0").request(FHIRMediaType.APPLICATION_FHIR_JSON).get(); + assertResponse(response, Response.Status.OK.getStatusCode()); + Patient responsePatient = response.readEntity(Patient.class); + + TestUtil.assertResourceEquals(patient, responsePatient); + } + + /** + * Create a minimal Patient, then attempt to retrieve it with varying format with invalid FHIR version + */ + @Test(groups = { "server-basic" }) + public void testCreatePatientMinimalWithFormat_invalidFhirVersion() throws Exception { + WebTarget target = getWebTarget(); + + // Build a new Patient and then call the 'create' API. + Patient patient = TestUtil.readLocalResource("Patient_DavidOrtiz.json"); + + Entity entity = Entity.entity(patient, FHIRMediaType.APPLICATION_FHIR_JSON); + Response response = target.path("Patient").request().post(entity, Response.class); + assertResponse(response, Response.Status.CREATED.getStatusCode()); + + // Get the patient's logical id value. + String patientId = getLocationLogicalId(response); + + // Next, call the 'read' API to attempt to retrieve the new patient + response = target.path("Patient/" + patientId).queryParam("_format", "application/fhir+json;fhirVersion=3.0").request(FHIRMediaType.APPLICATION_FHIR_JSON).get(); + assertResponse(response, Response.Status.NOT_ACCEPTABLE.getStatusCode()); + } + /** * Create a minimal Patient, then make sure we can retrieve it. */ diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/PrettyServerFormatTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/PrettyServerFormatTest.java index c3fb8cdc5e3..229bcea3923 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/PrettyServerFormatTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/PrettyServerFormatTest.java @@ -1,5 +1,5 @@ /* - * (C) Copyright IBM Corp. 2017,2019 + * (C) Copyright IBM Corp. 2017, 2021 * * SPDX-License-Identifier: Apache-2.0 */ @@ -45,18 +45,18 @@ public void testPrettyFormatting() throws Exception { // Next, call the 'read' API to retrieve the new patient and verify it. response = - target.queryParam("_pretty", "true").path("Patient/" + patientId) - .request(FHIRMediaType.APPLICATION_FHIR_JSON).header("_format", "application/fhir+json").get(); + target.queryParam("_pretty", "true").queryParam("_format", "application/fhir+json").path("Patient/" + patientId) + .request(FHIRMediaType.APPLICATION_FHIR_JSON).get(); assertResponse(response, Response.Status.OK.getStatusCode()); String prettyOutput = response.readEntity(String.class); response = - target.queryParam("_pretty", "false").path("Patient/" + patientId) - .request(FHIRMediaType.APPLICATION_FHIR_JSON).header("_format", "application/fhir+json").get(); + target.queryParam("_pretty", "false").queryParam("_format", "application/fhir+json").path("Patient/" + patientId) + .request(FHIRMediaType.APPLICATION_FHIR_JSON).get(); String notPrettyOutput = response.readEntity(String.class); - + assertNotEquals(prettyOutput, notPrettyOutput); assertFalse(notPrettyOutput.contains("\n")); assertTrue(prettyOutput.contains("\n")); diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java b/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java index 193eba869ef..98a818abe6b 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java @@ -23,6 +23,7 @@ import javax.servlet.http.HttpFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.UriBuilder; @@ -32,6 +33,7 @@ import com.ibm.fhir.config.FHIRConfiguration; import com.ibm.fhir.config.FHIRRequestContext; import com.ibm.fhir.config.PropertyGroup; +import com.ibm.fhir.core.FHIRMediaType; import com.ibm.fhir.core.HTTPHandlingPreference; import com.ibm.fhir.core.HTTPReturnPreference; import com.ibm.fhir.exception.FHIRException; @@ -115,6 +117,8 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F String encodedRequestDescription = Encode.forHtml(requestDescription.toString()); log.info("Received request: " + encodedRequestDescription); + int statusOnException = HttpServletResponse.SC_BAD_REQUEST; + try { // Checks for Valid Tenant Configuration checkValidTenantConfiguration(tenantId); @@ -138,6 +142,20 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F Map> requestHeaders = extractRequestHeaders(request); context.setHttpHeaders(requestHeaders); + // Check the FHIR version parameter + // 415 Unsupported Media Type is the appropriate response when the client posts a format that is not supported to the server. + // 406 Not Acceptable is the appropriate response when the Accept header requests a format that the server does not support. + String errorMsg = checkFhirVersionParameter(HttpHeaders.CONTENT_TYPE, requestHeaders); + if (errorMsg != null) { + statusOnException = HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; + throw new FHIRException(errorMsg); + } + errorMsg = checkFhirVersionParameter(HttpHeaders.ACCEPT, requestHeaders); + if (errorMsg != null) { + statusOnException = HttpServletResponse.SC_NOT_ACCEPTABLE; + throw new FHIRException(errorMsg); + } + // Pass the request through to the next filter in the chain. chain.doFilter(request, response); } catch (Exception e) { @@ -145,9 +163,9 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F OperationOutcome outcome = FHIRUtil.buildOperationOutcome(e, IssueType.INVALID, IssueSeverity.FATAL, false); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + response.setStatus(statusOnException); - Format format = chooseResponseFormat(request.getHeader("Accept")); + Format format = chooseResponseFormat(request.getHeader(HttpHeaders.ACCEPT)); switch (format) { case XML: response.setContentType(com.ibm.fhir.core.FHIRMediaType.APPLICATION_FHIR_XML); @@ -261,6 +279,30 @@ private HTTPReturnPreference computeReturnPref(ServletRequest request, HTTPHandl return returnPref; } + /** + * Checks if the FHIR version parameter in the specified HTTP header is valid. + * @param headerName the name of the header + * @param requestHeaders the headers + * @return the error message if FHIR version parameter in not valid, otherwise null + */ + private String checkFhirVersionParameter(String headerName, Map> requestHeaders) throws FHIRException { + for (String headerValue : requestHeaders.getOrDefault(headerName, Collections.emptyList())) { + Map parameters = MediaType.valueOf(headerValue).getParameters(); + if (parameters != null) { + for (Map.Entry parameter : parameters.entrySet()) { + if (FHIRMediaType.FHIR_VERSION_PARAMETER.equalsIgnoreCase(parameter.getKey())) { + String fhirVersion = parameter.getValue(); + if (fhirVersion != null && !FHIRMediaType.SUPPORTED_FHIR_VERSIONS.contains(fhirVersion)) { + return "Invalid '" + FHIRMediaType.FHIR_VERSION_PARAMETER + "' parameter value in '" + headerName + + "' header; the following FHIR versions are supported: " + FHIRMediaType.SUPPORTED_FHIR_VERSIONS; + } + } + } + } + } + return null; + } + private Format chooseResponseFormat(String acceptableContentTypes) { if (acceptableContentTypes.contains(com.ibm.fhir.core.FHIRMediaType.APPLICATION_FHIR_JSON) || acceptableContentTypes.contains(MediaType.APPLICATION_JSON)) { From cf516a8c24c0ceb7fce2631f72dce28d731bd6e5 Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Thu, 27 May 2021 13:27:05 -0500 Subject: [PATCH 2/3] Issue #896 - Update after review comments Signed-off-by: Troy Biesterfeld --- .../java/com/ibm/fhir/core/FHIRMediaType.java | 3 +- .../FHIRRestServletRequestException.java | 37 +++++++++ .../filter/rest/FHIRRestServletFilter.java | 67 ++++++++++------ .../rest/FHIRRestServletFilterTest.java | 79 +++++++++++++++++++ 4 files changed, 158 insertions(+), 28 deletions(-) create mode 100644 fhir-server/src/main/java/com/ibm/fhir/server/exception/FHIRRestServletRequestException.java create mode 100644 fhir-server/src/test/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilterTest.java diff --git a/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java b/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java index 03cb63796cc..a1ab070ce23 100644 --- a/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java +++ b/fhir-core/src/main/java/com/ibm/fhir/core/FHIRMediaType.java @@ -40,8 +40,7 @@ public class FHIRMediaType extends MediaType { // Supported values for the MIME-type parameter fhirVersion. // https://www.hl7.org/fhir/http.html#version-parameter - // The value of this parameter is the publication and major version number for the specification. public static final String FHIR_VERSION_PARAMETER = "fhirVersion"; public static final Set SUPPORTED_FHIR_VERSIONS = - Collections.unmodifiableSet(new HashSet<>(Arrays.asList("4.0"))); + Collections.unmodifiableSet(new HashSet<>(Arrays.asList("4.0","4.0.1"))); } diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/exception/FHIRRestServletRequestException.java b/fhir-server/src/main/java/com/ibm/fhir/server/exception/FHIRRestServletRequestException.java new file mode 100644 index 00000000000..c670ace581a --- /dev/null +++ b/fhir-server/src/main/java/com/ibm/fhir/server/exception/FHIRRestServletRequestException.java @@ -0,0 +1,37 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.ibm.fhir.server.exception; + +import javax.servlet.http.HttpServletResponse; + +import com.ibm.fhir.exception.FHIROperationException; + +public class FHIRRestServletRequestException extends FHIROperationException { + private static final long serialVersionUID = 1L; + private int httpStatusCode = HttpServletResponse.SC_BAD_REQUEST; + + public FHIRRestServletRequestException(String message) { + super(message); + } + + public FHIRRestServletRequestException(String message, Throwable cause) { + super(message, cause); + } + + public FHIRRestServletRequestException(String message, int httpStatusCode) { + this(message, httpStatusCode, null); + } + + public FHIRRestServletRequestException(String message, int httpStatusCode, Throwable t) { + super(message, t); + this.httpStatusCode = httpStatusCode; + } + + public int getHttpStatusCode() { + return httpStatusCode; + } +} diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java b/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java index 98a818abe6b..aac40240f6a 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java @@ -43,6 +43,7 @@ import com.ibm.fhir.model.type.code.IssueSeverity; import com.ibm.fhir.model.type.code.IssueType; import com.ibm.fhir.model.util.FHIRUtil; +import com.ibm.fhir.server.exception.FHIRRestServletRequestException; /** * This class is a servlet filter which is registered with the REST API's servlet. The main purpose of the class is to @@ -142,18 +143,12 @@ public void doFilter(HttpServletRequest request, HttpServletResponse response, F Map> requestHeaders = extractRequestHeaders(request); context.setHttpHeaders(requestHeaders); - // Check the FHIR version parameter - // 415 Unsupported Media Type is the appropriate response when the client posts a format that is not supported to the server. - // 406 Not Acceptable is the appropriate response when the Accept header requests a format that the server does not support. - String errorMsg = checkFhirVersionParameter(HttpHeaders.CONTENT_TYPE, requestHeaders); - if (errorMsg != null) { - statusOnException = HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE; - throw new FHIRException(errorMsg); - } - errorMsg = checkFhirVersionParameter(HttpHeaders.ACCEPT, requestHeaders); - if (errorMsg != null) { - statusOnException = HttpServletResponse.SC_NOT_ACCEPTABLE; - throw new FHIRException(errorMsg); + // Check the FHIR version parameter. + try { + checkFhirVersionParameter(requestHeaders); + } catch (FHIRRestServletRequestException e) { + statusOnException = e.getHttpStatusCode(); + throw e; } // Pass the request through to the next filter in the chain. @@ -280,27 +275,47 @@ private HTTPReturnPreference computeReturnPref(ServletRequest request, HTTPHandl } /** - * Checks if the FHIR version parameter in the specified HTTP header is valid. - * @param headerName the name of the header + * Check that FHIR version parameters in the HTTP headers are valid. * @param requestHeaders the headers - * @return the error message if FHIR version parameter in not valid, otherwise null + * @throws FHIRRestServletRequestException */ - private String checkFhirVersionParameter(String headerName, Map> requestHeaders) throws FHIRException { - for (String headerValue : requestHeaders.getOrDefault(headerName, Collections.emptyList())) { - Map parameters = MediaType.valueOf(headerValue).getParameters(); - if (parameters != null) { - for (Map.Entry parameter : parameters.entrySet()) { - if (FHIRMediaType.FHIR_VERSION_PARAMETER.equalsIgnoreCase(parameter.getKey())) { - String fhirVersion = parameter.getValue(); - if (fhirVersion != null && !FHIRMediaType.SUPPORTED_FHIR_VERSIONS.contains(fhirVersion)) { - return "Invalid '" + FHIRMediaType.FHIR_VERSION_PARAMETER + "' parameter value in '" + headerName - + "' header; the following FHIR versions are supported: " + FHIRMediaType.SUPPORTED_FHIR_VERSIONS; + void checkFhirVersionParameter(Map> requestHeaders) throws FHIRRestServletRequestException { + Map headerStatusMap = new LinkedHashMap<>(); + // 415 Unsupported Media Type is the appropriate response when the client posts a format that is not supported to the server. + headerStatusMap.put(HttpHeaders.CONTENT_TYPE, HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + // 406 Not Acceptable is the appropriate response when the Accept header requests a format that the server does not support. + headerStatusMap.put(HttpHeaders.ACCEPT, HttpServletResponse.SC_NOT_ACCEPTABLE); + + for (String headerName : headerStatusMap.keySet()) { + String fhirVersion = null; + for (String headerValue : requestHeaders.getOrDefault(headerName, Collections.emptyList())) { + if (headerValue != null) { + for (String headerValueElement : headerValue.split(",")) { + Map parameters = MediaType.valueOf(headerValueElement).getParameters(); + if (parameters != null) { + for (Map.Entry parameter : parameters.entrySet()) { + if (FHIRMediaType.FHIR_VERSION_PARAMETER.equalsIgnoreCase(parameter.getKey())) { + String curFhirVersion = parameter.getValue(); + if (curFhirVersion != null && !FHIRMediaType.SUPPORTED_FHIR_VERSIONS.contains(curFhirVersion)) { + throw new FHIRRestServletRequestException("Invalid '" + FHIRMediaType.FHIR_VERSION_PARAMETER + + "' parameter value in '" + headerName + "' header; the following FHIR versions are supported: " + + FHIRMediaType.SUPPORTED_FHIR_VERSIONS, headerStatusMap.get(headerName)); + } + // If Content-Type header, check for multiple different FHIR versions + if (headerName.equals(HttpHeaders.CONTENT_TYPE)) { + if (fhirVersion != null && !fhirVersion.equals(curFhirVersion)) { + throw new FHIRRestServletRequestException("Multiple different '" + FHIRMediaType.FHIR_VERSION_PARAMETER + + "' parameter values in '" + headerName + "' header", headerStatusMap.get(headerName)); + } + fhirVersion = curFhirVersion; + } + } + } } } } } } - return null; } private Format chooseResponseFormat(String acceptableContentTypes) { diff --git a/fhir-server/src/test/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilterTest.java b/fhir-server/src/test/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilterTest.java new file mode 100644 index 00000000000..e0b5b1d9219 --- /dev/null +++ b/fhir-server/src/test/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilterTest.java @@ -0,0 +1,79 @@ +/* + * (C) Copyright IBM Corp. 2021 + * + * SPDX-License-Identifier: Apache-2.0 + */ +package com.ibm.fhir.server.filter.rest; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.fail; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.core.HttpHeaders; + +import org.testng.annotations.Test; + +import com.ibm.fhir.server.exception.FHIRRestServletRequestException; + +public class FHIRRestServletFilterTest { + + @Test + void testCheckFhirVersionParameter_valid() throws Exception { + Map> requestHeaders = new HashMap<>(); + requestHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList("application/fhir+json;fhirVersion=4.0.1")); + requestHeaders.put(HttpHeaders.ACCEPT, Collections.singletonList("application/fhir+json;fhirVersion=4.0.1")); + + FHIRRestServletFilter servletFilter = new FHIRRestServletFilter(); + servletFilter.checkFhirVersionParameter(requestHeaders); + } + + @Test + void testCheckFhirVersionParameter_contentType_invalidFhirVersion() throws Exception { + Map> requestHeaders = new HashMap<>(); + requestHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList("application/fhir+json;fhirVersion=3.0.1")); + requestHeaders.put(HttpHeaders.ACCEPT, Collections.singletonList("application/fhir+json;fhirVersion=4.0.1")); + + FHIRRestServletFilter servletFilter = new FHIRRestServletFilter(); + try { + servletFilter.checkFhirVersionParameter(requestHeaders); + fail(); + } catch (FHIRRestServletRequestException e) { + assertEquals(e.getHttpStatusCode(), HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + } + } + + @Test + void testCheckFhirVersionParameter_contentType_multipleFhirVersions() throws Exception { + Map> requestHeaders = new HashMap<>(); + requestHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList("application/fhir+json;fhirVersion=3.0.1")); + requestHeaders.put(HttpHeaders.ACCEPT, Collections.singletonList("application/fhir+json;fhirVersion=4.0.1")); + + FHIRRestServletFilter servletFilter = new FHIRRestServletFilter(); + try { + servletFilter.checkFhirVersionParameter(requestHeaders); + fail(); + } catch (FHIRRestServletRequestException e) { + assertEquals(e.getHttpStatusCode(), HttpServletResponse.SC_UNSUPPORTED_MEDIA_TYPE); + } + } + + @Test + void testCheckFhirVersionParameter_accept_invalidFhirVersion() throws Exception { + Map> requestHeaders = new HashMap<>(); + requestHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList("application/fhir+json;fhirVersion=4.0.1")); + requestHeaders.put(HttpHeaders.ACCEPT, Collections.singletonList("application/fhir+json;fhirVersion=3.0.1")); + + FHIRRestServletFilter servletFilter = new FHIRRestServletFilter(); + try { + servletFilter.checkFhirVersionParameter(requestHeaders); + fail(); + } catch (FHIRRestServletRequestException e) { + assertEquals(e.getHttpStatusCode(), HttpServletResponse.SC_NOT_ACCEPTABLE); + } + } +} From a7e01ae47265a89e4c1c24775a2a39719788cbcf Mon Sep 17 00:00:00 2001 From: Troy Biesterfeld Date: Wed, 2 Jun 2021 10:10:06 -0500 Subject: [PATCH 3/3] Issue #896 - Update after review comments Signed-off-by: Troy Biesterfeld --- .../ibm/fhir/server/test/BasicServerTest.java | 5 ++- .../filter/rest/FHIRRestServletFilter.java | 35 ++++++++----------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java index f62268c673f..a45cd9199ba 100644 --- a/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java +++ b/fhir-server-test/src/test/java/com/ibm/fhir/server/test/BasicServerTest.java @@ -15,12 +15,13 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.Iterator; import java.util.List; -import jakarta.json.JsonObject; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.testng.annotations.Test; @@ -49,6 +50,8 @@ import com.ibm.fhir.path.evaluator.FHIRPathEvaluator.EvaluationContext; import com.ibm.fhir.path.exception.FHIRPathException; +import jakarta.json.JsonObject; + /** * Basic sniff test of the FHIR Server. */ diff --git a/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java b/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java index aac40240f6a..1692ed62ee1 100644 --- a/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java +++ b/fhir-server/src/main/java/com/ibm/fhir/server/filter/rest/FHIRRestServletFilter.java @@ -289,27 +289,22 @@ void checkFhirVersionParameter(Map> requestHeaders) throws for (String headerName : headerStatusMap.keySet()) { String fhirVersion = null; for (String headerValue : requestHeaders.getOrDefault(headerName, Collections.emptyList())) { - if (headerValue != null) { - for (String headerValueElement : headerValue.split(",")) { - Map parameters = MediaType.valueOf(headerValueElement).getParameters(); - if (parameters != null) { - for (Map.Entry parameter : parameters.entrySet()) { - if (FHIRMediaType.FHIR_VERSION_PARAMETER.equalsIgnoreCase(parameter.getKey())) { - String curFhirVersion = parameter.getValue(); - if (curFhirVersion != null && !FHIRMediaType.SUPPORTED_FHIR_VERSIONS.contains(curFhirVersion)) { - throw new FHIRRestServletRequestException("Invalid '" + FHIRMediaType.FHIR_VERSION_PARAMETER - + "' parameter value in '" + headerName + "' header; the following FHIR versions are supported: " - + FHIRMediaType.SUPPORTED_FHIR_VERSIONS, headerStatusMap.get(headerName)); - } - // If Content-Type header, check for multiple different FHIR versions - if (headerName.equals(HttpHeaders.CONTENT_TYPE)) { - if (fhirVersion != null && !fhirVersion.equals(curFhirVersion)) { - throw new FHIRRestServletRequestException("Multiple different '" + FHIRMediaType.FHIR_VERSION_PARAMETER - + "' parameter values in '" + headerName + "' header", headerStatusMap.get(headerName)); - } - fhirVersion = curFhirVersion; - } + for (String headerValueElement : headerValue.split(",")) { + for (Map.Entry parameter : MediaType.valueOf(headerValueElement).getParameters().entrySet()) { + if (FHIRMediaType.FHIR_VERSION_PARAMETER.equalsIgnoreCase(parameter.getKey())) { + String curFhirVersion = parameter.getValue(); + if (curFhirVersion != null && !FHIRMediaType.SUPPORTED_FHIR_VERSIONS.contains(curFhirVersion)) { + throw new FHIRRestServletRequestException("Invalid '" + FHIRMediaType.FHIR_VERSION_PARAMETER + + "' parameter value in '" + headerName + "' header; the following FHIR versions are supported: " + + FHIRMediaType.SUPPORTED_FHIR_VERSIONS, headerStatusMap.get(headerName)); + } + // If Content-Type header, check for multiple different FHIR versions + if (headerName.equals(HttpHeaders.CONTENT_TYPE)) { + if (fhirVersion != null && !fhirVersion.equals(curFhirVersion)) { + throw new FHIRRestServletRequestException("Multiple different '" + FHIRMediaType.FHIR_VERSION_PARAMETER + + "' parameter values in '" + headerName + "' header", headerStatusMap.get(headerName)); } + fhirVersion = curFhirVersion; } } }