Fix #1483 - Don't let RP methods handle a request with includes if the
method doesn't understand them
This commit is contained in:
parent
0b630f6851
commit
57377f5557
|
@ -19,15 +19,6 @@ package ca.uhn.fhir.rest.server.method;
|
|||
* limitations under the License.
|
||||
* #L%
|
||||
*/
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import ca.uhn.fhir.context.ConfigurationException;
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
|
@ -44,27 +35,38 @@ import ca.uhn.fhir.rest.param.ParameterUtil;
|
|||
import ca.uhn.fhir.rest.param.QualifierDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hl7.fhir.instance.model.api.IAnyResource;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.*;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isBlank;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchMethodBinding.class);
|
||||
|
||||
private static final Set<String> SPECIAL_SEARCH_PARAMS;
|
||||
private String myCompartmentName;
|
||||
private String myDescription;
|
||||
private Integer myIdParamIndex;
|
||||
private String myQueryName;
|
||||
private boolean myAllowUnknownParams;
|
||||
private final String myResourceProviderResourceName;
|
||||
|
||||
static {
|
||||
HashSet<String> specialSearchParams = new HashSet<>();
|
||||
specialSearchParams.add(IAnyResource.SP_RES_ID);
|
||||
specialSearchParams.add(IAnyResource.SP_RES_LANGUAGE);
|
||||
specialSearchParams.add(Constants.PARAM_INCLUDE);
|
||||
specialSearchParams.add(Constants.PARAM_REVINCLUDE);
|
||||
SPECIAL_SEARCH_PARAMS = Collections.unmodifiableSet(specialSearchParams);
|
||||
}
|
||||
|
||||
private final String myResourceProviderResourceName;
|
||||
private String myCompartmentName;
|
||||
private String myDescription;
|
||||
private Integer myIdParamIndex;
|
||||
private String myQueryName;
|
||||
private boolean myAllowUnknownParams;
|
||||
|
||||
public SearchMethodBinding(Class<? extends IBaseResource> theReturnResourceType, Class<? extends IBaseResource> theResourceProviderResourceType, Method theMethod, FhirContext theContext, Object theProvider) {
|
||||
super(theReturnResourceType, theMethod, theContext, theProvider);
|
||||
Search search = theMethod.getAnnotation(Search.class);
|
||||
|
@ -90,11 +92,11 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
throw new ConfigurationException(msg);
|
||||
}
|
||||
|
||||
if (theResourceProviderResourceType != null) {
|
||||
this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName();
|
||||
} else {
|
||||
this.myResourceProviderResourceName = null;
|
||||
}
|
||||
if (theResourceProviderResourceType != null) {
|
||||
this.myResourceProviderResourceName = theContext.getResourceDefinition(theResourceProviderResourceType).getName();
|
||||
} else {
|
||||
this.myResourceProviderResourceName = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -106,8 +108,8 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
return myQueryName;
|
||||
}
|
||||
|
||||
public String getResourceProviderResourceName() {
|
||||
return myResourceProviderResourceName;
|
||||
public String getResourceProviderResourceName() {
|
||||
return myResourceProviderResourceName;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
|
@ -123,28 +125,14 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
|
||||
@Override
|
||||
public ReturnTypeEnum getReturnType() {
|
||||
return ReturnTypeEnum.BUNDLE;
|
||||
return ReturnTypeEnum.BUNDLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean incomingServerRequestMatchesMethod(RequestDetails theRequest) {
|
||||
|
||||
String clientPreference = theRequest.getHeader(Constants.HEADER_PREFER);
|
||||
boolean lenientHandling = false;
|
||||
if(clientPreference != null)
|
||||
{
|
||||
String[] preferences = clientPreference.split(";");
|
||||
for( String p : preferences){
|
||||
if("handling:lenient".equalsIgnoreCase(p))
|
||||
{
|
||||
lenientHandling = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (theRequest.getId() != null && myIdParamIndex == null) {
|
||||
ourLog.trace("Method {} doesn't match because ID is not null: {}", theRequest.getId());
|
||||
ourLog.trace("Method {} doesn't match because ID is not null: {}", getMethod(), theRequest.getId());
|
||||
return false;
|
||||
}
|
||||
if (theRequest.getRequestType() == RequestTypeEnum.GET && theRequest.getOperation() != null && !Constants.PARAM_SEARCH.equals(theRequest.getOperation())) {
|
||||
|
@ -156,40 +144,39 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
return false;
|
||||
}
|
||||
if (theRequest.getRequestType() != RequestTypeEnum.GET && theRequest.getRequestType() != RequestTypeEnum.POST) {
|
||||
ourLog.trace("Method {} doesn't match because request type is {}", getMethod());
|
||||
ourLog.trace("Method {} doesn't match because request type is {}", getMethod(), theRequest.getRequestType());
|
||||
return false;
|
||||
}
|
||||
if (!StringUtils.equals(myCompartmentName, theRequest.getCompartmentName())) {
|
||||
ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", new Object[] { getMethod(), myCompartmentName, theRequest.getCompartmentName() });
|
||||
ourLog.trace("Method {} doesn't match because it is for compartment {} but request is compartment {}", getMethod(), myCompartmentName, theRequest.getCompartmentName());
|
||||
return false;
|
||||
}
|
||||
// This is used to track all the parameters so we can reject queries that
|
||||
// have additional params we don't understand
|
||||
Set<String> methodParamsTemp = new HashSet<String>();
|
||||
Set<String> methodParamsTemp = new HashSet<>();
|
||||
|
||||
Set<String> unqualifiedNames = theRequest.getUnqualifiedToQualifiedNames().keySet();
|
||||
Set<String> qualifiedParamNames = theRequest.getParameters().keySet();
|
||||
for (int i = 0; i < this.getParameters().size(); i++) {
|
||||
if (!(getParameters().get(i) instanceof BaseQueryParameter)) {
|
||||
for (IParameter nextParameter : getParameters()) {
|
||||
if (!(nextParameter instanceof BaseQueryParameter)) {
|
||||
continue;
|
||||
}
|
||||
BaseQueryParameter temp = (BaseQueryParameter) getParameters().get(i);
|
||||
String name = temp.getName();
|
||||
if (temp.isRequired()) {
|
||||
BaseQueryParameter nextQueryParameter = (BaseQueryParameter) nextParameter;
|
||||
String name = nextQueryParameter.getName();
|
||||
if (nextQueryParameter.isRequired()) {
|
||||
|
||||
if (qualifiedParamNames.contains(name)) {
|
||||
QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
|
||||
if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
|
||||
if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
|
||||
methodParamsTemp.add(name);
|
||||
}
|
||||
}
|
||||
if (unqualifiedNames.contains(name)) {
|
||||
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
|
||||
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
|
||||
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
|
||||
methodParamsTemp.addAll(qualifiedNames);
|
||||
}
|
||||
if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name))
|
||||
{
|
||||
if (!qualifiedParamNames.contains(name) && !unqualifiedNames.contains(name)) {
|
||||
ourLog.trace("Method {} doesn't match param '{}' is not present", getMethod().getName(), name);
|
||||
return false;
|
||||
}
|
||||
|
@ -197,16 +184,16 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
} else {
|
||||
if (qualifiedParamNames.contains(name)) {
|
||||
QualifierDetails qualifiers = extractQualifiersFromParameterName(name);
|
||||
if (qualifiers.passes(temp.getQualifierWhitelist(), temp.getQualifierBlacklist())) {
|
||||
if (qualifiers.passes(nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist())) {
|
||||
methodParamsTemp.add(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unqualifiedNames.contains(name)) {
|
||||
List<String> qualifiedNames = theRequest.getUnqualifiedToQualifiedNames().get(name);
|
||||
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, temp.getQualifierWhitelist(), temp.getQualifierBlacklist());
|
||||
qualifiedNames = processWhitelistAndBlacklist(qualifiedNames, nextQueryParameter.getQualifierWhitelist(), nextQueryParameter.getQualifierBlacklist());
|
||||
methodParamsTemp.addAll(qualifiedNames);
|
||||
}
|
||||
if (!qualifiedParamNames.contains(name)) {
|
||||
if (!qualifiedParamNames.contains(name)) {
|
||||
methodParamsTemp.add(name);
|
||||
}
|
||||
}
|
||||
|
@ -237,8 +224,6 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
}
|
||||
}
|
||||
Set<String> keySet = theRequest.getParameters().keySet();
|
||||
if(lenientHandling == true)
|
||||
return true;
|
||||
|
||||
if (myAllowUnknownParams == false) {
|
||||
for (String next : keySet) {
|
||||
|
@ -272,7 +257,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
if (theQualifierWhitelist == null && theQualifierBlacklist == null) {
|
||||
return theQualifiedNames;
|
||||
}
|
||||
ArrayList<String> retVal = new ArrayList<String>(theQualifiedNames.size());
|
||||
ArrayList<String> retVal = new ArrayList<>(theQualifiedNames.size());
|
||||
for (String next : theQualifiedNames) {
|
||||
QualifierDetails qualifiers = extractQualifiersFromParameterName(next);
|
||||
if (!qualifiers.passes(theQualifierWhitelist, theQualifierBlacklist)) {
|
||||
|
@ -287,6 +272,7 @@ public class SearchMethodBinding extends BaseResourceReturningMethodBinding {
|
|||
public String toString() {
|
||||
return getMethod().toString();
|
||||
}
|
||||
|
||||
public static QualifierDetails extractQualifiersFromParameterName(String theParamName) {
|
||||
QualifierDetails retVal = new QualifierDetails();
|
||||
if (theParamName == null || theParamName.length() == 0) {
|
||||
|
|
|
@ -35,15 +35,15 @@ public class SearchMethodBindingTest {
|
|||
}
|
||||
|
||||
@Test // fails
|
||||
public void methodShouldNotMatchWhenExtraUnderscoreQueryParameter() throws NoSuchMethodException {
|
||||
public void methodShouldNotMatchWhenUnderscoreQueryParameter() throws NoSuchMethodException {
|
||||
Assert.assertThat(getBinding("param", String.class).incomingServerRequestMatchesMethod(
|
||||
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_extra", new String[]{"test"}))),
|
||||
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
|
||||
Matchers.is(false));
|
||||
Assert.assertThat(getBinding("paramAndTest", String.class, String.class).incomingServerRequestMatchesMethod(
|
||||
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_extra", new String[]{"test"}))),
|
||||
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
|
||||
Matchers.is(false));
|
||||
Assert.assertThat(getBinding("paramAndUnderscoreTest", String.class, String.class).incomingServerRequestMatchesMethod(
|
||||
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_extra", new String[]{"test"}))),
|
||||
mockSearchRequest(ImmutableMap.of("param", new String[]{"value"}, "_include", new String[]{"test"}))),
|
||||
Matchers.is(false));
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,190 @@
|
|||
package ca.uhn.fhir.rest.server;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.api.BundleInclusionRule;
|
||||
import ca.uhn.fhir.model.api.Include;
|
||||
import ca.uhn.fhir.rest.annotation.IncludeParam;
|
||||
import ca.uhn.fhir.rest.annotation.OptionalParam;
|
||||
import ca.uhn.fhir.rest.annotation.Search;
|
||||
import ca.uhn.fhir.rest.api.EncodingEnum;
|
||||
import ca.uhn.fhir.rest.client.api.IGenericClient;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.test.utilities.JettyUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.r4.model.Bundle;
|
||||
import org.hl7.fhir.r4.model.Patient;
|
||||
import org.hl7.fhir.r4.model.StringType;
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
public class ServerMethodSelectionR4Test {
|
||||
|
||||
|
||||
private FhirContext myCtx = FhirContext.forR4();
|
||||
private Server myServer;
|
||||
private IGenericClient myClient;
|
||||
|
||||
@After
|
||||
public void after() throws Exception {
|
||||
JettyUtil.closeServer(myServer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Server method with no _include
|
||||
* Client request with _include
|
||||
* <p>
|
||||
* See #1421
|
||||
*/
|
||||
@Test
|
||||
public void testRejectIncludeIfNotProvided() throws Exception {
|
||||
|
||||
class MyProvider extends MyBaseProvider {
|
||||
@Search
|
||||
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName) {
|
||||
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
|
||||
}
|
||||
}
|
||||
MyProvider provider = new MyProvider();
|
||||
|
||||
startServer(provider);
|
||||
|
||||
try {
|
||||
myClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.NAME.matches().value("foo"))
|
||||
.include(Patient.INCLUDE_ORGANIZATION)
|
||||
.execute();
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertThat(e.getMessage(), containsString("this server does not know how to handle GET operation[Patient] with parameters [[_include, name]]"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server method with no _include
|
||||
* Client request with _include
|
||||
* <p>
|
||||
* See #1421
|
||||
*/
|
||||
@Test
|
||||
public void testAllowIncludeIfProvided() throws Exception {
|
||||
|
||||
class MyProvider extends MyBaseProvider {
|
||||
@Search
|
||||
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName, @IncludeParam Set<Include> theIncludes) {
|
||||
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
|
||||
}
|
||||
}
|
||||
MyProvider provider = new MyProvider();
|
||||
|
||||
startServer(provider);
|
||||
|
||||
Bundle results = myClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.NAME.matches().value("foo"))
|
||||
.include(Patient.INCLUDE_ORGANIZATION)
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
assertEquals(1, results.getEntry().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Server method with no _revinclude
|
||||
* Client request with _revinclude
|
||||
* <p>
|
||||
* See #1421
|
||||
*/
|
||||
@Test
|
||||
public void testRejectRevIncludeIfNotProvided() throws Exception {
|
||||
|
||||
class MyProvider extends MyBaseProvider {
|
||||
@Search
|
||||
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName) {
|
||||
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
|
||||
}
|
||||
}
|
||||
MyProvider provider = new MyProvider();
|
||||
|
||||
startServer(provider);
|
||||
|
||||
try {
|
||||
myClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.NAME.matches().value("foo"))
|
||||
.revInclude(Patient.INCLUDE_ORGANIZATION)
|
||||
.execute();
|
||||
fail();
|
||||
} catch (InvalidRequestException e) {
|
||||
assertThat(e.getMessage(), containsString("this server does not know how to handle GET operation[Patient] with parameters [[_revinclude, name]]"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Server method with no _revInclude
|
||||
* Client request with _revInclude
|
||||
* <p>
|
||||
* See #1421
|
||||
*/
|
||||
@Test
|
||||
public void testAllowRevIncludeIfProvided() throws Exception {
|
||||
|
||||
class MyProvider extends MyBaseProvider {
|
||||
@Search
|
||||
public List<IBaseResource> search(@OptionalParam(name = "name") StringType theName, @IncludeParam(reverse = true) Set<Include> theRevIncludes) {
|
||||
return Lists.newArrayList(new Patient().setActive(true).setId("Patient/123"));
|
||||
}
|
||||
}
|
||||
MyProvider provider = new MyProvider();
|
||||
|
||||
startServer(provider);
|
||||
|
||||
Bundle results = myClient
|
||||
.search()
|
||||
.forResource(Patient.class)
|
||||
.where(Patient.NAME.matches().value("foo"))
|
||||
.revInclude(Patient.INCLUDE_ORGANIZATION)
|
||||
.returnBundle(Bundle.class)
|
||||
.execute();
|
||||
assertEquals(1, results.getEntry().size());
|
||||
}
|
||||
|
||||
private void startServer(Object theProvider) throws Exception {
|
||||
RestfulServer servlet = new RestfulServer(myCtx);
|
||||
servlet.registerProvider(theProvider);
|
||||
ServletHandler proxyHandler = new ServletHandler();
|
||||
servlet.setDefaultResponseEncoding(EncodingEnum.XML);
|
||||
servlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
|
||||
ServletHolder servletHolder = new ServletHolder(servlet);
|
||||
proxyHandler.addServletWithMapping(servletHolder, "/*");
|
||||
|
||||
myServer = new Server(0);
|
||||
myServer.setHandler(proxyHandler);
|
||||
JettyUtil.startServer(myServer);
|
||||
int port = JettyUtil.getPortForStartedServer(myServer);
|
||||
|
||||
myClient = myCtx.newRestfulGenericClient("http://localhost:" + port);
|
||||
}
|
||||
|
||||
|
||||
public static class MyBaseProvider implements IResourceProvider {
|
||||
|
||||
@Override
|
||||
public Class<? extends IBaseResource> getResourceType() {
|
||||
return Patient.class;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -212,6 +212,11 @@
|
|||
with the new request id, resulting in an ever growing source.meta value. E.g. after the first update, it looks
|
||||
like "#9f0a901387128111#5f37835ee38a89e2" when it should only be "#5f37835ee38a89e2". This has been corrected.
|
||||
</action>
|
||||
<action type="fix" issue="1421">
|
||||
The Plain Server method selector was incorrectly allowing client requests with _include statements to be
|
||||
handled by method implementations that did not have any <![CDATA[<code>@IncludeParam</code>]]> defined. This
|
||||
is now corrected. Thanks to Tuomo Ala-Vannesluoma for reporting and providing a test case!
|
||||
</action>
|
||||
</release>
|
||||
<release version="4.0.3" date="2019-09-03" description="Igloo (Point Release)">
|
||||
<action type="fix">
|
||||
|
|
Loading…
Reference in New Issue