Apply SearchNarrowingInterceptor to conditional URLs (#5712)

* Fix #5110 - Failure in tx processing

* Test fix

* Work on narrowins

* Add changelog

* Docs cleanup

* Fix compile error

* Rollback incompatible change

* Test fix

* Test fix

* Force update

* Test fixes

* Build fixes

* Bump HTMLUnit

* Update hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_2_0/5712-apply-searchnarrowing-to-conditional-urls.yaml

Co-authored-by: Ken Stevens <khstevens@gmail.com>

* Address review comments

* Version bump to 7.1.4-SNAPSHOT

* Spotless

* Roll back version

---------

Co-authored-by: Ken Stevens <khstevens@gmail.com>
This commit is contained in:
James Agnew 2024-02-23 08:42:26 -05:00 committed by GitHub
parent c7f413d6ea
commit ec525f4457
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
97 changed files with 1170 additions and 280 deletions

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -627,6 +627,8 @@ public class BundleUtil {
//noinspection EnumSwitchStatementWhichMissesCases
switch (requestType) {
case PUT:
case DELETE:
case PATCH:
conditionalUrl = url != null && url.contains("?") ? url : null;
break;
case POST:

View File

@ -315,6 +315,7 @@ public class UrlUtil {
return theCtx.getResourceDefinition(resourceName);
}
@Nonnull
public static Map<String, String[]> parseQueryString(String theQueryString) {
HashMap<String, List<String>> map = new HashMap<>();
parseQueryString(theQueryString, map);

View File

@ -69,4 +69,12 @@ public class BundleEntryMutator {
BaseRuntimeChildDefinition resourceChild = myEntryDefinition.getChildByName("resource");
resourceChild.getMutator().setValue(myEntry, theUpdatedResource);
}
public void setRequestIfNoneExist(FhirContext theFhirContext, String theIfNoneExist) {
BaseRuntimeChildDefinition requestUrlChildDef = myRequestChildContentsDef.getChildByName("ifNoneExist");
IPrimitiveType<?> url = ParametersUtil.createString(theFhirContext, theIfNoneExist);
for (IBase nextRequest : myRequestChildDef.getAccessor().getValues(myEntry)) {
requestUrlChildDef.getMutator().addValue(nextRequest, url);
}
}
}

View File

@ -20,6 +20,7 @@
package ca.uhn.fhir.util.bundle;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import org.hl7.fhir.instance.model.api.IBaseResource;
public class ModifiableBundleEntry {
@ -58,4 +59,16 @@ public class ModifiableBundleEntry {
public void setResource(IBaseResource theUpdatedResource) {
myBundleEntryMutator.setResource(theUpdatedResource);
}
public RequestTypeEnum getRequestMethod() {
return myBundleEntryParts.getRequestType();
}
public String getConditionalUrl() {
return myBundleEntryParts.getConditionalUrl();
}
public void setRequestIfNoneExist(FhirContext theFhirContext, String theIfNoneExist) {
myBundleEntryMutator.setRequestIfNoneExist(theFhirContext, theIfNoneExist);
}
}

View File

@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
@ -12,7 +12,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -55,13 +55,13 @@ import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@SuppressWarnings("unused")
/**
* Examples integrated into our documentation.
*/
@SuppressWarnings({"unused", "WriteOnlyObject", "UnnecessaryLocalVariable"})
public class AuthorizationInterceptors {
public class PatientResourceProvider implements IResourceProvider {
public static class PatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
@ -74,8 +74,8 @@ public class AuthorizationInterceptors {
}
}
@SuppressWarnings({"ConstantConditions", "InnerClassMayBeStatic"})
// START SNIPPET: patientAndAdmin
@SuppressWarnings("ConstantConditions")
public class PatientAndAdminAuthorizationInterceptor extends AuthorizationInterceptor {
@Override
@ -265,6 +265,7 @@ public class AuthorizationInterceptors {
}
@SuppressWarnings("InnerClassMayBeStatic")
// START SNIPPET: narrowing
public class MyPatientSearchNarrowingInterceptor extends SearchNarrowingInterceptor {
@ -300,6 +301,13 @@ public class AuthorizationInterceptors {
}
// END SNIPPET: narrowing
public void narrowingConditional() {
// START SNIPPET: narrowingConditional
SearchNarrowingInterceptor interceptor = new SearchNarrowingInterceptor();
interceptor.setNarrowConditionalUrls(true);
// END SNIPPET: narrowingConditional
}
@SuppressWarnings("SpellCheckingInspection")
public void rsNarrowing() {
RestfulServer restfulServer = new RestfulServer();
@ -330,6 +338,7 @@ public class AuthorizationInterceptors {
// END SNIPPET: rsnarrowing
}
@SuppressWarnings("InnerClassMayBeStatic")
// START SNIPPET: narrowingByCode
public class MyCodeSearchNarrowingInterceptor extends SearchNarrowingInterceptor {

View File

@ -0,0 +1,8 @@
---
type: add
jira: SMILE-7971
issue: 5712
title: "The SearchNarrowingInterceptor can now optionally be configured to also apply
URL narrowing to conditional URLs used by conditional create/update/delete/patch
operations, both as raw HTTP transactions as well as within FHIR transaction
Bundles."

View File

@ -193,7 +193,7 @@ HAPI FHIR provides an interceptor that can be used to implement consent rules an
# Security: Search Narrowing
HAPI FHIR provides an interceptor that can be used to implement consent rules and directives. See [Consent Interceptor](/docs/security/consent_interceptor.html) for more information.
HAPI FHIR provides an interceptor that can be used to implement consent rules and directives. See [Search Narrowing Interceptor](/docs/security/search_narrowing_interceptor.html) for more information.
# Security: Rejecting Unsupported HTTP Verbs

View File

@ -25,6 +25,24 @@ An example of this interceptor follows:
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowing}}
```
# Narrowing Conditional URLs
By default, this interceptor will narrow URLs for FHIR search operations only. The
interceptor can also be configured to narrow URLs on conditional operations.
When this feature is enabled request URLs are also narrowed for the following FHIR operations:
* Conditional Create (The `If-None-Exist` header is narrowed)
* Conditional Update (The request URL is narrowed if it is a conditional URL)
* Conditional Delete (The request URL is narrowed if it is a conditional URL)
* Conditional Patch (The request URL is narrowed if it is a conditional URL)
The following example shows how to enable conditional URL narrowing on the interceptor.
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowingConditional}}
```
<a name="constraining-by-valueset-membership"/>
# Constraining by ValueSet Membership

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -117,6 +117,16 @@ public class JaxRsRequest extends RequestDetails {
return requestHeader == null ? Collections.<String>emptyList() : requestHeader;
}
@Override
public void addHeader(String theName, String theValue) {
throw new UnsupportedOperationException(Msg.code(2499) + "Headers can not be modified in JAX-RS");
}
@Override
public void setHeaders(String theName, List<String> theValue) {
throw new UnsupportedOperationException(Msg.code(2500) + "Headers can not be modified in JAX-RS");
}
@Override
public Object getAttribute(String theAttributeName) {
return myAttributes.get(theAttributeName);

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,7 +3,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -567,12 +567,13 @@ public class SystemProviderR4Test extends BaseJpaR4Test {
MyAnonymousInterceptor1 interceptor1 = new MyAnonymousInterceptor1();
ourRestServer.getInterceptorService().registerAnonymousInterceptor(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED, interceptor1);
MySearchNarrowingInterceptor interceptor2 = new MySearchNarrowingInterceptor();
interceptor2.setNarrowConditionalUrls(true);
ourRestServer.getInterceptorService().registerInterceptor(interceptor2);
try {
myClient.transaction().withBundle(input).execute();
assertEquals(1, counter0.get());
assertEquals(1, counter1.get());
assertEquals(5, counter2.get());
assertEquals(1, counter2.get());
} finally {
ourRestServer.getInterceptorService().unregisterInterceptor(interceptor1);

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -249,6 +249,24 @@ public abstract class RequestDetails {
public abstract List<String> getHeaders(String name);
/**
* Adds a new header
*
* @param theName The header name
* @param theValue The header value
* @since 7.2.0
*/
public abstract void addHeader(String theName, String theValue);
/**
* Replaces any existing header(s) with the given name using a List of new header values
*
* @param theName The header name
* @param theValue The header value
* @since 7.2.0
*/
public abstract void setHeaders(String theName, List<String> theValue);
public IIdType getId() {
return myId;
}

View File

@ -33,9 +33,9 @@ import ca.uhn.fhir.rest.server.IPagingProvider;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import java.io.IOException;
import java.io.InputStream;
@ -124,13 +124,27 @@ public class SystemRequestDetails extends RequestDetails {
return headers.get(name);
}
@Override
public void addHeader(String theName, String theValue) {
if (myHeaders == null) {
myHeaders = ArrayListMultimap.create();
}
initHeaderMap();
myHeaders.put(theName, theValue);
}
@Override
public void setHeaders(String theName, List<String> theValues) {
initHeaderMap();
myHeaders.putAll(theName, theValues);
}
private void initHeaderMap() {
if (myHeaders == null) {
// Make sure we are case-insensitive on keys
myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
.arrayListValues()
.build();
}
}
@Override
public Object getAttribute(String theAttributeName) {
return null;
@ -145,7 +159,7 @@ public class SystemRequestDetails extends RequestDetails {
}
@Override
public Reader getReader() throws IOException {
public Reader getReader() {
return null;
}

View File

@ -0,0 +1,25 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.interceptor;
/**
* Request object for {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_CONDITIONAL_URL_PREPROCESS}
*/
public class ConditionalUrlRequest {}

View File

@ -0,0 +1,25 @@
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2024 Smile CDR, Inc.
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/
package ca.uhn.fhir.rest.server.interceptor;
/**
* Request object for {@link ca.uhn.fhir.interceptor.api.Pointcut#STORAGE_CONDITIONAL_URL_PREPROCESS}
*/
public class ConditionalUrlResponse {}

View File

@ -30,21 +30,21 @@ import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.servlet.ServletSubRequestDetails;
import ca.uhn.fhir.rest.server.util.ServletRequestUtil;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.FhirTerser;
import ca.uhn.fhir.util.UrlUtil;
import ca.uhn.fhir.util.ValidateUtil;
import ca.uhn.fhir.util.bundle.ModifiableBundleEntry;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -65,6 +65,8 @@ import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
/**
* This interceptor can be used to automatically narrow the scope of searches in order to
* automatically restrict the searches to specific compartments.
@ -88,12 +90,26 @@ import java.util.stream.Collectors;
*
* @see AuthorizationInterceptor
*/
@SuppressWarnings("JavadocLinkAsPlainText")
public class SearchNarrowingInterceptor {
public static final String POST_FILTERING_LIST_ATTRIBUTE_NAME =
SearchNarrowingInterceptor.class.getName() + "_POST_FILTERING_LIST";
private IValidationSupport myValidationSupport;
private int myPostFilterLargeValueSetThreshold = 500;
private boolean myNarrowConditionalUrls;
/**
* If set to {@literal true} (default is {@literal false}), conditional URLs such
* as the If-None-Exist header used for Conditional Create operations will
* also be narrowed.
*
* @param theNarrowConditionalUrls Should we narrow conditional URLs in requests
* @since 7.2.0
*/
public void setNarrowConditionalUrls(boolean theNarrowConditionalUrls) {
myNarrowConditionalUrls = theNarrowConditionalUrls;
}
/**
* Supplies a threshold over which any ValueSet-based rules will be applied by
@ -126,6 +142,68 @@ public class SearchNarrowingInterceptor {
return this;
}
/**
* This method handles narrowing for FHIR search/create/update/patch operations.
*
* @see #hookIncomingRequestPreHandled(ServletRequestDetails, HttpServletRequest, HttpServletResponse) This method narrows FHIR transaction bundles
*/
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
public void hookIncomingRequestPostProcessed(
RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
throws AuthenticationException {
// We don't support this operation type yet
RestOperationTypeEnum restOperationType = theRequestDetails.getRestOperationType();
Validate.isTrue(restOperationType != RestOperationTypeEnum.SEARCH_SYSTEM);
switch (restOperationType) {
case EXTENDED_OPERATION_INSTANCE:
case EXTENDED_OPERATION_TYPE: {
if ("$everything".equals(theRequestDetails.getOperation())) {
narrowEverythingOperation(theRequestDetails);
}
break;
}
case SEARCH_TYPE:
narrowTypeSearch(theRequestDetails);
break;
case CREATE:
narrowIfNoneExistHeader(theRequestDetails);
break;
case DELETE:
case UPDATE:
case PATCH:
narrowRequestUrl(theRequestDetails, restOperationType);
break;
}
}
/**
* This method narrows FHIR transaction operations (because this pointcut
* is called after the request body is parsed).
*
* @see #hookIncomingRequestPostProcessed(RequestDetails, HttpServletRequest, HttpServletResponse) This method narrows FHIR search/create/update/etc operations
*/
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void hookIncomingRequestPreHandled(
ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
throws AuthenticationException {
if (theRequestDetails.getRestOperationType() != null) {
switch (theRequestDetails.getRestOperationType()) {
case TRANSACTION:
case BATCH:
IBaseBundle bundle = (IBaseBundle) theRequestDetails.getResource();
FhirContext ctx = theRequestDetails.getFhirContext();
BundleEntryUrlProcessor processor = new BundleEntryUrlProcessor(ctx, theRequestDetails);
BundleUtil.processEntries(ctx, bundle, processor);
break;
}
}
}
/**
* Subclasses should override this method to supply the set of compartments that
* the user making the request should actually have access to.
@ -143,54 +221,214 @@ public class SearchNarrowingInterceptor {
return null;
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
public boolean hookIncomingRequestPostProcessed(
RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
throws AuthenticationException {
// We don't support this operation type yet
Validate.isTrue(theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_SYSTEM);
/**
* For the $everything operation, we only do code narrowing, and in this case
* we're not actually even making any changes to the request. All we do here is
* ensure that an attribute is added to the request, which is picked up later
* by {@link SearchNarrowingConsentService}.
*/
private void narrowEverythingOperation(RequestDetails theRequestDetails) {
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
if (authorizedList != null) {
buildParameterListForAuthorizedCodes(
theRequestDetails, theRequestDetails.getResourceName(), authorizedList);
}
}
private void narrowIfNoneExistHeader(RequestDetails theRequestDetails) {
if (myNarrowConditionalUrls) {
String ifNoneExist = theRequestDetails.getHeader(Constants.HEADER_IF_NONE_EXIST);
if (isNotBlank(ifNoneExist)) {
String newConditionalUrl = narrowConditionalUrlForCompartmentOnly(
theRequestDetails, ifNoneExist, true, theRequestDetails.getResourceName());
if (newConditionalUrl != null) {
theRequestDetails.setHeaders(Constants.HEADER_IF_NONE_EXIST, List.of(newConditionalUrl));
}
}
}
}
private void narrowRequestUrl(RequestDetails theRequestDetails, RestOperationTypeEnum theRestOperationType) {
if (myNarrowConditionalUrls) {
String conditionalUrl = theRequestDetails.getConditionalUrl(theRestOperationType);
if (isNotBlank(conditionalUrl)) {
String newConditionalUrl = narrowConditionalUrlForCompartmentOnly(
theRequestDetails, conditionalUrl, false, theRequestDetails.getResourceName());
if (newConditionalUrl != null) {
String newCompleteUrl = theRequestDetails
.getCompleteUrl()
.substring(
0,
theRequestDetails.getCompleteUrl().indexOf('?') + 1)
+ newConditionalUrl;
theRequestDetails.setCompleteUrl(newCompleteUrl);
}
}
}
}
/**
* Does not narrow codes
*/
@Nullable
private String narrowConditionalUrlForCompartmentOnly(
RequestDetails theRequestDetails,
@Nonnull String theConditionalUrl,
boolean theIncludeUpToQuestionMarkInResponse,
String theResourceName) {
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
return narrowConditionalUrl(
theRequestDetails,
theConditionalUrl,
theIncludeUpToQuestionMarkInResponse,
theResourceName,
false,
authorizedList);
}
@Nullable
private String narrowConditionalUrl(
RequestDetails theRequestDetails,
@Nonnull String theConditionalUrl,
boolean theIncludeUpToQuestionMarkInResponse,
String theResourceName,
boolean theNarrowCodes,
AuthorizedList theAuthorizedList) {
if (theAuthorizedList == null) {
return null;
}
ListMultimap<String, String> parametersToAdd =
buildParameterListForAuthorizedCompartment(theRequestDetails, theResourceName, theAuthorizedList);
if (theNarrowCodes) {
ListMultimap<String, String> parametersToAddForCodes =
buildParameterListForAuthorizedCodes(theRequestDetails, theResourceName, theAuthorizedList);
if (parametersToAdd == null) {
parametersToAdd = parametersToAddForCodes;
} else if (parametersToAddForCodes != null) {
parametersToAdd.putAll(parametersToAddForCodes);
}
}
String newConditionalUrl = null;
if (parametersToAdd != null) {
String query = theConditionalUrl;
int qMarkIndex = theConditionalUrl.indexOf('?');
if (qMarkIndex != -1) {
query = theConditionalUrl.substring(qMarkIndex + 1);
}
Map<String, String[]> inputParams = UrlUtil.parseQueryString(query);
Map<String, String[]> newParameters = applyCompartmentParameters(parametersToAdd, true, inputParams);
StringBuilder newUrl = new StringBuilder();
if (theIncludeUpToQuestionMarkInResponse) {
newUrl.append(qMarkIndex != -1 ? theConditionalUrl.substring(0, qMarkIndex + 1) : "?");
}
boolean first = true;
for (Map.Entry<String, String[]> nextEntry : newParameters.entrySet()) {
for (String nextValue : nextEntry.getValue()) {
if (isNotBlank(nextValue)) {
if (first) {
first = false;
} else {
newUrl.append("&");
}
newUrl.append(UrlUtil.escapeUrlParam(nextEntry.getKey()));
newUrl.append("=");
newUrl.append(UrlUtil.escapeUrlParam(nextValue));
}
}
}
newConditionalUrl = newUrl.toString();
}
return newConditionalUrl;
}
private void narrowTypeSearch(RequestDetails theRequestDetails) {
// N.B do not add code above this for filtering, this should only ever occur on search.
if (shouldSkipNarrowing(theRequestDetails)) {
return true;
return;
}
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
if (authorizedList == null) {
return true;
return;
}
// Add rules to request so that the SearchNarrowingConsentService can pick them up
String resourceName = theRequestDetails.getResourceName();
// Narrow request URL for compartments
ListMultimap<String, String> parametersToAdd =
buildParameterListForAuthorizedCompartment(theRequestDetails, resourceName, authorizedList);
if (parametersToAdd != null) {
applyParametersToRequestDetails(theRequestDetails, parametersToAdd, true);
}
// Narrow request URL for codes - Add rules to request so that the SearchNarrowingConsentService can pick them
// up
ListMultimap<String, String> parameterToOrValues =
buildParameterListForAuthorizedCodes(theRequestDetails, resourceName, authorizedList);
if (parameterToOrValues != null) {
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, false);
}
}
@Nullable
private ListMultimap<String, String> buildParameterListForAuthorizedCodes(
RequestDetails theRequestDetails, String resourceName, AuthorizedList authorizedList) {
List<AllowedCodeInValueSet> postFilteringList = getPostFilteringList(theRequestDetails);
if (authorizedList.getAllowedCodeInValueSets() != null) {
postFilteringList.addAll(authorizedList.getAllowedCodeInValueSets());
}
List<AllowedCodeInValueSet> allowedCodeInValueSet = authorizedList.getAllowedCodeInValueSets();
ListMultimap<String, String> parameterToOrValues = null;
if (allowedCodeInValueSet != null) {
FhirContext context = theRequestDetails.getServer().getFhirContext();
RuntimeResourceDefinition resourceDef = context.getResourceDefinition(resourceName);
parameterToOrValues = processAllowedCodes(resourceDef, allowedCodeInValueSet);
}
return parameterToOrValues;
}
@Nullable
private ListMultimap<String, String> buildParameterListForAuthorizedCompartment(
RequestDetails theRequestDetails, String theResourceName, @Nullable AuthorizedList theAuthorizedList) {
if (theAuthorizedList == null) {
return null;
}
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName());
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theResourceName);
/*
* Create a map of search parameter values that need to be added to the
* given request
*/
Collection<String> compartments = authorizedList.getAllowedCompartments();
Collection<String> compartments = theAuthorizedList.getAllowedCompartments();
ListMultimap<String, String> parametersToAdd = null;
if (compartments != null) {
Map<String, List<String>> parameterToOrValues =
processResourcesOrCompartments(theRequestDetails, resDef, compartments, true);
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true);
}
Collection<String> resources = authorizedList.getAllowedInstances();
if (resources != null) {
Map<String, List<String>> parameterToOrValues =
processResourcesOrCompartments(theRequestDetails, resDef, resources, false);
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true);
}
List<AllowedCodeInValueSet> allowedCodeInValueSet = authorizedList.getAllowedCodeInValueSets();
if (allowedCodeInValueSet != null) {
Map<String, List<String>> parameterToOrValues = processAllowedCodes(resDef, allowedCodeInValueSet);
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, false);
parametersToAdd =
processResourcesOrCompartments(theRequestDetails, resDef, compartments, true, theResourceName);
}
return true;
Collection<String> resources = theAuthorizedList.getAllowedInstances();
if (resources != null) {
ListMultimap<String, String> parameterToOrValues =
processResourcesOrCompartments(theRequestDetails, resDef, resources, false, theResourceName);
if (parametersToAdd == null) {
parametersToAdd = parameterToOrValues;
} else if (parameterToOrValues != null) {
parametersToAdd.putAll(parameterToOrValues);
}
}
return parametersToAdd;
}
/**
@ -201,102 +439,26 @@ public class SearchNarrowingInterceptor {
&& !"$everything".equalsIgnoreCase(theRequestDetails.getOperation());
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void hookIncomingRequestPreHandled(
ServletRequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse)
throws AuthenticationException {
if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.TRANSACTION) {
return;
}
IBaseBundle bundle = (IBaseBundle) theRequestDetails.getResource();
FhirContext ctx = theRequestDetails.getFhirContext();
BundleEntryUrlProcessor processor =
new BundleEntryUrlProcessor(ctx, theRequestDetails, theRequest, theResponse);
BundleUtil.processEntries(ctx, bundle, processor);
}
private void applyParametersToRequestDetails(
RequestDetails theRequestDetails,
@Nullable Map<String, List<String>> theParameterToOrValues,
@Nullable ListMultimap<String, String> theParameterToOrValues,
boolean thePatientIdMode) {
Map<String, String[]> inputParameters = theRequestDetails.getParameters();
if (theParameterToOrValues != null) {
Map<String, String[]> newParameters = new HashMap<>(theRequestDetails.getParameters());
for (Map.Entry<String, List<String>> nextEntry : theParameterToOrValues.entrySet()) {
String nextParamName = nextEntry.getKey();
List<String> nextAllowedValues = nextEntry.getValue();
if (!newParameters.containsKey(nextParamName)) {
/*
* If we don't already have a parameter of the given type, add one
*/
String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
String[] paramValues = {nextValuesJoined};
newParameters.put(nextParamName, paramValues);
} else {
/*
* If the client explicitly requested the given parameter already, we'll
* just update the request to have the intersection of the values that the client
* requested, and the values that the user is allowed to see
*/
String[] existingValues = newParameters.get(nextParamName);
if (thePatientIdMode) {
List<String> nextAllowedValueIds = nextAllowedValues.stream()
.map(t -> t.lastIndexOf("/") > -1 ? t.substring(t.lastIndexOf("/") + 1) : t)
.collect(Collectors.toList());
boolean restrictedExistingList = false;
for (int i = 0; i < existingValues.length; i++) {
String nextExistingValue = existingValues[i];
List<String> nextRequestedValues =
QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue);
List<String> nextPermittedValues = ListUtils.union(
ListUtils.intersection(nextRequestedValues, nextAllowedValues),
ListUtils.intersection(nextRequestedValues, nextAllowedValueIds));
if (nextPermittedValues.size() > 0) {
restrictedExistingList = true;
existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues);
}
}
/*
* If none of the values that were requested by the client overlap at all
* with the values that the user is allowed to see, the client shouldn't
* get *any* results back. We return an error code indicating that the
* caller is forbidden from accessing the resources they requested.
*/
if (!restrictedExistingList) {
throw new ForbiddenOperationException(Msg.code(2026) + "Value not permitted for parameter "
+ UrlUtil.escapeUrlParam(nextParamName));
}
} else {
int existingValuesCount = existingValues.length;
String[] newValues =
Arrays.copyOf(existingValues, existingValuesCount + nextAllowedValues.size());
for (int i = 0; i < nextAllowedValues.size(); i++) {
newValues[existingValuesCount + i] = nextAllowedValues.get(i);
}
newParameters.put(nextParamName, newValues);
}
}
}
Map<String, String[]> newParameters =
applyCompartmentParameters(theParameterToOrValues, thePatientIdMode, inputParameters);
theRequestDetails.setParameters(newParameters);
}
}
@Nullable
private Map<String, List<String>> processResourcesOrCompartments(
private ListMultimap<String, String> processResourcesOrCompartments(
RequestDetails theRequestDetails,
RuntimeResourceDefinition theResDef,
Collection<String> theResourcesOrCompartments,
boolean theAreCompartments) {
Map<String, List<String>> retVal = null;
boolean theAreCompartments,
String theResourceName) {
ListMultimap<String, String> retVal = null;
String lastCompartmentName = null;
String lastSearchParamName = null;
@ -315,7 +477,7 @@ public class SearchNarrowingInterceptor {
} else {
if (compartmentName.equalsIgnoreCase(theRequestDetails.getResourceName())) {
if (compartmentName.equalsIgnoreCase(theResourceName)) {
searchParamName = "_id";
@ -331,10 +493,9 @@ public class SearchNarrowingInterceptor {
if (searchParamName != null) {
if (retVal == null) {
retVal = new HashMap<>();
retVal = MultimapBuilder.hashKeys().arrayListValues().build();
}
List<String> orValues = retVal.computeIfAbsent(searchParamName, t -> new ArrayList<>());
orValues.add(nextCompartment);
retVal.put(searchParamName, nextCompartment);
}
}
@ -342,9 +503,9 @@ public class SearchNarrowingInterceptor {
}
@Nullable
private Map<String, List<String>> processAllowedCodes(
private ListMultimap<String, String> processAllowedCodes(
RuntimeResourceDefinition theResDef, List<AllowedCodeInValueSet> theAllowedCodeInValueSet) {
Map<String, List<String>> retVal = null;
ListMultimap<String, String> retVal = null;
for (AllowedCodeInValueSet next : theAllowedCodeInValueSet) {
String resourceName = next.getResourceName();
@ -371,9 +532,9 @@ public class SearchNarrowingInterceptor {
}
if (retVal == null) {
retVal = new HashMap<>();
retVal = MultimapBuilder.hashKeys().arrayListValues().build();
}
retVal.computeIfAbsent(paramName, k -> new ArrayList<>()).add(valueSetUrl);
retVal.put(paramName, valueSetUrl);
}
return retVal;
@ -408,7 +569,7 @@ public class SearchNarrowingInterceptor {
Set<String> queryParameters = theRequestDetails.getParameters().keySet();
List<RuntimeSearchParam> searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName);
if (searchParams.size() > 0) {
if (!searchParams.isEmpty()) {
// Resources like Observation have several fields that add the resource to
// the compartment. In the case of Observation, it's subject, patient and performer.
@ -467,41 +628,75 @@ public class SearchNarrowingInterceptor {
}
}
private class BundleEntryUrlProcessor implements Consumer<ModifiableBundleEntry> {
private final FhirContext myFhirContext;
private final ServletRequestDetails myRequestDetails;
private final HttpServletRequest myRequest;
private final HttpServletResponse myResponse;
@Nonnull
private static Map<String, String[]> applyCompartmentParameters(
@Nonnull ListMultimap<String, String> theParameterToOrValues,
boolean thePatientIdMode,
Map<String, String[]> theInputParameters) {
Map<String, String[]> newParameters = new HashMap<>(theInputParameters);
for (String nextParamName : theParameterToOrValues.keySet()) {
List<String> nextAllowedValues = theParameterToOrValues.get(nextParamName);
public BundleEntryUrlProcessor(
FhirContext theFhirContext,
ServletRequestDetails theRequestDetails,
HttpServletRequest theRequest,
HttpServletResponse theResponse) {
myFhirContext = theFhirContext;
myRequestDetails = theRequestDetails;
myRequest = theRequest;
myResponse = theResponse;
}
@Override
public void accept(ModifiableBundleEntry theModifiableBundleEntry) {
ArrayListMultimap<String, String> paramValues = ArrayListMultimap.create();
String url = theModifiableBundleEntry.getRequestUrl();
ServletSubRequestDetails subServletRequestDetails =
ServletRequestUtil.getServletSubRequestDetails(myRequestDetails, url, paramValues);
BaseMethodBinding method =
subServletRequestDetails.getServer().determineResourceMethod(subServletRequestDetails, url);
RestOperationTypeEnum restOperationType = method.getRestOperationType();
subServletRequestDetails.setRestOperationType(restOperationType);
hookIncomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse);
theModifiableBundleEntry.setRequestUrl(
myFhirContext, ServletRequestUtil.extractUrl(subServletRequestDetails));
if (!newParameters.containsKey(nextParamName)) {
/*
* If we don't already have a parameter of the given type, add one
*/
String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
String[] paramValues = {nextValuesJoined};
newParameters.put(nextParamName, paramValues);
} else {
/*
* If the client explicitly requested the given parameter already, we'll
* just update the request to have the intersection of the values that the client
* requested, and the values that the user is allowed to see
*/
String[] existingValues = newParameters.get(nextParamName);
if (thePatientIdMode) {
List<String> nextAllowedValueIds = nextAllowedValues.stream()
.map(t -> t.lastIndexOf("/") > -1 ? t.substring(t.lastIndexOf("/") + 1) : t)
.collect(Collectors.toList());
boolean restrictedExistingList = false;
for (int i = 0; i < existingValues.length; i++) {
String nextExistingValue = existingValues[i];
List<String> nextRequestedValues =
QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue);
List<String> nextPermittedValues = ListUtils.union(
ListUtils.intersection(nextRequestedValues, nextAllowedValues),
ListUtils.intersection(nextRequestedValues, nextAllowedValueIds));
if (!nextPermittedValues.isEmpty()) {
restrictedExistingList = true;
existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues);
}
}
/*
* If none of the values that were requested by the client overlap at all
* with the values that the user is allowed to see, the client shouldn't
* get *any* results back. We return an error code indicating that the
* caller is forbidden from accessing the resources they requested.
*/
if (!restrictedExistingList) {
throw new ForbiddenOperationException(Msg.code(2026) + "Value not permitted for parameter "
+ UrlUtil.escapeUrlParam(nextParamName));
}
} else {
int existingValuesCount = existingValues.length;
String[] newValues = Arrays.copyOf(existingValues, existingValuesCount + nextAllowedValues.size());
for (int i = 0; i < nextAllowedValues.size(); i++) {
newValues[existingValuesCount + i] = nextAllowedValues.get(i);
}
newParameters.put(nextParamName, newValues);
}
}
}
return newParameters;
}
static List<AllowedCodeInValueSet> getPostFilteringList(RequestDetails theRequestDetails) {
@ -517,4 +712,82 @@ public class SearchNarrowingInterceptor {
static List<AllowedCodeInValueSet> getPostFilteringListOrNull(RequestDetails theRequestDetails) {
return (List<AllowedCodeInValueSet>) theRequestDetails.getAttribute(POST_FILTERING_LIST_ATTRIBUTE_NAME);
}
private class BundleEntryUrlProcessor implements Consumer<ModifiableBundleEntry> {
private final FhirContext myFhirContext;
private final ServletRequestDetails myRequestDetails;
private final AuthorizedList myAuthorizedList;
public BundleEntryUrlProcessor(FhirContext theFhirContext, ServletRequestDetails theRequestDetails) {
myFhirContext = theFhirContext;
myRequestDetails = theRequestDetails;
myAuthorizedList = buildAuthorizedList(theRequestDetails);
}
@SuppressWarnings("EnumSwitchStatementWhichMissesCases")
@Override
public void accept(ModifiableBundleEntry theModifiableBundleEntry) {
if (myAuthorizedList == null) {
return;
}
RequestTypeEnum method = theModifiableBundleEntry.getRequestMethod();
String requestUrl = theModifiableBundleEntry.getRequestUrl();
if (method != null && isNotBlank(requestUrl)) {
String resourceType = UrlUtil.parseUrl(requestUrl).getResourceType();
switch (method) {
case GET: {
String existingRequestUrl = theModifiableBundleEntry.getRequestUrl();
String newConditionalUrl = narrowConditionalUrl(
myRequestDetails, existingRequestUrl, false, resourceType, true, myAuthorizedList);
if (isNotBlank(newConditionalUrl)) {
newConditionalUrl = resourceType + "?" + newConditionalUrl;
theModifiableBundleEntry.setRequestUrl(myFhirContext, newConditionalUrl);
}
break;
}
case POST: {
if (myNarrowConditionalUrls) {
String existingConditionalUrl = theModifiableBundleEntry.getConditionalUrl();
if (isNotBlank(existingConditionalUrl)) {
String newConditionalUrl = narrowConditionalUrl(
myRequestDetails,
existingConditionalUrl,
true,
resourceType,
false,
myAuthorizedList);
if (isNotBlank(newConditionalUrl)) {
theModifiableBundleEntry.setRequestIfNoneExist(myFhirContext, newConditionalUrl);
}
}
}
break;
}
case PUT:
case DELETE:
case PATCH: {
if (myNarrowConditionalUrls) {
String existingConditionalUrl = theModifiableBundleEntry.getConditionalUrl();
if (isNotBlank(existingConditionalUrl)) {
String newConditionalUrl = narrowConditionalUrl(
myRequestDetails,
existingConditionalUrl,
true,
resourceType,
false,
myAuthorizedList);
if (isNotBlank(newConditionalUrl)) {
theModifiableBundleEntry.setRequestUrl(myFhirContext, newConditionalUrl);
}
}
}
break;
}
}
}
}
}
}

View File

@ -28,6 +28,8 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestfulServerUtils;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import jakarta.annotation.Nonnull;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -59,6 +61,7 @@ public class ServletRequestDetails extends RequestDetails {
private RestfulServer myServer;
private HttpServletRequest myServletRequest;
private HttpServletResponse myServletResponse;
private ListMultimap<String, String> myHeaders;
/**
* Constructor for testing only
@ -129,17 +132,63 @@ public class ServletRequestDetails extends RequestDetails {
@Override
public String getHeader(String name) {
// For efficiency, we only make a copy of the request headers if we need to
// modify them
if (myHeaders != null) {
List<String> values = myHeaders.get(name);
if (values.isEmpty()) {
return null;
} else {
return values.get(0);
}
}
return getServletRequest().getHeader(name);
}
@Override
public List<String> getHeaders(String name) {
// For efficiency, we only make a copy of the request headers if we need to
// modify them
if (myHeaders != null) {
return myHeaders.get(name);
}
Enumeration<String> headers = getServletRequest().getHeaders(name);
return headers == null
? Collections.emptyList()
: Collections.list(getServletRequest().getHeaders(name));
}
@Override
public void addHeader(String theName, String theValue) {
initHeaders();
myHeaders.put(theName, theValue);
}
@Override
public void setHeaders(String theName, List<String> theValue) {
initHeaders();
myHeaders.removeAll(theName);
myHeaders.putAll(theName, theValue);
}
private void initHeaders() {
if (myHeaders == null) {
// Make sure we are case-insensitive for header names
myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
.arrayListValues()
.build();
Enumeration<String> headerNames = getServletRequest().getHeaderNames();
while (headerNames.hasMoreElements()) {
String nextName = headerNames.nextElement();
Enumeration<String> values = getServletRequest().getHeaders(nextName);
while (values.hasMoreElements()) {
myHeaders.put(nextName, values.nextElement());
}
}
}
}
@Override
public Object getAttribute(String theAttributeName) {
Validate.notBlank(theAttributeName, "theAttributeName must not be null or blank");

View File

@ -19,34 +19,38 @@
*/
package ca.uhn.fhir.rest.server.servlet;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.MultimapBuilder;
import jakarta.annotation.Nonnull;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class ServletSubRequestDetails extends ServletRequestDetails {
private final ServletRequestDetails myWrap;
private Map<String, List<String>> myHeaders = new HashMap<>();
/**
* Map with case-insensitive keys
*/
private final ListMultimap<String, String> myHeaders = MultimapBuilder.treeKeys(String.CASE_INSENSITIVE_ORDER)
.arrayListValues()
.build();
/**
* Constructor
*
* @param theRequestDetails The parent request details
*/
public ServletSubRequestDetails(ServletRequestDetails theRequestDetails) {
public ServletSubRequestDetails(@Nonnull ServletRequestDetails theRequestDetails) {
super(theRequestDetails.getInterceptorBroadcaster());
myWrap = theRequestDetails;
if (theRequestDetails != null) {
Map<String, List<String>> headers = theRequestDetails.getHeaders();
for (Map.Entry<String, List<String>> next : headers.entrySet()) {
myHeaders.put(next.getKey().toLowerCase(), next.getValue());
}
Map<String, List<String>> headers = theRequestDetails.getHeaders();
for (Map.Entry<String, List<String>> next : headers.entrySet()) {
myHeaders.putAll(next.getKey(), next.getValue());
}
}
@ -60,16 +64,15 @@ public class ServletSubRequestDetails extends ServletRequestDetails {
return myWrap.getServletResponse();
}
@Override
public void addHeader(String theName, String theValue) {
String lowerCase = theName.toLowerCase();
List<String> list = myHeaders.computeIfAbsent(lowerCase, k -> new ArrayList<>());
list.add(theValue);
myHeaders.put(theName, theValue);
}
@Override
public String getHeader(String theName) {
List<String> list = myHeaders.get(theName.toLowerCase());
if (list == null || list.isEmpty()) {
List<String> list = myHeaders.get(theName);
if (list.isEmpty()) {
return null;
}
return list.get(0);
@ -78,7 +81,7 @@ public class ServletSubRequestDetails extends ServletRequestDetails {
@Override
public List<String> getHeaders(String theName) {
List<String> list = myHeaders.get(theName.toLowerCase());
if (list == null || list.isEmpty()) {
if (list.isEmpty()) {
return null;
}
return list;

View File

@ -68,22 +68,4 @@ public class ServletRequestUtil {
theRequestDetails.getServer().populateRequestDetailsFromRequestPath(requestDetails, url);
return requestDetails;
}
public static String extractUrl(ServletRequestDetails theRequestDetails) {
StringBuilder b = new StringBuilder();
for (Map.Entry<String, String[]> next :
theRequestDetails.getParameters().entrySet()) {
for (String nextValue : next.getValue()) {
if (b.length() == 0) {
b.append('?');
} else {
b.append('&');
}
b.append(UrlUtil.escapeUrlParam(next.getKey()));
b.append('=');
b.append(UrlUtil.escapeUrlParam(nextValue));
}
}
return theRequestDetails.getRequestPath() + b.toString();
}
}

View File

@ -1,16 +1,32 @@
package ca.uhn.fhir.rest.server.servlet;
import ca.uhn.fhir.rest.api.Constants;
import org.apache.commons.collections4.iterators.IteratorEnumeration;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Enumeration;
import java.util.List;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class ServletRequestDetailsTest {
@Mock
private HttpServletRequest myHttpServletRequest;
@Test
public void testRewriteHistoryHeader() {
ServletRequestDetails servletRequestDetails = new ServletRequestDetails();
@ -41,4 +57,24 @@ class ServletRequestDetailsTest {
assertFalse(servletRequestDetails.isRewriteHistory());
}
@Test
public void testAddHeader() {
ServletRequestDetails srd = new ServletRequestDetails();
srd.setServletRequest(myHttpServletRequest);
when(myHttpServletRequest.getHeaderNames()).thenReturn(new IteratorEnumeration<>(List.of("Foo").iterator()));
when(myHttpServletRequest.getHeaders(eq("Foo"))).thenReturn(new IteratorEnumeration<>(List.of("Bar", "Baz").iterator()));
srd.addHeader("Name", "Value");
srd.addHeader("Name", "Value2");
// Verify added headers (make sure we're case insensitive)
assertEquals("Value", srd.getHeader("NAME"));
assertThat(srd.getHeaders("name"), Matchers.contains("Value", "Value2"));
// Verify original headers (make sure we're case insensitive)
assertEquals("Bar", srd.getHeader("FOO"));
assertThat(srd.getHeaders("foo"), Matchers.contains("Bar", "Baz"));
}
}

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
@ -21,7 +21,7 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-caching-api</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
</dependency>
<dependency>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir-serviceloaders</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,12 +4,20 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.annotation.Delete;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Patch;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.annotation.Transaction;
import ca.uhn.fhir.rest.annotation.TransactionParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.PatchTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
@ -23,12 +31,18 @@ import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.BundleBuilder;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import jakarta.annotation.Nonnull;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.IdType;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.Resource;
@ -44,16 +58,19 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.annotation.Nonnull;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static ca.uhn.fhir.util.UrlUtil.escapeUrlParam;
import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
@ -77,10 +94,13 @@ public class SearchNarrowingInterceptorTest {
private static List<Resource> ourReturn;
private static AuthorizedList ourNextAuthorizedList;
private static Bundle.BundleEntryRequestComponent ourLastBundleRequest;
private static String ourLastConditionalUrl;
private IGenericClient myClient;
@Mock
private IValidationSupport myValidationSupport;
private MySearchNarrowingInterceptor myInterceptor;
@RegisterExtension
private RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx)
.registerProvider(new DummyObservationResourceProvider())
@ -99,8 +119,11 @@ public class SearchNarrowingInterceptorTest {
ourLastPerformerParam = null;
ourLastCodeParam = null;
ourNextAuthorizedList = null;
ourLastConditionalUrl = null;
myInterceptor = new MySearchNarrowingInterceptor();
myInterceptor.setNarrowConditionalUrls(true);
myRestfulServerExtension.registerInterceptor(myInterceptor);
myClient = myRestfulServerExtension.getFhirClient();
@ -299,7 +322,7 @@ public class SearchNarrowingInterceptorTest {
.execute();
assertEquals("transaction", ourLastHitMethod);
assertEquals("Patient?_id=" + URLEncoder.encode("Patient/123,Patient/456"), ourLastBundleRequest.getUrl());
assertEquals("Patient?_id=" + UrlUtil.escapeUrlParam("Patient/123,Patient/456"), ourLastBundleRequest.getUrl());
}
@Test
public void testNarrow_OnlyAppliesToSearches() {
@ -539,6 +562,319 @@ public class SearchNarrowingInterceptorTest {
assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456"));
}
@Test
public void testNarrowCompartment_ConditionalCreate_Patient() {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
myClient
.create()
.resource(new Patient().setActive(true))
.conditionalByUrl("Patient?active=true")
.execute();
assertEquals("Patient.create", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("/Patient?"));
assertThat(ourLastConditionalUrl, containsString("active=true"));
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalCreate_Patient_Disabled() {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
myInterceptor.setNarrowConditionalUrls(false);
myClient
.create()
.resource(new Patient().setActive(true))
.conditionalByUrl("Patient?active=true")
.execute();
assertEquals("Patient.create", ourLastHitMethod);
assertEquals("/Patient?active=true", ourLastConditionalUrl);
}
@Test
public void testNarrowCompartment_ConditionalCreate_Patient_ReturnNull() {
ourNextAuthorizedList = null;
myClient
.create()
.resource(new Patient().setActive(true))
.conditionalByUrl("Patient?active=true")
.execute();
assertEquals("Patient.create", ourLastHitMethod);
assertThat(ourLastConditionalUrl, equalTo("/Patient?active=true"));
}
@Test
public void testNarrowCompartment_ConditionalCreate_Observation() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
myClient
.create()
.resource(new Observation().setStatus(Observation.ObservationStatus.FINAL))
.conditionalByUrl("Observation?status=final")
.execute();
assertEquals("Observation.create", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("/Observation?"));
assertThat(ourLastConditionalUrl, containsString("status=final"));
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalCreate_Observation_InTransaction() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionCreateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL)).conditional("Observation?status=final");
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertEquals("Observation", ourLastBundleRequest.getUrl());
assertThat(ourLastBundleRequest.getIfNoneExist(), startsWith("Observation?"));
assertThat(ourLastBundleRequest.getIfNoneExist(), containsString("status=final"));
assertThat(ourLastBundleRequest.getIfNoneExist(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalCreate_Observation_InTransaction_Disabled() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
myInterceptor.setNarrowConditionalUrls(false);
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionCreateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL)).conditional("Observation?status=final");
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertEquals("Observation", ourLastBundleRequest.getUrl());
assertEquals("Observation?status=final", ourLastBundleRequest.getIfNoneExist());
}
@Test
public void testNarrowCompartment_ConditionalUpdate_Patient() {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
myClient
.update()
.resource(new Patient().setActive(true))
.conditionalByUrl("Patient?active=true")
.execute();
assertEquals("Patient.update", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("Patient?"));
assertThat(ourLastConditionalUrl, containsString("active=true"));
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalUpdate_Patient_Disabled() {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
myInterceptor.setNarrowConditionalUrls(false);
myClient
.update()
.resource(new Patient().setActive(true))
.conditionalByUrl("Patient?active=true")
.execute();
assertEquals("Patient.update", ourLastHitMethod);
assertEquals("Patient?active=true", ourLastConditionalUrl);
}
@Test
public void testNarrowCompartment_ConditionalUpdate_Observation() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
myClient
.update()
.resource(new Observation().setStatus(Observation.ObservationStatus.FINAL))
.conditionalByUrl("Observation?status=final")
.execute();
assertEquals("Observation.update", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("Observation?"));
assertThat(ourLastConditionalUrl, containsString("status=final"));
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalUpdate_Observation_InTransaction() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionUpdateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL)).conditional("Observation?status=final");
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertThat(ourLastBundleRequest.getUrl(), startsWith("Observation?"));
assertThat(ourLastBundleRequest.getUrl(), containsString("status=final"));
assertThat(ourLastBundleRequest.getUrl(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalUpdate_Observation_InTransaction_NoConditionalUrl() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionUpdateEntry(new Observation().setStatus(Observation.ObservationStatus.FINAL).setId("Observation/ABC"));
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertEquals("Observation/ABC", ourLastBundleRequest.getUrl());
}
@Test
public void testNarrowCompartment_ConditionalDelete_Patient() {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
myClient
.delete()
.resourceConditionalByUrl("Patient?active=true")
.execute();
assertEquals("Patient.delete", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("Patient?"));
assertThat(ourLastConditionalUrl, containsString("active=true"));
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalDelete_Observation() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
myClient
.delete()
.resourceConditionalByUrl("Observation?status=final")
.execute();
assertEquals("Observation.delete", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("Observation?"));
assertThat(ourLastConditionalUrl, containsString("status=final"));
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalDelete_Observation_InTransaction() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionDeleteConditionalEntry("Observation?status=final");
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertThat(ourLastBundleRequest.getUrl(), startsWith("Observation?"));
assertThat(ourLastBundleRequest.getUrl(), containsString("status=final"));
assertThat(ourLastBundleRequest.getUrl(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalPatch_Patient() {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
myClient
.patch()
.withFhirPatch(new Parameters())
.conditional(Patient.class)
.whereMap(Map.of("active", List.of("true")))
.execute();
assertEquals("Patient.patch", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("Patient?"));
assertThat(ourLastConditionalUrl, containsString("active=true"));
assertThat(ourLastConditionalUrl, containsString("_id=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalPatch_Observation() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
myClient
.patch()
.withFhirPatch(new Parameters())
.conditional(Observation.class)
.whereMap(Map.of("status", List.of("final")))
.execute();
assertEquals("Observation.patch", ourLastHitMethod);
assertThat(ourLastConditionalUrl, startsWith("Observation?"));
assertThat(ourLastConditionalUrl, containsString("status=final"));
assertThat(ourLastConditionalUrl, containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testNarrowCompartment_ConditionalPatch_Observation_InTransaction() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionFhirPatchEntry(new Parameters()).conditional("Observation?status=final");
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertThat(ourLastBundleRequest.getUrl(), startsWith("Observation?"));
assertThat(ourLastBundleRequest.getUrl(), containsString("status=final"));
assertThat(ourLastBundleRequest.getUrl(), containsString("patient=" + escapeUrlParam("Patient/123,Patient/456")));
}
@Test
public void testTransactionWithNonConditionalDeleteNotModified() {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
BundleBuilder bb = new BundleBuilder(ourCtx);
bb.addTransactionDeleteEntry("Patient", "ABC");
myClient
.transaction()
.withBundle(bb.getBundle())
.execute();
assertEquals("transaction", ourLastHitMethod);
assertEquals("Patient/ABC", ourLastBundleRequest.getUrl());
}
private List<String> toStrings(BaseAndListParam<? extends IQueryParameterOr<?>> theParams) {
List<? extends IQueryParameterOr<? extends IQueryParameterType>> valuesAsQueryTokens = theParams.getValuesAsQueryTokens();
@ -572,6 +908,7 @@ public class SearchNarrowingInterceptorTest {
TestUtil.randomizeLocaleAndTimezone();
}
@SuppressWarnings("unused")
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
@ -590,8 +927,37 @@ public class SearchNarrowingInterceptorTest {
return ourReturn;
}
@Create
public MethodOutcome create(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
ourLastHitMethod = "Patient.create";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Patient/123"), true);
}
@Update
public MethodOutcome update(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
ourLastHitMethod = "Patient.update";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Patient/123"), true);
}
@Delete
public MethodOutcome delete(@IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl) {
ourLastHitMethod = "Patient.delete";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Patient/123"), true);
}
@Patch
public MethodOutcome patch(@IdParam IIdType theId, @ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl, PatchTypeEnum thePatchType) {
ourLastHitMethod = "Patient.patch";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Patient/123"), true);
}
}
@SuppressWarnings("unused")
public static class DummyObservationResourceProvider implements IResourceProvider {
@Override
@ -617,6 +983,34 @@ public class SearchNarrowingInterceptorTest {
return ourReturn;
}
@Create
public MethodOutcome create(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
ourLastHitMethod = "Observation.create";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Observation/123"), true);
}
@Update
public MethodOutcome update(@ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl) {
ourLastHitMethod = "Observation.update";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Observation/123"), true);
}
@Delete
public MethodOutcome delete(@IdParam IIdType theId, @ConditionalUrlParam String theConditionalUrl) {
ourLastHitMethod = "Observation.delete";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Observation/123"), true);
}
@Patch
public MethodOutcome patch(@IdParam IIdType theId, @ResourceParam IBaseResource theResource, @ConditionalUrlParam String theConditionalUrl, PatchTypeEnum thePatchType) {
ourLastHitMethod = "Observation.patch";
ourLastConditionalUrl = theConditionalUrl;
return new MethodOutcome(new IdType("Observation/123"), true);
}
}
public static class DummySystemProvider {

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -9,7 +9,7 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<packaging>pom</packaging>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<name>HAPI-FHIR</name>
<description>An open-source implementation of the FHIR specification in Java.</description>
@ -3241,3 +3241,4 @@
</profile>
</profiles>
</project>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>7.1.3-SNAPSHOT</version>
<version>7.1.4-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>