Enable search narrowing on large ValueSets (#3405)

* Enable search narrowing on large ValueSets

* ValueSet improvements

* Work on narrowing

* Work on narrowing

* Work on narrowing

* Work on narrowing

* Add test

* Work on narrowing interceptor

* Work on narrowing

* License headers

* Refactor code narrowing

* Add docs

* Version bump

* Test fix

* Test fixes

* Build fix

* Fixes

* Version bump

* Test fix

* License header updates

* Build fix

* Test fixes

* Test fix

* Test fix

* Docs fix

* Test fix

* Test fix

* Resolve fixme

* Bump

* Force a CI build

* Make CI happen again
This commit is contained in:
James Agnew 2022-03-01 09:16:31 -05:00 committed by GitHub
parent 68c1015552
commit 2cba62b4e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
126 changed files with 2562 additions and 650 deletions

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -24,6 +24,7 @@ import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.util.ParametersUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.apache.commons.lang3.Validate;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
@ -84,7 +85,7 @@ public interface IValidationSupport {
*
* @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to
* other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter.
* @param theExpansionOptions If provided (may be <code>null</code>), contains options controlling the expansion
* @param theExpansionOptions If provided (can be <code>null</code>), contains options controlling the expansion
* @param theValueSetToExpand The valueset that should be expanded
* @return The expansion, or null
*/
@ -93,6 +94,27 @@ public interface IValidationSupport {
return null;
}
/**
* Expands the given portion of a ValueSet by canonical URL.
*
* @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to
* other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter.
* @param theExpansionOptions If provided (can be <code>null</code>), contains options controlling the expansion
* @param theValueSetUrlToExpand The valueset that should be expanded
* @return The expansion, or null
* @throws ResourceNotFoundException If no ValueSet can be found with the given URL
* @since 6.0.0
*/
@Nullable
default ValueSetExpansionOutcome expandValueSet(ValidationSupportContext theValidationSupportContext, @Nullable ValueSetExpansionOptions theExpansionOptions, @Nonnull String theValueSetUrlToExpand) throws ResourceNotFoundException {
Validate.notBlank(theValueSetUrlToExpand, "theValueSetUrlToExpand must not be null or blank");
IBaseResource valueSet = fetchValueSet(theValueSetUrlToExpand);
if (valueSet == null) {
throw new ResourceNotFoundException(Msg.code(2024) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(theValueSetUrlToExpand));
}
return expandValueSet(theValidationSupportContext, theExpansionOptions, valueSet);
}
/**
* Load and return all conformance resources associated with this
* validation support module. This method may return null if it doesn't
@ -225,7 +247,7 @@ public interface IValidationSupport {
}
/**
* Fetch the given ValueSet by URL
* Fetch the given ValueSet by URL, or returns null if one can't be found for the given URL
*/
@Nullable
default IBaseResource fetchValueSet(String theValueSetUrl) {
@ -235,7 +257,7 @@ public interface IValidationSupport {
/**
* Validates that the given code exists and if possible returns a display
* name. This method is called to check codes which are found in "example"
* binding fields (e.g. <code>Observation.code</code> in the default profile.
* binding fields (e.g. <code>Observation.code</code>) in the default profile.
*
* @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to
* other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter.
@ -253,7 +275,7 @@ public interface IValidationSupport {
/**
* Validates that the given code exists and if possible returns a display
* name. This method is called to check codes which are found in "example"
* binding fields (e.g. <code>Observation.code</code> in the default profile.
* binding fields (e.g. <code>Observation.code</code>) in the default profile.
*
* @param theValidationSupportContext The validation support module will be passed in to this method. This is convenient in cases where the operation needs to make calls to
* other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter.
@ -275,7 +297,7 @@ public interface IValidationSupport {
* other method in the support chain, so that they can be passed through the entire chain. Implementations of this interface may always safely ignore this parameter.
* @param theSystem The CodeSystem URL
* @param theCode The code
* @param theDisplayLanguage to filter out the designation by the display language, to return all designation, the this value to null
* @param theDisplayLanguage to filter out the designation by the display language. To return all designation, set this value to <code>null</code>.
*/
@Nullable
default LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode, String theDisplayLanguage) {
@ -294,7 +316,7 @@ public interface IValidationSupport {
default LookupCodeResult lookupCode(ValidationSupportContext theValidationSupportContext, String theSystem, String theCode) {
return lookupCode(theValidationSupportContext, theSystem, theCode, null);
}
/**
* Returns <code>true</code> if the given valueset can be validated by the given
* validation support module
@ -839,5 +861,4 @@ public interface IValidationSupport {
}
}

View File

@ -25,7 +25,7 @@ public final class Msg {
/**
* IMPORTANT: Please update the following comment after you add a new code
* Last code value: 2065
* Last code value: 2068
*/
private Msg() {}

View File

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

View File

@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-bom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<packaging>pom</packaging>
<name>HAPI FHIR BOM</name>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -20,6 +20,8 @@ package ca.uhn.hapi.fhir.docs;
* #L%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.HookParams;
import ca.uhn.fhir.interceptor.api.IInterceptorBroadcaster;
@ -32,8 +34,10 @@ import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.auth.*;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.collect.Lists;
import org.hl7.fhir.dstu3.model.IdType;
@ -55,28 +59,28 @@ public class AuthorizationInterceptors {
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
public MethodOutcome create(@ResourceParam Patient thePatient, RequestDetails theRequestDetails) {
return new MethodOutcome(); // populate this
}
}
//START SNIPPET: patientAndAdmin
@SuppressWarnings("ConstantConditions")
public class PatientAndAdminAuthorizationInterceptor extends AuthorizationInterceptor {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
// Process authorization header - The following is a fake
// Process authorization header - The following is a fake
// implementation. Obviously we'd want something more real
// for a production scenario.
//
//
// In this basic example we have two hardcoded bearer tokens,
// one which is for a user that has access to one patient, and
// another that has full access.
// another that has full access.
IdType userIdPatientId = null;
boolean userIsAdmin = false;
String authHeader = theRequestDetails.getHeader("Authorization");
@ -109,8 +113,8 @@ public class AuthorizationInterceptors {
.allowAll()
.build();
}
// By default, deny everything. This should never get hit, but it's
// By default, deny everything. This should never get hit, but it's
// good to be defensive
return new RuleBuilder()
.denyAll()
@ -119,27 +123,27 @@ public class AuthorizationInterceptors {
}
//END SNIPPET: patientAndAdmin
//START SNIPPET: conditionalUpdate
@Update()
public MethodOutcome update(
@IdParam IdType theId,
@ResourceParam Patient theResource,
@ConditionalUrlParam String theConditionalUrl,
@ResourceParam Patient theResource,
@ConditionalUrlParam String theConditionalUrl,
ServletRequestDetails theRequestDetails,
IInterceptorBroadcaster theInterceptorBroadcaster) {
// If we're processing a conditional URL...
if (isNotBlank(theConditionalUrl)) {
// Pretend we've done the conditional processing. Now let's
// notify the interceptors that an update has been performed
// and supply the actual ID that's being updated
IdType actual = new IdType("Patient", "1123");
}
// In a real server, perhaps we would process the conditional
// In a real server, perhaps we would process the conditional
// request differently and follow a separate path. Either way,
// let's pretend there is some storage code here.
theResource.setId(theId.withVersion("2"));
@ -264,6 +268,35 @@ public class AuthorizationInterceptors {
}
//END SNIPPET: narrowing
@SuppressWarnings("SpellCheckingInspection")
public void rsNarrowing() {
RestfulServer restfulServer = new RestfulServer();
//START SNIPPET: rsnarrowing
SearchNarrowingInterceptor narrowingInterceptor = new SearchNarrowingInterceptor() {
@Override
protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) {
// Your rules go here
return new AuthorizedList()
.addCodeInValueSet("Observation", "code", "http://hl7.org/fhir/ValueSet/observation-vitalsignresult");
}
};
restfulServer.registerInterceptor(narrowingInterceptor);
// Create a consent service for search narrowing
IValidationSupport validationSupport = null; // This needs to be populated
FhirContext searchParamRegistry = null; // This needs to be populated
SearchNarrowingConsentService consentService = new SearchNarrowingConsentService(validationSupport, searchParamRegistry);
// Create a ConsentIntereptor to apply the ConsentService and register it with the server
ConsentInterceptor consentInterceptor = new ConsentInterceptor();
consentInterceptor.registerConsentService(consentService);
restfulServer.registerInterceptor(consentInterceptor);
//END SNIPPET: rsnarrowing
}
//START SNIPPET: narrowingByCode
public class MyCodeSearchNarrowingInterceptor extends SearchNarrowingInterceptor {

View File

@ -56,6 +56,7 @@ import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.StructureDefinition;
import org.hl7.fhir.r4.model.ValueSet;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FileReader;
@ -294,7 +295,7 @@ public class ValidatorExamples {
}
@Override
public CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
public CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
// TODO: implement (or return null if your implementation does not support this function)
return null;
}

View File

@ -0,0 +1,7 @@
---
type: add
issue: 3405
title: "A consent service implementation that enforces search narrowing rules specified by the SearchNarrowingInterceptor
has been added. This can be used to narrow results from searches in cases where this can't be done using search URL
manipulation. See [ResultSet Narrowing](/hapi-fhir/docs/security/search_narrowing_interceptor.html#resultset-narrowing) for
more information."

View File

@ -0,0 +1,5 @@
---
type: add
issue: 3405
title: "It is now possible to register multiple `IConsentService` implementations against a single
ConsentInterceptor."

View File

@ -0,0 +1,5 @@
---
type: add
issue: 3405
title: "When validating a code, a helpful error message is now returned if the user has supplied a
ValueSet URL where a CodeSystem URL should be used, since this is a common mistake."

View File

@ -25,6 +25,8 @@ An example of this interceptor follows:
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowing}}
```
<a name="constraining-by-valueset-membership"/>
# Constraining by ValueSet Membership
SearchNarrowingInterceptor can also be used to narrow searches by automatically appending `token:in` and `token:not-in` parameters.
@ -34,6 +36,36 @@ In the example below, searches are narrowed as shown below:
* Searches for http://localhost:8000/Observation become http://localhost:8000/Observation?code:in=http://hl7.org/fhir/ValueSet/observation-vitalsignresult
* Searches for http://localhost:8000/Encounter become http://localhost:8000/Encounter?class:not-in=http://my-forbidden-encounter-classes
Important note: ValueSet Membership rules are only applied in cases where the ValueSet expansion has a code count below a configurable threshold (default is 500). To narrow searches with a larger ValueSet expansion, it is necessary to also enable [ResultSet Narrowing](#resultset-narrowing).
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|narrowingByCode}}
```
<a name="resultset-narrowing"/>
# ResultSet Narrowing
<div class="helpInfoCalloutBox">
ResultSet narrowing currently applies only to <a href="#constraining-by-valueset-membership">Constraining by ValueSet
Membership</a>. ResultSet narrowing may be added for other types of search narrowing (e.g. by compartment) in a future
release.
</div>
By default, narrowing will simply modify search parameters in order to automatically constrain the results that are returned to the client. This is helpful for situations where the resource type you are trying to filter is the primary type of the search, but is less helpful when it is not.
For example suppose you wanted to narrow searches for Observations to only include Observations with a code in `http://my-value-set`. When a search is performed for `Observation?subject=Patient/123` the SearchNarrowingInterceptor will typically modify this to be performed as `Observation?subject=Patient/123&code:in=http://my-value-set`.
However this is not always possible:
* If the ValueSet expansion is too large, it is inefficient to use it in an `:in` clause and the SearchNarrowingInterceptor will not do so.
* If the result in question is fetched through an `_include` or `_revinclude` parameter, it is not possible to filter it by adding URL parameters.
* If the result in question is being returned as a result of an operation (e.g. `Patient/[id]/$expand`), it is not possible to filter it by adding URL parameters.
To enable ResultSet narrowing, the SearchNarrowingInterceptor is used along with the ConsentInterceptor, and the ConsentInterceptor is configured to include a companion consent service implementation that works with search narrowing rules. This is shown in the following example:
```java
{{snippet:classpath:/ca/uhn/hapi/fhir/docs/AuthorizationInterceptors.java|rsnarrowing}}
```

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

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

View File

@ -131,8 +131,7 @@ public class JpaPersistedResourceValidationSupport implements IValidationSupport
public IBaseResource fetchValueSet(String theSystem) {
if (TermReadSvcUtil.isLoincUnversionedValueSet(theSystem)) {
Optional<IBaseResource> currentVSOpt = getValueSetCurrentVersion(new UriType(theSystem));
return currentVSOpt.orElseThrow(() -> new ResourceNotFoundException(
"Unable to find current version of ValueSet for url: " + theSystem));
return currentVSOpt.orElse(null);
}
return fetchResource(myValueSetType, theSystem);

View File

@ -21,6 +21,7 @@ package ca.uhn.fhir.jpa.dao.data;
*/
import ca.uhn.fhir.jpa.entity.TermValueSetConcept;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
@ -38,6 +39,9 @@ public interface ITermValueSetConceptDao extends JpaRepository<TermValueSetConce
@Modifying
void deleteByTermValueSetId(@Param("pid") Long theValueSetId);
@Query("SELECT vsc FROM TermValueSetConcept vsc WHERE vsc.myValueSetPid = :pid AND vsc.mySystem = :system_url")
List<TermValueSetConcept> findByTermValueSetIdSystemOnly(Pageable thePage, @Param("pid") Long theValueSetId, @Param("system_url") String theSystem);
@Query("SELECT vsc FROM TermValueSetConcept vsc WHERE vsc.myValueSetPid = :pid AND vsc.mySystem = :system_url AND vsc.myCode = :codeval")
Optional<TermValueSetConcept> findByTermValueSetIdSystemAndCode(@Param("pid") Long theValueSetId, @Param("system_url") String theSystem, @Param("codeval") String theCode);

View File

@ -147,27 +147,26 @@ public class ValueSetOperationProvider extends BaseJpaProvider {
IFhirResourceDaoValueSet<IBaseResource, ICompositeType, ICompositeType> dao = getDao();
IBaseResource valueSet = theValueSet;
IValidationSupport.ValueSetExpansionOutcome outcome;
if (haveId) {
valueSet = dao.read(theId, theRequestDetails);
IBaseResource valueSet = dao.read(theId, theRequestDetails);
outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, valueSet);
} else if (haveIdentifier) {
String url;
if (haveValueSetVersion) {
url = theUrl.getValue() + "|" + theValueSetVersion.getValue();
valueSet = myValidationSupport.fetchValueSet(url);
} else {
url = theUrl.getValue();
valueSet = myValidationSupport.fetchValueSet(url);
}
if (valueSet == null) {
throw new ResourceNotFoundException(Msg.code(2030) + "Can not find ValueSet with URL: " + UrlUtil.escapeUrlParam(url));
}
outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, url);
} else {
outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, theValueSet);
}
IValidationSupport.ValueSetExpansionOutcome outcome = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), options, valueSet);
if (outcome == null) {
throw new InternalErrorException(Msg.code(2028) + outcome.getError());
throw new InternalErrorException(Msg.code(2028) + "No validation support module was able to expand the given valueset");
}
if (outcome.getError() != null) {
throw new PreconditionFailedException(Msg.code(2029) + outcome.getError());
}

View File

@ -40,9 +40,6 @@ import ca.uhn.fhir.jpa.model.entity.ResourceHistoryTable;
import ca.uhn.fhir.jpa.partition.RequestPartitionHelperSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
import ca.uhn.fhir.jpa.util.InterceptorUtil;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import ca.uhn.fhir.jpa.util.MemoryCacheService;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
@ -52,10 +49,11 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceAccessDetails;
import ca.uhn.fhir.rest.api.server.SimplePreResourceShowDetails;
import ca.uhn.fhir.rest.api.server.storage.ResourcePersistentId;
import ca.uhn.fhir.rest.server.interceptor.ServerInterceptorUtil;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import com.google.common.annotations.VisibleForTesting;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@ -70,12 +68,10 @@ import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
public class PersistedJpaBundleProvider implements IBundleProvider {
@ -435,7 +431,7 @@ public class PersistedJpaBundleProvider implements IBundleProvider {
List<IBaseResource> resources = new ArrayList<>();
theSearchBuilder.loadResourcesByPid(thePids, includedPidList, resources, false, myRequest);
resources = InterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster);
resources = ServerInterceptorUtil.fireStoragePreshowResource(resources, myRequest, myInterceptorBroadcaster);
return resources;
}

View File

@ -47,7 +47,7 @@ import ca.uhn.fhir.jpa.search.cache.ISearchCacheSvc;
import ca.uhn.fhir.jpa.search.cache.ISearchResultCacheSvc;
import ca.uhn.fhir.jpa.search.cache.SearchCacheStatusEnum;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.jpa.util.InterceptorUtil;
import ca.uhn.fhir.rest.server.interceptor.ServerInterceptorUtil;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.api.CacheControlDirective;
@ -606,7 +606,7 @@ public class SearchCoordinatorSvcImpl implements ISearchCoordinatorSvc {
List<IBaseResource> resources = new ArrayList<>();
theSb.loadResourcesByPid(pids, includedPidsList, resources, false, theRequestDetails);
// Hook: STORAGE_PRESHOW_RESOURCES
resources = InterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster);
resources = ServerInterceptorUtil.fireStoragePreshowResource(resources, theRequestDetails, myInterceptorBroadcaster);
SimpleBundleProvider bundleProvider = new SimpleBundleProvider(resources);
if (theParams.isOffsetQuery()) {

View File

@ -180,11 +180,7 @@ public class TokenPredicateBuilder extends BaseSearchParamPredicateBuilder {
if (modifier == TokenParamModifier.IN || modifier == TokenParamModifier.NOT_IN) {
if (myContext.getVersion().getVersion().isNewerThan(FhirVersionEnum.DSTU2)) {
IBaseResource valueSet = myValidationSupport.fetchValueSet(code);
if (valueSet == null) {
throw new ResourceNotFoundException(Msg.code(2024) + "Unknown ValueSet: " + UrlUtil.escapeUrlParam(code));
}
IValidationSupport.ValueSetExpansionOutcome expanded = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), valueSet);
IValidationSupport.ValueSetExpansionOutcome expanded = myValidationSupport.expandValueSet(new ValidationSupportContext(myValidationSupport), new ValueSetExpansionOptions(), code);
codes.addAll(extractValueSetCodes(expanded.getValueSet()));
} else {
codes.addAll(myTerminologySvc.expandValueSetIntoConceptList(null, code));

View File

@ -1616,7 +1616,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
}
TermValueSet valueSetEntity = myTermValueSetDao.findByResourcePid(valueSetResourcePid.getIdAsLong()).orElseThrow(() -> new IllegalStateException());
Object timingDescription = toHumanReadableExpansionTimestamp(valueSetEntity);
String timingDescription = toHumanReadableExpansionTimestamp(valueSetEntity);
String msg = myContext.getLocalizer().getMessage(BaseTermReadSvcImpl.class, "validationPerformedAgainstPreExpansion", timingDescription);
if (theValidationOptions.isValidateDisplay() && concepts.size() > 0) {
@ -1642,8 +1642,17 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
.setCodeSystemVersion(concepts.get(0).getSystemVersion())
.setMessage(msg);
}
// Ok, we failed
List<TermValueSetConcept> outcome = myValueSetConceptDao.findByTermValueSetIdSystemOnly(Pageable.ofSize(1), valueSetEntity.getId(), theSystem);
String append;
if (outcome.size() == 0) {
append = " - No codes in ValueSet belong to CodeSystem with URL " + theSystem;
} else {
append = " - Unknown code " + theSystem + "#" + theCode + ". " + msg;
}
return createFailureCodeValidationResult(theSystem, theCode, null, " - Unknown code " + theSystem + "#" + theCode + ". " + msg);
return createFailureCodeValidationResult(theSystem, theCode, null, append);
}
private CodeValidationResult createFailureCodeValidationResult(String theSystem, String theCode, String theCodeSystemVersion, String theAppend) {
@ -2207,7 +2216,7 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
@CoverageIgnore
@Override
public IValidationSupport.CodeValidationResult validateCode(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
public IValidationSupport.CodeValidationResult validateCode(@Nonnull ValidationSupportContext theValidationSupportContext, @Nonnull ConceptValidationOptions theOptions, String theCodeSystem, String theCode, String theDisplay, String theValueSetUrl) {
//TODO GGG TRY TO JUST AUTO_PASS HERE AND SEE WHAT HAPPENS.
invokeRunnableForUnitTest();
@ -2235,24 +2244,41 @@ public abstract class BaseTermReadSvcImpl implements ITermReadSvc {
IValidationSupport.CodeValidationResult validateCodeInValueSet(ValidationSupportContext theValidationSupportContext, ConceptValidationOptions theValidationOptions, String theValueSetUrl, String theCodeSystem, String theCode, String theDisplay) {
IBaseResource valueSet = theValidationSupportContext.getRootValidationSupport().fetchValueSet(theValueSetUrl);
CodeValidationResult retVal = null;
// If we don't have a PID, this came from some source other than the JPA
// database, so we don't need to check if it's pre-expanded or not
if (valueSet instanceof IAnyResource) {
Long pid = IDao.RESOURCE_PID.get((IAnyResource) valueSet);
if (pid != null) {
if (isValueSetPreExpandedForCodeValidation(valueSet)) {
return validateCodeIsInPreExpandedValueSet(theValidationOptions, valueSet, theCodeSystem, theCode, theDisplay, null, null);
}
TransactionTemplate txTemplate = new TransactionTemplate(myTxManager);
retVal = txTemplate.execute(tx -> {
if (isValueSetPreExpandedForCodeValidation(valueSet)) {
return validateCodeIsInPreExpandedValueSet(theValidationOptions, valueSet, theCodeSystem, theCode, theDisplay, null, null);
} else {
return null;
}
});
}
}
CodeValidationResult retVal;
if (valueSet != null) {
retVal = new InMemoryTerminologyServerValidationSupport(myContext).validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, valueSet);
} else {
String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]";
retVal = createFailureCodeValidationResult(theCodeSystem, theCode, null, append);
if (retVal == null) {
if (valueSet != null) {
retVal = new InMemoryTerminologyServerValidationSupport(myContext).validateCodeInValueSet(theValidationSupportContext, theValidationOptions, theCodeSystem, theCode, theDisplay, valueSet);
} else {
String append = " - Unable to locate ValueSet[" + theValueSetUrl + "]";
retVal = createFailureCodeValidationResult(theCodeSystem, theCode, null, append);
}
}
// Check if someone is accidentally using a VS url where it should be a CS URL
if (retVal != null && retVal.getCode() == null && theCodeSystem != null) {
if (isValueSetSupported(theValidationSupportContext, theCodeSystem)) {
if (!isCodeSystemSupported(theValidationSupportContext, theCodeSystem)) {
String newMessage = "Unable to validate code " + theCodeSystem + "#" + theCode + " - Supplied system URL is a ValueSet URL and not a CodeSystem URL, check if it is correct: " + theCodeSystem;
retVal.setMessage(newMessage);
}
}
}
return retVal;

View File

@ -42,6 +42,7 @@ import org.springframework.test.util.ReflectionTestUtils;
import java.util.function.Function;
import static org.hl7.fhir.common.hapi.validation.support.ValidationConstants.LOINC_LOW;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
@ -103,11 +104,7 @@ class JpaPersistedResourceValidationSupportTest {
@Test
void fetchValueSetMustUseForcedId() {
final String valueSetId = "string-containing-loinc";
ResourceNotFoundException thrown = assertThrows(
ResourceNotFoundException.class,
() -> testedClass.fetchValueSet(valueSetId));
assertTrue(thrown.getMessage().contains("Unable to find current version of ValueSet for url: " + valueSetId));
assertNull(testedClass.fetchValueSet(valueSetId));
}

View File

@ -11,6 +11,8 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -44,11 +46,11 @@ public class FhirResourceDaoR4InlineResourceModeTest extends BaseJpaR4Test {
// Version 1
ResourceHistoryTable entity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resourceId, 1);
assertNull(entity.getResource());
assertEquals("{\"resourceType\":\"Patient\",\"active\":true}", entity.getResourceTextVc());
assertThat(entity.getResourceTextVc(), containsString("\"active\":true"));
// Version 2
entity = myResourceHistoryTableDao.findForIdAndVersionAndFetchProvenance(resourceId, 2);
assertNull(entity.getResource());
assertEquals("{\"resourceType\":\"Patient\",\"active\":false}", entity.getResourceTextVc());
assertThat(entity.getResourceTextVc(), containsString("\"active\":false"));
});
patient = myPatientDao.read(new IdType("Patient/" + resourceId));

View File

@ -59,6 +59,38 @@ public class FhirResourceDaoR4ValueSetTest extends BaseJpaR4Test {
}
@Test
public void testValidateCodeInValueSet_ValueSetUrlUsedInsteadOfCodeSystem() throws IOException {
myCodeSystemDao.update(loadResourceFromClasspath(CodeSystem.class, "r4/adi-cs.json"));
myValueSetDao.update(loadResourceFromClasspath(ValueSet.class, "r4/adi-vs.json"));
myTermSvc.preExpandDeferredValueSetsToTerminologyTables();
ValidationSupportContext context = new ValidationSupportContext(myValidationSupport);
ConceptValidationOptions options = new ConceptValidationOptions();
IValidationSupport.CodeValidationResult outcome = myValidationSupport.validateCode(context, options, "http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc", "378397893", null, "http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc");
assertFalse(outcome.isOk());
assertEquals("Unable to validate code http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc#378397893 - Supplied system URL is a ValueSet URL and not a CodeSystem URL, check if it is correct: http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc", outcome.getMessage());
}
@Test
public void testValidateCodeInValueSet_SystemThatAppearsNowhereInValueSet() throws IOException {
myCodeSystemDao.update(loadResourceFromClasspath(CodeSystem.class, "r4/adi-cs.json"));
myValueSetDao.update(loadResourceFromClasspath(ValueSet.class, "r4/adi-vs.json"));
myTermSvc.preExpandDeferredValueSetsToTerminologyTables();
logAllValueSetConcepts();
ValidationSupportContext context = new ValidationSupportContext(myValidationSupport);
ConceptValidationOptions options = new ConceptValidationOptions();
IValidationSupport.CodeValidationResult outcome = myValidationSupport.validateCode(context, options, "http://payer-to-payer-exchange/fhir/CodeSystem/ndc", "378397893", null, "http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc");
assertFalse(outcome.isOk());
assertEquals("Unable to validate code http://payer-to-payer-exchange/fhir/CodeSystem/ndc#378397893 - No codes in ValueSet belong to CodeSystem with URL http://payer-to-payer-exchange/fhir/CodeSystem/ndc", outcome.getMessage());
}
@Test
public void testValidateCodeOperationNoValueSet() {
UriType valueSetIdentifier = null;

View File

@ -1,10 +1,7 @@
package ca.uhn.fhir.jpa.dao.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.config.PartitionSettings;
import ca.uhn.fhir.jpa.model.entity.BaseResourceIndexedSearchParam;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
@ -17,10 +14,8 @@ import ca.uhn.fhir.jpa.model.util.UcumServiceUtil;
import ca.uhn.fhir.jpa.searchparam.extractor.ISearchParamExtractor;
import ca.uhn.fhir.jpa.searchparam.extractor.PathAndRef;
import ca.uhn.fhir.jpa.searchparam.extractor.SearchParamExtractorR4;
import ca.uhn.fhir.jpa.searchparam.registry.ISearchParamRegistryController;
import ca.uhn.fhir.jpa.searchparam.registry.ReadOnlySearchParamCache;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
import ca.uhn.fhir.util.HapiExtensions;
import com.google.common.collect.Sets;
import org.hl7.fhir.r4.model.BooleanType;
@ -41,13 +36,9 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -61,13 +52,13 @@ public class SearchParamExtractorR4Test {
private static final Logger ourLog = LoggerFactory.getLogger(SearchParamExtractorR4Test.class);
private static final FhirContext ourCtx = FhirContext.forR4Cached();
private MySearchParamRegistry mySearchParamRegistry;
private FhirContextSearchParamRegistry mySearchParamRegistry;
private PartitionSettings myPartitionSettings;
@BeforeEach
public void before() {
mySearchParamRegistry = new MySearchParamRegistry();
mySearchParamRegistry = new FhirContextSearchParamRegistry(ourCtx);
myPartitionSettings = new PartitionSettings();
}
@ -386,75 +377,4 @@ public class SearchParamExtractorR4Test {
}
private static class MySearchParamRegistry implements ISearchParamRegistry, ISearchParamRegistryController {
private final List<RuntimeSearchParam> myExtraSearchParams = new ArrayList<>();
@Override
public void forceRefresh() {
// nothing
}
@Override
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
return getActiveSearchParams(theResourceName).get(theParamName);
}
@Override
public ResourceChangeResult refreshCacheIfNecessary() {
// nothing
return new ResourceChangeResult();
}
public ReadOnlySearchParamCache getActiveSearchParams() {
throw new UnsupportedOperationException();
}
@Override
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
Map<String, RuntimeSearchParam> sps = new HashMap<>();
RuntimeResourceDefinition nextResDef = ourCtx.getResourceDefinition(theResourceName);
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
sps.put(nextSp.getName(), nextSp);
}
for (RuntimeSearchParam next : myExtraSearchParams) {
sps.put(next.getName(), next);
}
return sps;
}
public void addSearchParam(RuntimeSearchParam theSearchParam) {
myExtraSearchParams.add(theSearchParam);
}
@Override
public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) {
throw new UnsupportedOperationException();
}
@Nullable
@Override
public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) {
throw new UnsupportedOperationException();
}
@Override
public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName) {
throw new UnsupportedOperationException();
}
@Override
public void requestRefresh() {
// nothing
}
@Override
public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
// nothing
}
}
}

View File

@ -443,7 +443,7 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}
@ -486,7 +486,7 @@ public class ResourceProviderDstu3ValueSetTest extends BaseResourceProviderDstu3
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}

View File

@ -574,7 +574,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
}
}
@ -655,7 +655,7 @@ public class ResourceProviderDstu3ValueSetVersionedTest extends BaseResourceProv
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
}
}

View File

@ -6,6 +6,7 @@ import ca.uhn.fhir.jpa.bulk.export.model.BulkExportJobStatusEnum;
import ca.uhn.fhir.jpa.bulk.export.provider.BulkDataExportProvider;
import ca.uhn.fhir.jpa.dao.r4.FhirResourceDaoR4TerminologyTest;
import ca.uhn.fhir.jpa.interceptor.CascadingDeleteInterceptor;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
@ -51,6 +52,7 @@ import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Practitioner;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -264,11 +266,9 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
}
@Test
public void testBulkExport_AuthorizeAny() {
@ -309,7 +309,6 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
}
}
@Test
@ -698,13 +697,29 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
// Should be ok
myClient.read().resource(Observation.class).withId("Observation/allowed").execute();
try {
myClient.read().resource(Observation.class).withId("Observation/disallowed").execute();
fail();
} catch (ForbiddenOperationException e) {
// good
}
}
@Test
public void testReadCodeIn_AllowedInCompartment() throws IOException {
myValueSetDao.update(loadResourceFromClasspath(ValueSet.class, "r4/adi-vs2.json"));
myTermSvc.preExpandDeferredValueSetsToTerminologyTables();
logAllValueSetConcepts();
mySystemDao.transaction(mySrd, loadResourceFromClasspath(Bundle.class, "r4/adi-ptbundle.json"));
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.deny().read().resourcesOfType("Observation").withCodeNotInValueSet("code", "http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc-canonical-valueset").andThen()
.allow().read().allResources().inCompartment("Patient", new IdType("Patient/P")).andThen()
.build();
}
}.setValidationSupport(myValidationSupport));
// Should be ok
myClient.read().resource(Patient.class).withId("Patient/P").execute();
myClient.read().resource(Observation.class).withId("Observation/O").execute();
}
/**
@ -1215,7 +1230,7 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
p.setId("123");
request.addEntry().setResource(p).getRequest().setMethod(Bundle.HTTPVerb.POST)
.setUrl(
"Patient/"+
"Patient/" +
p.getId());
Observation o = new Observation();
@ -1381,6 +1396,61 @@ public class AuthorizationInterceptorJpaR4Test extends BaseResourceProviderR4Tes
ourLog.info(myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(resp));
}
@Test
public void testOperationEverything_SomeIncludedResourcesNotAuthorized() {
Patient pt1 = new Patient();
pt1.setActive(true);
final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless();
Observation obs1 = new Observation();
obs1.setStatus(ObservationStatus.FINAL);
obs1.setSubject(new Reference(pid1));
myClient.create().resource(obs1).execute();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().operation().named(JpaConstants.OPERATION_EVERYTHING).onInstance(pid1).andRequireExplicitResponseAuthorization().andThen()
.allow().read().resourcesOfType(Patient.class).inCompartment("Patient", pid1).andThen()
.allow().read().resourcesOfType(Observation.class).inCompartment("Patient", pid1).andThen()
.allow().create().resourcesOfType(Encounter.class).withAnyId().andThen()
.build();
}
});
Bundle outcome = myClient
.operation()
.onInstance(pid1)
.named(JpaConstants.OPERATION_EVERYTHING)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
assertEquals(2, outcome.getEntry().size());
// Add an Encounter, which will be returned by $everything but that hasn't been
// explicitly authorized
Encounter enc = new Encounter();
enc.setSubject(new Reference(pid1));
myClient.create().resource(enc).execute();
try {
outcome = myClient
.operation()
.onInstance(pid1)
.named(JpaConstants.OPERATION_EVERYTHING)
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
fail();
} catch (ForbiddenOperationException e) {
assertThat(e.getMessage(), containsString("Access denied by default policy"));
}
}
@Test
public void testPatchWithinCompartment() {
Patient pt1 = new Patient();

View File

@ -445,7 +445,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}
@ -493,7 +493,7 @@ public class ResourceProviderR4ValueSetNoVerCSNoVerTest extends BaseResourceProv
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}

View File

@ -357,7 +357,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}
@ -405,7 +405,7 @@ public class ResourceProviderR4ValueSetVerCSNoVerTest extends BaseResourceProvid
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}

View File

@ -525,7 +525,7 @@ public class ResourceProviderR4ValueSetVerCSVerTest extends BaseResourceProvider
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
}
}
@ -609,7 +609,7 @@ public class ResourceProviderR4ValueSetVerCSVerTest extends BaseResourceProvider
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
}
}

View File

@ -485,7 +485,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test {
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}
@ -622,7 +622,7 @@ public class ResourceProviderR5ValueSetTest extends BaseResourceProviderR5Test {
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fbogus", e.getMessage());
}
}

View File

@ -558,7 +558,7 @@ public class ResourceProviderR5ValueSetVersionedTest extends BaseResourceProvide
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
}
}
@ -640,7 +640,7 @@ public class ResourceProviderR5ValueSetVersionedTest extends BaseResourceProvide
.execute();
} catch (ResourceNotFoundException e) {
assertEquals(404, e.getStatusCode());
assertEquals("HTTP 404 Not Found: HAPI-2030: Can not find ValueSet with URL: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
assertEquals("HTTP 404 Not Found: HAPI-2024: Unknown ValueSet: http%3A%2F%2Fwww.healthintersections.com.au%2Ffhir%2FValueSet%2Fextensional-case-2%7C3", e.getMessage());
}
}

View File

@ -0,0 +1,70 @@
{
"resourceType": "CodeSystem",
"id": "cs",
"meta": {
"versionId": "1",
"lastUpdated": "2022-02-18T17:49:05.119-05:00"
},
"url": "http://payer-to-payer-exchange/fhir/CodeSystem/ndc",
"identifier": [
{
"system": "https://payer-to-payer-exchange/fhir/identifiers/codesystems",
"value": "internal"
}
],
"version": "1",
"name": "ndc",
"title": "National Drug Codes",
"status": "active",
"experimental": true,
"date": "2022-02-14",
"description": "National Drug Codes for granular consent",
"caseSensitive": true,
"content": "complete",
"concept": [
{
"code": "378351391",
"display": "risperidone-3-mg-tablet",
"definition": "Code for risperidone",
"property": [
{
"code": "consent-type",
"valueString": "mental-health"
}
]
},
{
"code": "378351570",
"display": "mirtazapine-15-mg-tablet",
"definition": "Code for risperidone",
"property": [
{
"code": "consent-type",
"valueString": "mental-health"
}
]
},
{
"code": "378381501",
"display": "clozapine-100-mg-tablet",
"definition": "Code for clozapine",
"property": [
{
"code": "consent-type",
"valueString": "mental-health"
}
]
},
{
"code": "378397893",
"display": "paliperidone-1.5-mg-tablet",
"definition": "Code for paliperidone",
"property": [
{
"code": "consent-type",
"valueString": "mental-health"
}
]
}
]
}

View File

@ -0,0 +1,51 @@
{
"resourceType": "Bundle",
"type": "transaction",
"entry": [
{
"fullUrl": "Patient/P",
"resource": {
"resourceType": "Patient",
"active": true,
"name": [
{
"family": "Gilbert",
"given": [
"Whitley"
]
}
],
"gender": "female",
"birthDate": "1988-04-18"
},
"request": {
"method": "PUT",
"url": "Patient/P"
}
},
{
"fullUrl": "Observation/O",
"resource": {
"resourceType": "Observation",
"status": "final",
"code": {
"coding": [
{
"system": "http://hl7.org/fhir/sid/ndc",
"code": "0008-1222-30",
"display": "Pristiq Extended-Release, 30 TABLET, EXTENDED RELEASE in 1 BOTTLE, PLASTIC (0008-1222-30) (package)"
}
],
"text": "Pristiq Extended-Release, 30 TABLET, EXTENDED RELEASE in 1 BOTTLE, PLASTIC (0008-1222-30) (package)"
},
"subject": {
"reference": "Patient/P"
}
},
"request": {
"method": "PUT",
"url": "Observation/O"
}
}
]
}

View File

@ -0,0 +1,36 @@
{
"resourceType": "ValueSet",
"id": "vs",
"meta": {
"versionId": "1",
"lastUpdated": "2022-02-18T17:49:43.229-05:00"
},
"url": "http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc",
"version": "20220214",
"name": "NDC-Codes-Mental-Health",
"status": "draft",
"experimental": true,
"date": "2022-02-14",
"compose": {
"include": [
{
"system": "http://payer-to-payer-exchange/fhir/CodeSystem/ndc-codes",
"version": "1.0",
"concept": [
{
"code": "378351391",
"display": "risperidone-3-mg-tablet"
},
{
"code": "378351570",
"display": "mirtazapine-15-mg-tablet"
},
{
"code": "378397893",
"display": "paliperidone-1.5-mg-tablet"
}
]
}
]
}
}

View File

@ -0,0 +1,20 @@
{
"resourceType": "ValueSet",
"id": "ndc-vs",
"url": "http://payer-to-payer-exchange/fhir/ValueSet/mental-health/ndc-canonical-valueset",
"version": "20220214",
"name": "NDC-Codes-Mental-Health-Canonical-VS",
"status": "draft",
"experimental": true,
"date": "2022-02-14",
"compose": {
"include": [ {
"system": "http://hl7.org/fhir/sid/ndc",
"version": "1.0",
"concept": [ {
"code": "0008-1222-30",
"display": "Pristiq Extended-Release, 30 TABLET, EXTENDED RELEASE in 1 BOTTLE, PLASTIC (0008-1222-30) (package)"
}]
}]
}
}

View File

@ -7,7 +7,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,8 +1,8 @@
package ca.uhn.fhir.jpa.util;
package ca.uhn.fhir.rest.server.interceptor;
/*-
* #%L
* HAPI FHIR JPA Server
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%
@ -30,15 +30,21 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.rest.server.util.CompositeInterceptorBroadcaster;
import org.hl7.fhir.instance.model.api.IBaseResource;
import javax.annotation.CheckReturnValue;
import java.util.List;
import java.util.Objects;
public class InterceptorUtil {
public class ServerInterceptorUtil {
private ServerInterceptorUtil() {
super();
}
/**
* Fires {@link Pointcut#STORAGE_PRESHOW_RESOURCES} interceptor hook, and potentially remove resources
* from the resource list
*/
@CheckReturnValue
public static List<IBaseResource> fireStoragePreshowResource(List<IBaseResource> theResources, RequestDetails theRequest, IInterceptorBroadcaster theInterceptorBroadcaster) {
List<IBaseResource> retVal = theResources;
retVal.removeIf(Objects::isNull);

View File

@ -0,0 +1,32 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2022 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%
*/
public class AuthorizationConstants {
public static final int ORDER_CONSENT_INTERCEPTOR = 100;
public static final int ORDER_AUTH_INTERCEPTOR = 200;
private AuthorizationConstants() {
super();
}
}

View File

@ -69,7 +69,7 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
*
* @see SearchNarrowingInterceptor
*/
@Interceptor
@Interceptor(order = AuthorizationConstants.ORDER_AUTH_INTERCEPTOR)
public class AuthorizationInterceptor implements IRuleApplier {
public static final String REQUEST_ATTRIBUTE_BULK_DATA_EXPORT_OPTIONS = AuthorizationInterceptor.class.getName() + "_BulkDataExportOptions";

View File

@ -0,0 +1,137 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2022 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%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentContextServices;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import ca.uhn.fhir.rest.server.util.FhirContextSearchParamRegistry;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.util.List;
public class SearchNarrowingConsentService implements IConsentService {
private static final Logger ourLog = LoggerFactory.getLogger(SearchNarrowingConsentService.class);
private final IValidationSupport myValidationSupport;
private final ISearchParamRegistry mySearchParamRegistry;
private Logger myTroubleshootingLog = ourLog;
/**
* Constructor (use this only if no {@link ISearchParamRegistry} is available
*
* @param theValidationSupport The validation support module
*/
public SearchNarrowingConsentService(IValidationSupport theValidationSupport, FhirContext theFhirContext) {
this(theValidationSupport, new FhirContextSearchParamRegistry(theFhirContext));
}
/**
* Constructor
*
* @param theValidationSupport The validation support module
* @param theSearchParamRegistry The search param registry
*/
public SearchNarrowingConsentService(IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry) {
myValidationSupport = theValidationSupport;
mySearchParamRegistry = theSearchParamRegistry;
}
/**
* Provides a log that will be apppended to for troubleshooting messages
*
* @param theTroubleshootingLog The logger (must not be <code>null</code>)
*/
public void setTroubleshootingLog(@Nonnull Logger theTroubleshootingLog) {
Validate.notNull(theTroubleshootingLog, "theTroubleshootingLog must not be null");
myTroubleshootingLog = theTroubleshootingLog;
}
@Override
public boolean shouldProcessCanSeeResource(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
List<AllowedCodeInValueSet> postFilteringList = SearchNarrowingInterceptor.getPostFilteringListOrNull(theRequestDetails);
return postFilteringList != null && !postFilteringList.isEmpty();
}
@Override
public ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return applyFilterForResource(theRequestDetails, theResource);
}
@Override
public ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return applyFilterForResource(theRequestDetails, theResource);
}
@Nonnull
private ConsentOutcome applyFilterForResource(RequestDetails theRequestDetails, IBaseResource theResource) {
List<AllowedCodeInValueSet> postFilteringList = SearchNarrowingInterceptor.getPostFilteringListOrNull(theRequestDetails);
if (postFilteringList == null) {
return ConsentOutcome.PROCEED;
}
String resourceType = myValidationSupport.getFhirContext().getResourceType(theResource);
boolean allPositiveRulesMatched = true;
for (AllowedCodeInValueSet next : postFilteringList) {
if (!next.getResourceName().equals(resourceType)) {
continue;
}
boolean returnOnFirstMatch = true;
String searchParamName = next.getSearchParameterName();
String valueSetUrl = next.getValueSetUrl();
SearchParameterAndValueSetRuleImpl.CodeMatchCount outcome = SearchParameterAndValueSetRuleImpl.countMatchingCodesInValueSetForSearchParameter(theResource, myValidationSupport, mySearchParamRegistry, returnOnFirstMatch, searchParamName, valueSetUrl, myTroubleshootingLog, "Search Narrowing");
if (outcome.isAtLeastOneUnableToValidate()) {
myTroubleshootingLog.warn("Terminology Services failed to validate value from " + next.getResourceName() + ":" + next.getSearchParameterName() + " in ValueSet " + next.getValueSetUrl() + " - Assuming REJECT");
return ConsentOutcome.REJECT;
}
if (next.isNegate()) {
if (outcome.getMatchingCodeCount() > 0) {
return ConsentOutcome.REJECT;
}
} else {
if (outcome.getMatchingCodeCount() == 0) {
allPositiveRulesMatched = false;
break;
}
}
}
if (!allPositiveRulesMatched) {
return ConsentOutcome.REJECT;
}
return ConsentOutcome.PROCEED;
}
}

View File

@ -23,6 +23,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValidationSupportContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.interceptor.api.Hook;
import ca.uhn.fhir.interceptor.api.Pointcut;
@ -38,12 +41,15 @@ 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 org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import javax.annotation.Nullable;
@ -85,6 +91,39 @@ import java.util.stream.Collectors;
*/
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;
/**
* Supplies a threshold over which any ValueSet-based rules will be applied by
*
*
* <p>
* Note that this setting will have no effect if {@link #setValidationSupport(IValidationSupport)}
* has not also been called in order to supply a validation support module for
* testing ValueSet membership.
* </p>
*
* @param thePostFilterLargeValueSetThreshold The threshold
* @see #setValidationSupport(IValidationSupport)
*/
public void setPostFilterLargeValueSetThreshold(int thePostFilterLargeValueSetThreshold) {
Validate.isTrue(thePostFilterLargeValueSetThreshold > 0, "thePostFilterLargeValueSetThreshold must be a positive integer");
myPostFilterLargeValueSetThreshold = thePostFilterLargeValueSetThreshold;
}
/**
* Supplies a validation support module that will be used to apply the
*
* @see #setPostFilterLargeValueSetThreshold(int)
* @since 6.0.0
*/
public SearchNarrowingInterceptor setValidationSupport(IValidationSupport theValidationSupport) {
myValidationSupport = theValidationSupport;
return this;
}
/**
* Subclasses should override this method to supply the set of compartments that
* the user making the request should actually have access to.
@ -103,28 +142,34 @@ public class SearchNarrowingInterceptor {
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_POST_PROCESSED)
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
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);
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
if (authorizedList == null) {
return true;
}
// Add rules to request so that the SearchNarrowingConsentService can pick them up
List<AllowedCodeInValueSet> postFilteringList = getPostFilteringList(theRequestDetails);
if (authorizedList.getAllowedCodeInValueSets() != null) {
postFilteringList.addAll(authorizedList.getAllowedCodeInValueSets());
}
if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_TYPE) {
return true;
}
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName());
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
if (authorizedList == null) {
return true;
}
/*
* Create a map of search parameter values that need to be added to the
* given request
*/
Collection<String> compartments = authorizedList.getAllowedCompartments();
if (compartments != null) {
Map<String, List<String>> parameterToOrValues = processResourcesOrCompartments(theRequestDetails, resDef, compartments, true);
Map<String, List<String>> parameterToOrValues = processResourcesOrCompartments(theRequestDetails, resDef, compartments, true);
applyParametersToRequestDetails(theRequestDetails, parameterToOrValues, true);
}
Collection<String> resources = authorizedList.getAllowedInstances();
@ -141,6 +186,18 @@ public class SearchNarrowingInterceptor {
return true;
}
@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, boolean thePatientIdMode) {
if (theParameterToOrValues != null) {
Map<String, String[]> newParameters = new HashMap<>(theRequestDetails.getParameters());
@ -215,18 +272,6 @@ public class SearchNarrowingInterceptor {
}
}
@Hook(Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void incomingRequestPreHandled(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);
}
@Nullable
private Map<String, List<String>> processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, Collection<String> theResourcesOrCompartments, boolean theAreCompartments) {
Map<String, List<String>> retVal = null;
@ -276,7 +321,17 @@ public class SearchNarrowingInterceptor {
Map<String, List<String>> retVal = null;
for (AllowedCodeInValueSet next : theAllowedCodeInValueSet) {
if (!next.getResourceName().equals(theResDef.getName())) {
String resourceName = next.getResourceName();
String valueSetUrl = next.getValueSetUrl();
ValidateUtil.isNotBlankOrThrowIllegalArgument(resourceName, "Resource name supplied by SearchNarrowingInterceptor must not be null");
ValidateUtil.isNotBlankOrThrowIllegalArgument(valueSetUrl, "ValueSet URL supplied by SearchNarrowingInterceptor must not be null");
if (!resourceName.equals(theResDef.getName())) {
continue;
}
if (shouldHandleThroughConsentService(valueSetUrl)) {
continue;
}
@ -290,12 +345,32 @@ public class SearchNarrowingInterceptor {
if (retVal == null) {
retVal = new HashMap<>();
}
retVal.computeIfAbsent(paramName, k->new ArrayList<>()).add(next.getValueSetUrl());
retVal.computeIfAbsent(paramName, k -> new ArrayList<>()).add(valueSetUrl);
}
return retVal;
}
/**
* For a given ValueSet URL, expand the valueset and check if the number of
* codes present is larger than the post filter threshold.
*/
private boolean shouldHandleThroughConsentService(String theValueSetUrl) {
if (myValidationSupport != null && myPostFilterLargeValueSetThreshold != -1) {
ValidationSupportContext ctx = new ValidationSupportContext(myValidationSupport);
ValueSetExpansionOptions options = new ValueSetExpansionOptions();
options.setCount(myPostFilterLargeValueSetThreshold);
options.setIncludeHierarchy(false);
IValidationSupport.ValueSetExpansionOutcome outcome = myValidationSupport.expandValueSet(ctx, options, theValueSetUrl);
if (outcome != null && outcome.getValueSet() != null) {
FhirTerser terser = myValidationSupport.getFhirContext().newTerser();
List<IBase> contains = terser.getValues(outcome.getValueSet(), "ValueSet.expansion.contains");
int codeCount = contains.size();
return codeCount >= myPostFilterLargeValueSetThreshold;
}
}
return false;
}
private String selectBestSearchParameterForCompartment(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, String compartmentName) {
@ -387,10 +462,26 @@ public class SearchNarrowingInterceptor {
RestOperationTypeEnum restOperationType = method.getRestOperationType();
subServletRequestDetails.setRestOperationType(restOperationType);
incomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse);
hookIncomingRequestPostProcessed(subServletRequestDetails, myRequest, myResponse);
theModifiableBundleEntry.setRequestUrl(myFhirContext, ServletRequestUtil.extractUrl(subServletRequestDetails));
}
}
static List<AllowedCodeInValueSet> getPostFilteringList(RequestDetails theRequestDetails) {
List<AllowedCodeInValueSet> retVal = getPostFilteringListOrNull(theRequestDetails);
if (retVal == null) {
retVal = new ArrayList<>();
theRequestDetails.setAttribute(POST_FILTERING_LIST_ATTRIBUTE_NAME, retVal);
}
return retVal;
}
@SuppressWarnings("unchecked")
static List<AllowedCodeInValueSet> getPostFilteringListOrNull(RequestDetails theRequestDetails) {
return (List<AllowedCodeInValueSet>) theRequestDetails.getAttribute(POST_FILTERING_LIST_ATTRIBUTE_NAME);
}
}

View File

@ -30,13 +30,16 @@ import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
import ca.uhn.fhir.util.FhirTerser;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.ICompositeType;
import org.hl7.fhir.instance.model.api.IIdType;
import org.slf4j.Logger;
import javax.annotation.Nonnull;
import java.util.List;
import java.util.Set;
@ -96,68 +99,131 @@ class SearchParameterAndValueSetRuleImpl extends RuleImplOp {
validationSupport = theFhirContext.getValidationSupport();
}
FhirTerser terser = theFhirContext.newTerser();
ConceptValidationOptions conceptValidationOptions = new ConceptValidationOptions();
ValidationSupportContext validationSupportContext = new ValidationSupportContext(validationSupport);
String operationDescription = "Authorization Rule";
Logger troubleshootingLog = theRuleApplier.getTroubleshootingLog();
boolean wantCode = myWantCode;
RuntimeResourceDefinition resourceDefinition = theFhirContext.getResourceDefinition(theResource);
RuntimeSearchParam searchParameter = resourceDefinition.getSearchParam(mySearchParameterName);
if (searchParameter == null) {
throw new InternalErrorException(Msg.code(2025) + "Unknown SearchParameter for resource " + resourceDefinition.getName() + ": " + mySearchParameterName);
ISearchParamRegistry searchParamRegistry = null;
CodeMatchCount codeMatchCount = countMatchingCodesInValueSetForSearchParameter(theResource, validationSupport, searchParamRegistry, wantCode, mySearchParameterName, myValueSetUrl, troubleshootingLog, operationDescription);
if (codeMatchCount.isAtLeastOneUnableToValidate()) {
troubleshootingLog
.warn("ValueSet {} could not be validated by terminology service - Assuming DENY", myValueSetUrl);
return new AuthorizationInterceptor.Verdict(PolicyEnum.DENY, this);
}
theRuleApplier
.getTroubleshootingLog()
.debug("Applying {}:{} rule for valueSet: {}", mySearchParameterName, myWantCode ? "in" : "not-in", myValueSetUrl);
if (myWantCode && codeMatchCount.getMatchingCodeCount() > 0) {
return newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
} else if (!myWantCode) {
boolean notFound = getMode() == PolicyEnum.ALLOW && codeMatchCount.getMatchingCodeCount() == 0;
boolean othersFound = getMode() == PolicyEnum.DENY && codeMatchCount.getMatchingCodeCount() < codeMatchCount.getOverallCodeCount();
if (notFound || othersFound) {
AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
if (notFound) {
troubleshootingLog
.debug("Code was not found in VS - Verdict: {}", verdict);
} else {
troubleshootingLog
.debug("Code(s) found that are not in VS - Verdict: {}", verdict);
}
return verdict;
}
}
return null;
}
/**
* Scan a resource for all codes indexed by the given SearchParameter and validates the codes for membership in
* the given SearchParameter
*
* @param theResource The resource to scan
* @param theValidationSupport The validation support module
* @param theReturnOnFirstMatch Should we return as soon as one match is found? (as an optimization)
* @param theSearchParameterName The search parameter name being searched for
* @param theValueSetUrl The ValueSet URL to validate against
* @param theTroubleshootingLog A log to use for writing status updates
* @param theOperationDescription A description of the operation being peformed (for logging)
*/
@Nonnull
static CodeMatchCount countMatchingCodesInValueSetForSearchParameter(IBaseResource theResource, IValidationSupport theValidationSupport, ISearchParamRegistry theSearchParamRegistry, boolean theReturnOnFirstMatch, String theSearchParameterName, String theValueSetUrl, Logger theTroubleshootingLog, String theOperationDescription) {
theTroubleshootingLog.debug("Applying {} {}:{} for valueSet: {}", theOperationDescription, theSearchParameterName, theReturnOnFirstMatch ? "in" : "not-in", theValueSetUrl);
FhirContext fhirContext = theValidationSupport.getFhirContext();
FhirTerser terser = fhirContext.newTerser();
ConceptValidationOptions conceptValidationOptions = new ConceptValidationOptions();
ValidationSupportContext validationSupportContext = new ValidationSupportContext(theValidationSupport);
RuntimeResourceDefinition resourceDefinition = fhirContext.getResourceDefinition(theResource);
RuntimeSearchParam searchParameter = resourceDefinition.getSearchParam(theSearchParameterName);
if (searchParameter == null) {
throw new InternalErrorException(Msg.code(2025) + "Unknown SearchParameter for resource " + resourceDefinition.getName() + ": " + theSearchParameterName);
}
List<String> paths = searchParameter.getPathsSplitForResourceType(resourceDefinition.getName());
CodeMatchCount codeMatchCount = new CodeMatchCount();
for (String nextPath : paths) {
List<ICompositeType> foundCodeableConcepts = theFhirContext.newFhirPath().evaluate(theResource, nextPath, ICompositeType.class);
int codeCount = 0;
int matchCount = 0;
List<ICompositeType> foundCodeableConcepts = fhirContext.newFhirPath().evaluate(theResource, nextPath, ICompositeType.class);
for (ICompositeType nextCodeableConcept : foundCodeableConcepts) {
for (IBase nextCoding : terser.getValues(nextCodeableConcept, "coding")) {
String system = terser.getSinglePrimitiveValueOrNull(nextCoding, "system");
String code = terser.getSinglePrimitiveValueOrNull(nextCoding, "code");
if (isNotBlank(system) && isNotBlank(code)) {
codeCount++;
IValidationSupport.CodeValidationResult validateCodeResult = validationSupport.validateCode(validationSupportContext, conceptValidationOptions, system, code, null, myValueSetUrl);
IValidationSupport.CodeValidationResult validateCodeResult = theValidationSupport.validateCode(validationSupportContext, conceptValidationOptions, system, code, null, theValueSetUrl);
if (validateCodeResult != null) {
if (validateCodeResult.isOk()) {
if (myWantCode) {
AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
theRuleApplier
.getTroubleshootingLog()
.debug("Code {}:{} was found in VS - Verdict: {}", system, code, verdict);
return verdict;
} else {
matchCount++;
break;
codeMatchCount.addMatchingCode();
theTroubleshootingLog.debug("Code {}:{} was found in VS", system, code);
if (theReturnOnFirstMatch) {
return codeMatchCount;
}
} else {
theRuleApplier
.getTroubleshootingLog()
.debug("Code {}:{} was not found in VS", system, code);
codeMatchCount.addNonMatchingCode();
theTroubleshootingLog.debug("Code {}:{} was not found in VS: {}", system, code, validateCodeResult.getMessage());
}
} else {
codeMatchCount.addUnableToValidate();
}
}
}
}
if (!myWantCode) {
if ((getMode() == PolicyEnum.ALLOW && matchCount == 0) ||
(getMode() == PolicyEnum.DENY && matchCount < codeCount)) {
AuthorizationInterceptor.Verdict verdict = newVerdict(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
theRuleApplier
.getTroubleshootingLog()
.debug("Code was found in VS - Verdict: {}", verdict);
return verdict;
}
}
}
return codeMatchCount;
}
static class CodeMatchCount {
private int myMatchingCodeCount;
private int myOverallCodeCount;
private boolean myAtLeastOneUnableToValidate;
public boolean isAtLeastOneUnableToValidate() {
return myAtLeastOneUnableToValidate;
}
return null;
public void addUnableToValidate() {
myAtLeastOneUnableToValidate = true;
}
public void addNonMatchingCode() {
myOverallCodeCount++;
}
public void addMatchingCode() {
myMatchingCodeCount++;
myOverallCodeCount++;
}
public int getMatchingCodeCount() {
return myMatchingCodeCount;
}
public int getOverallCodeCount() {
return myOverallCodeCount;
}
}
}

View File

@ -35,22 +35,38 @@ import ca.uhn.fhir.rest.api.server.ResponseDetails;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationConstants;
import ca.uhn.fhir.rest.server.util.ICachedSearchDetails;
import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.IModelVisitor2;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.*;
import org.hl7.fhir.instance.model.api.IBase;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseExtension;
import org.hl7.fhir.instance.model.api.IBaseOperationOutcome;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static ca.uhn.fhir.rest.api.Constants.URL_TOKEN_METADATA;
import static ca.uhn.fhir.rest.server.provider.ProviderConstants.OPERATION_META;
@Interceptor
/**
* The ConsentInterceptor can be used to apply arbitrary consent rules and data access policies
* on responses from a FHIR server.
* <p>
* See <a href="https://hapifhir.io/hapi-fhir/docs/security/consent_interceptor.html">Consent Interceptor</a> for
* more information on this interceptor.
* </p>
*/
@Interceptor(order = AuthorizationConstants.ORDER_CONSENT_INTERCEPTOR)
public class ConsentInterceptor {
private static final AtomicInteger ourInstanceCount = new AtomicInteger(0);
private final int myInstanceIndex = ourInstanceCount.incrementAndGet();
@ -58,8 +74,8 @@ public class ConsentInterceptor {
private final String myRequestCompletedKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_COMPLETED";
private final String myRequestSeenResourcesKey = ConsentInterceptor.class.getName() + "_" + myInstanceIndex + "_SEENRESOURCES";
private IConsentService myConsentService;
private IConsentContextServices myContextConsentServices;
private volatile List<IConsentService> myConsentService = Collections.emptyList();
private IConsentContextServices myContextConsentServices = IConsentContextServices.NULL_IMPL;
/**
* Constructor
@ -71,7 +87,7 @@ public class ConsentInterceptor {
/**
* Constructor
*
* @param theConsentService Must not be <code>null</code>
* @param theConsentService Must not be <code>null</code>
*/
public ConsentInterceptor(IConsentService theConsentService) {
this(theConsentService, IConsentContextServices.NULL_IMPL);
@ -93,31 +109,72 @@ public class ConsentInterceptor {
myContextConsentServices = theContextConsentServices;
}
/**
* @deprecated Use {@link #registerConsentService(IConsentService)} instead
*/
@Deprecated
public void setConsentService(IConsentService theConsentService) {
Validate.notNull(theConsentService, "theConsentService must not be null");
myConsentService = theConsentService;
myConsentService = Collections.singletonList(theConsentService);
}
/**
* Adds a consent service to the chain.
* <p>
* Thread safety note: This method can be called while the service is actively processing requestes
*
* @param theConsentService The service to register. Must not be <code>null</code>.
* @since 6.0.0
*/
public ConsentInterceptor registerConsentService(IConsentService theConsentService) {
Validate.notNull(theConsentService, "theConsentService must not be null");
List<IConsentService> newList = new ArrayList<>(myConsentService.size() + 1);
newList.addAll(myConsentService);
newList.add(theConsentService);
myConsentService = newList;
return this;
}
/**
* Removes a consent service from the chain.
* <p>
* Thread safety note: This method can be called while the service is actively processing requestes
*
* @param theConsentService The service to unregister. Must not be <code>null</code>.
* @since 6.0.0
*/
public ConsentInterceptor unregisterConsentService(IConsentService theConsentService) {
Validate.notNull(theConsentService, "theConsentService must not be null");
List<IConsentService> newList = myConsentService
.stream()
.filter(t -> t != theConsentService)
.collect(Collectors.toList());
myConsentService = newList;
return this;
}
@Hook(value = Pointcut.SERVER_INCOMING_REQUEST_PRE_HANDLED)
public void interceptPreHandled(RequestDetails theRequestDetails) {
if (isAllowListedRequest(theRequestDetails)) {
if (isSkipServiceForRequest(theRequestDetails)) {
return;
}
validateParameter(theRequestDetails.getParameters());
ConsentOutcome outcome = myConsentService.startOperation(theRequestDetails, myContextConsentServices);
Validate.notNull(outcome, "Consent service returned null outcome");
for (IConsentService nextService : myConsentService) {
ConsentOutcome outcome = nextService.startOperation(theRequestDetails, myContextConsentServices);
Validate.notNull(outcome, "Consent service returned null outcome");
switch (outcome.getStatus()) {
case REJECT:
throw toForbiddenOperationException(outcome);
case PROCEED:
break;
case AUTHORIZED:
Map<Object, Object> userData = theRequestDetails.getUserData();
userData.put(myRequestAuthorizedKey, Boolean.TRUE);
break;
switch (outcome.getStatus()) {
case REJECT:
throw toForbiddenOperationException(outcome);
case PROCEED:
continue;
case AUTHORIZED:
Map<Object, Object> userData = theRequestDetails.getUserData();
userData.put(myRequestAuthorizedKey, Boolean.TRUE);
return;
}
}
}
@ -141,21 +198,60 @@ public class ConsentInterceptor {
if (isRequestAuthorized(theRequestDetails)) {
return;
}
if (isAllowListedRequest(theRequestDetails)) {
if (isSkipServiceForRequest(theRequestDetails)) {
return;
}
if (myConsentService.isEmpty()) {
return;
}
for (int i = 0; i < thePreResourceAccessDetails.size(); i++) {
IBaseResource nextResource = thePreResourceAccessDetails.getResource(i);
ConsentOutcome nextOutcome = myConsentService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices);
switch (nextOutcome.getStatus()) {
case PROCEED:
break;
case AUTHORIZED:
break;
case REJECT:
thePreResourceAccessDetails.setDontReturnResourceAtIndex(i);
// First check if we should be calling canSeeResource for the individual
// consent services
boolean[] processConsentSvcs = new boolean[myConsentService.size()];
boolean processAnyConsentSvcs = false;
for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
IConsentService nextService = myConsentService.get(consentSvcIdx);
boolean shouldCallCanSeeResource = nextService.shouldProcessCanSeeResource(theRequestDetails, myContextConsentServices);
processAnyConsentSvcs |= shouldCallCanSeeResource;
processConsentSvcs[consentSvcIdx] = shouldCallCanSeeResource;
}
if (!processAnyConsentSvcs) {
return;
}
IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
for (int resourceIdx = 0; resourceIdx < thePreResourceAccessDetails.size(); resourceIdx++) {
IBaseResource nextResource = thePreResourceAccessDetails.getResource(resourceIdx);
for (int consentSvcIdx = 0; consentSvcIdx < myConsentService.size(); consentSvcIdx++) {
IConsentService nextService = myConsentService.get(consentSvcIdx);
if (!processConsentSvcs[consentSvcIdx]) {
continue;
}
ConsentOutcome outcome = nextService.canSeeResource(theRequestDetails, nextResource, myContextConsentServices);
Validate.notNull(outcome, "Consent service returned null outcome");
Validate.isTrue(outcome.getResource() == null, "Consent service returned a resource in its outcome. This is not permitted in canSeeResource(..)");
boolean skipSubsequentServices = false;
switch (outcome.getStatus()) {
case PROCEED:
break;
case AUTHORIZED:
authorizedResources.put(nextResource, Boolean.TRUE);
skipSubsequentServices = true;
break;
case REJECT:
thePreResourceAccessDetails.setDontReturnResourceAtIndex(resourceIdx);
skipSubsequentServices = true;
break;
}
if (skipSubsequentServices) {
break;
}
}
}
}
@ -168,46 +264,53 @@ public class ConsentInterceptor {
if (isAllowListedRequest(theRequestDetails)) {
return;
}
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails);
if (isSkipServiceForRequest(theRequestDetails)) {
return;
}
if (myConsentService.isEmpty()) {
return;
}
IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
for (int i = 0; i < thePreResourceShowDetails.size(); i++) {
IBaseResource nextResource = thePreResourceShowDetails.getResource(i);
if (alreadySeenResources.putIfAbsent(nextResource, Boolean.TRUE) != null) {
IBaseResource resource = thePreResourceShowDetails.getResource(i);
if (resource == null || authorizedResources.putIfAbsent(resource, Boolean.TRUE) != null) {
continue;
}
ConsentOutcome nextOutcome = myConsentService.willSeeResource(theRequestDetails, nextResource, myContextConsentServices);
switch (nextOutcome.getStatus()) {
case PROCEED:
if (nextOutcome.getResource() != null) {
thePreResourceShowDetails.setResource(i, nextOutcome.getResource());
}
break;
case AUTHORIZED:
break;
case REJECT:
if (nextOutcome.getResource() != null) {
IBaseResource newResource = nextOutcome.getResource();
thePreResourceShowDetails.setResource(i, newResource);
alreadySeenResources.put(newResource, true);
} else if (nextOutcome.getOperationOutcome() != null) {
IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
thePreResourceShowDetails.setResource(i, newOperationOutcome);
alreadySeenResources.put(newOperationOutcome, true);
} else {
String resourceId = nextResource.getIdElement().getValue();
thePreResourceShowDetails.setResource(i, null);
nextResource.setId(resourceId);
}
break;
for (IConsentService nextService : myConsentService) {
ConsentOutcome nextOutcome = nextService.willSeeResource(theRequestDetails, resource, myContextConsentServices);
IBaseResource newResource = nextOutcome.getResource();
switch (nextOutcome.getStatus()) {
case PROCEED:
if (newResource != null) {
thePreResourceShowDetails.setResource(i, newResource);
resource = newResource;
}
continue;
case AUTHORIZED:
if (newResource != null) {
thePreResourceShowDetails.setResource(i, newResource);
}
continue;
case REJECT:
if (nextOutcome.getOperationOutcome() != null) {
IBaseOperationOutcome newOperationOutcome = nextOutcome.getOperationOutcome();
thePreResourceShowDetails.setResource(i, newOperationOutcome);
authorizedResources.put(newOperationOutcome, true);
} else {
resource = null;
thePreResourceShowDetails.setResource(i, null);
}
continue;
}
}
}
}
private IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails) {
return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
}
@Hook(value = Pointcut.SERVER_OUTGOING_RESPONSE)
public void interceptOutgoingResponse(RequestDetails theRequestDetails, ResponseDetails theResource) {
if (theResource.getResponseResource() == null) {
@ -219,36 +322,46 @@ public class ConsentInterceptor {
if (isAllowListedRequest(theRequestDetails)) {
return;
}
if (isSkipServiceForRequest(theRequestDetails)) {
return;
}
if (myConsentService.isEmpty()) {
return;
}
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = getAlreadySeenResourcesMap(theRequestDetails);
IdentityHashMap<IBaseResource, Boolean> authorizedResources = getAuthorizedResourcesMap(theRequestDetails);
// See outer resource
if (alreadySeenResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) {
final ConsentOutcome outcome = myConsentService.willSeeResource(theRequestDetails, theResource.getResponseResource(), myContextConsentServices);
if (outcome.getResource() != null) {
theResource.setResponseResource(outcome.getResource());
}
if (authorizedResources.putIfAbsent(theResource.getResponseResource(), Boolean.TRUE) == null) {
// Clear the total
if (theResource.getResponseResource() instanceof IBaseBundle) {
BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theResource.getResponseResource(), null);
}
for (IConsentService next : myConsentService) {
final ConsentOutcome outcome = next.willSeeResource(theRequestDetails, theResource.getResponseResource(), myContextConsentServices);
if (outcome.getResource() != null) {
theResource.setResponseResource(outcome.getResource());
}
switch (outcome.getStatus()) {
case REJECT:
if (outcome.getOperationOutcome() != null) {
theResource.setResponseResource(outcome.getOperationOutcome());
} else {
theResource.setResponseResource(null);
theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
}
return;
case AUTHORIZED:
// Don't check children
return;
case PROCEED:
// Check children
break;
// Clear the total
if (theResource.getResponseResource() instanceof IBaseBundle) {
BundleUtil.setTotal(theRequestDetails.getFhirContext(), (IBaseBundle) theResource.getResponseResource(), null);
}
switch (outcome.getStatus()) {
case REJECT:
if (outcome.getOperationOutcome() != null) {
theResource.setResponseResource(outcome.getOperationOutcome());
} else {
theResource.setResponseResource(null);
theResource.setResponseCode(Constants.STATUS_HTTP_204_NO_CONTENT);
}
// Return immediately
return;
case AUTHORIZED:
// Don't check children, so return immediately
return;
case PROCEED:
// Check children, so proceed
break;
}
}
}
@ -268,32 +381,38 @@ public class ConsentInterceptor {
return true;
}
if (theElement instanceof IBaseResource) {
if (alreadySeenResources.putIfAbsent((IBaseResource) theElement, Boolean.TRUE) != null) {
IBaseResource resource = (IBaseResource) theElement;
if (authorizedResources.putIfAbsent(resource, Boolean.TRUE) != null) {
return true;
}
ConsentOutcome childOutcome = myConsentService.willSeeResource(theRequestDetails, (IBaseResource) theElement, myContextConsentServices);
IBaseResource replacementResource = null;
boolean shouldReplaceResource = false;
boolean shouldCheckChildren = false;
boolean shouldCheckChildren = true;
for (IConsentService next : myConsentService) {
ConsentOutcome childOutcome = next.willSeeResource(theRequestDetails, resource, myContextConsentServices);
switch (childOutcome.getStatus()) {
case REJECT:
replacementResource = childOutcome.getOperationOutcome();
shouldReplaceResource = true;
break;
case PROCEED:
case AUTHORIZED:
replacementResource = childOutcome.getResource();
shouldReplaceResource = replacementResource != null;
shouldCheckChildren = childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED;
break;
}
IBaseResource replacementResource = null;
boolean shouldReplaceResource = false;
switch (childOutcome.getStatus()) {
case REJECT:
replacementResource = childOutcome.getOperationOutcome();
shouldReplaceResource = true;
break;
case PROCEED:
case AUTHORIZED:
replacementResource = childOutcome.getResource();
shouldReplaceResource = replacementResource != null;
shouldCheckChildren &= childOutcome.getStatus() == ConsentOperationStatusEnum.PROCEED;
break;
}
if (shouldReplaceResource) {
IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2);
BaseRuntimeChildDefinition containerChildElement = theChildDefinitionPath.get(theChildDefinitionPath.size() - 1);
containerChildElement.getMutator().setValue(container, replacementResource);
resource = replacementResource;
}
if (shouldReplaceResource) {
IBase container = theContainingElementPath.get(theContainingElementPath.size() - 2);
BaseRuntimeChildDefinition containerChildElement = theChildDefinitionPath.get(theChildDefinitionPath.size() - 1);
containerChildElement.getMutator().setValue(container, replacementResource);
}
return shouldCheckChildren;
@ -311,10 +430,16 @@ public class ConsentInterceptor {
}
private IdentityHashMap<IBaseResource, Boolean> getAuthorizedResourcesMap(RequestDetails theRequestDetails) {
return getAlreadySeenResourcesMap(theRequestDetails, myRequestSeenResourcesKey);
}
@Hook(value = Pointcut.SERVER_HANDLE_EXCEPTION)
public void requestFailed(RequestDetails theRequest, BaseServerResponseException theException) {
theRequest.getUserData().put(myRequestCompletedKey, Boolean.TRUE);
myConsentService.completeOperationFailure(theRequest, theException, myContextConsentServices);
for (IConsentService next : myConsentService) {
next.completeOperationFailure(theRequest, theException, myContextConsentServices);
}
}
@Hook(value = Pointcut.SERVER_PROCESSING_COMPLETED_NORMALLY)
@ -322,7 +447,9 @@ public class ConsentInterceptor {
if (Boolean.TRUE.equals(theRequest.getUserData().get(myRequestCompletedKey))) {
return;
}
myConsentService.completeOperationSuccess(theRequest, myContextConsentServices);
for (IConsentService next : myConsentService) {
next.completeOperationSuccess(theRequest, myContextConsentServices);
}
}
private boolean isRequestAuthorized(RequestDetails theRequestDetails) {
@ -334,6 +461,33 @@ public class ConsentInterceptor {
return retVal;
}
private boolean isSkipServiceForRequest(RequestDetails theRequestDetails) {
return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
}
private boolean isAllowListedRequest(RequestDetails theRequestDetails) {
return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
}
private boolean isMetaOperation(RequestDetails theRequestDetails) {
return OPERATION_META.equals(theRequestDetails.getOperation());
}
private boolean isMetadataPath(RequestDetails theRequestDetails) {
return URL_TOKEN_METADATA.equals(theRequestDetails.getRequestPath());
}
private void validateParameter(Map<String, String[]> theParameterMap) {
if (theParameterMap != null) {
if (theParameterMap.containsKey(Constants.PARAM_SEARCH_TOTAL_MODE) && Arrays.stream(theParameterMap.get("_total")).anyMatch("accurate"::equals)) {
throw new InvalidRequestException(Msg.code(2037) + Constants.PARAM_SEARCH_TOTAL_MODE + "=accurate is not permitted on this server");
}
if (theParameterMap.containsKey(Constants.PARAM_SUMMARY) && Arrays.stream(theParameterMap.get("_summary")).anyMatch("count"::equals)) {
throw new InvalidRequestException(Msg.code(2038) + Constants.PARAM_SUMMARY + "=count is not permitted on this server");
}
}
}
@SuppressWarnings("unchecked")
public static IdentityHashMap<IBaseResource, Boolean> getAlreadySeenResourcesMap(RequestDetails theRequestDetails, String theKey) {
IdentityHashMap<IBaseResource, Boolean> alreadySeenResources = (IdentityHashMap<IBaseResource, Boolean>) theRequestDetails.getUserData().get(theKey);
@ -351,27 +505,4 @@ public class ConsentInterceptor {
}
return new ForbiddenOperationException("Rejected by consent service", operationOutcome);
}
private boolean isAllowListedRequest(RequestDetails theRequestDetails) {
return isMetadataPath(theRequestDetails) || isMetaOperation(theRequestDetails);
}
private boolean isMetaOperation(RequestDetails theRequestDetails) {
return OPERATION_META.equals(theRequestDetails.getOperation());
}
private boolean isMetadataPath(RequestDetails theRequestDetails) {
return URL_TOKEN_METADATA.equals(theRequestDetails.getRequestPath());
}
private void validateParameter(Map<String, String[]> theParameterMap) {
if (theParameterMap != null) {
if (theParameterMap.containsKey("_total") && Arrays.stream(theParameterMap.get("_total")).anyMatch("accurate"::equals)) {
throw new InvalidRequestException(Msg.code(2037) + "_total=accurate is not permitted on this server");
}
if (theParameterMap.containsKey("_summary") && Arrays.stream(theParameterMap.get("_summary")).anyMatch("count"::equals)) {
throw new InvalidRequestException(Msg.code(2038) + "_summary=count is not permitted on this server");
}
}
}
}

View File

@ -25,7 +25,15 @@ import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import org.hl7.fhir.instance.model.api.IBaseResource;
/**
* This interface is intended to be implemented as the user-defined contract for
* the {@link ConsentInterceptor}.
* <p>
* Note: Since HAPI FHIR 5.1.0, methods in this interface have default methods that return {@link ConsentOutcome#PROCEED}
* </p>
* <p>
* See <a href="https://hapifhir.io/hapi-fhir/docs/security/consent_interceptor.html">Consent Interceptor</a> for
* more information on this interceptor.
* </p>
*/
public interface IConsentService {
@ -49,6 +57,30 @@ public interface IConsentService {
return ConsentOutcome.PROCEED;
}
/**
* This method will be invoked once prior to invoking {@link #canSeeResource(RequestDetails, IBaseResource, IConsentContextServices)}
* and can be used to skip that phase.
* <p>
* If this method returns {@literal false} (default is {@literal true}) {@link #willSeeResource(RequestDetails, IBaseResource, IConsentContextServices)}
* will be invoked for this request, but {@link #canSeeResource(RequestDetails, IBaseResource, IConsentContextServices)} will not.
* </p>
*
* @param theRequestDetails Contains details about the operation that is
* beginning, including details about the request type,
* URL, etc. Note that the RequestDetails has a generic
* Map (see {@link RequestDetails#getUserData()}) that
* can be used to store information and state to be
* passed between methods in the consent service.
* @param theContextServices An object passed in by the consent framework that
* provides utility functions relevant to acting on
* consent directives.
* @return Returns {@literal false} to avoid calling {@link #canSeeResource(RequestDetails, IBaseResource, IConsentContextServices)}
* @since 6.0.0
*/
default boolean shouldProcessCanSeeResource(RequestDetails theRequestDetails, IConsentContextServices theContextServices) {
return true;
}
/**
* This method is called if a user may potentially see a resource via READ
* operations, SEARCH operations, etc. This method may make decisions about
@ -60,12 +92,31 @@ public interface IConsentService {
* method to actually make changes. This method is intended to only
* to make decisions.
* </p>
* <p>
* In addition, the {@link ConsentOutcome} must return one of the following
* statuses:
* </p>
* <ul>
* <li>{@link ConsentOperationStatusEnum#AUTHORIZED}: The resource will be returned to the client. If multiple consent service implementation are present, no further implementations will be invoked for this resource. {@link #willSeeResource(RequestDetails, IBaseResource, IConsentContextServices)} will not be invoked for this resource.</li>
* <li>{@link ConsentOperationStatusEnum#PROCEED}: The resource will be returned to the client.</li>
* <li>{@link ConsentOperationStatusEnum#REJECT}: The resource will be stripped from the response. If multiple consent service implementation are present, no further implementations will be invoked for this resource. {@link #willSeeResource(RequestDetails, IBaseResource, IConsentContextServices)} will not be invoked for this resource.</li>
* </ul>
* </p>
* <p>
* There are two methods the consent service may use to suppress or modify response resources:
* </p>
* <ul>
* <li>{@link #canSeeResource(RequestDetails, IBaseResource, IConsentContextServices)} should be used to remove resources from results in scenarios where it is important to not reveal existence of those resources. It is called prior to any paging logic, so result pages will still be normal sized even if results are filtered.</li>
* <li>{@link #willSeeResource(RequestDetails, IBaseResource, IConsentContextServices)} should be used to filter individual elements from resources, or to remove entire resources in cases where it is not important to conceal their existence. It is called after paging logic, so any resources removed by this method may result in abnormally sized result pages. However, removing resourced using this method may also perform better so it is preferable for use in cases where revealing resource existence is not a concern.</li>
* </ul>
* <p>
* <b>Performance note:</b> Note that this method should be efficient, since it will be called once
* for every resource potentially returned (e.g. by searches). If this method
* takes a significant amount of time to execute, performance on the server
* will suffer.
* </p>
*
*
* @param theRequestDetails Contains details about the operation that is
* beginning, including details about the request type,
* URL, etc. Note that the RequestDetails has a generic
@ -76,7 +127,9 @@ public interface IConsentService {
* @param theContextServices An object passed in by the consent framework that
* provides utility functions relevant to acting on
* consent directives.
* @return An outcome object. See {@link ConsentOutcome}
* @return An outcome object. See {@link ConsentOutcome}. Note that this method is not allowed
* to modify the response object, so an error will be thrown if {@link ConsentOutcome#getResource()}
* returns a non-null response.
*/
default ConsentOutcome canSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;
@ -98,9 +151,9 @@ public interface IConsentService {
* statuses:
* </p>
* <ul>
* <li>{@link ConsentOperationStatusEnum#AUTHORIZED}: The resource will be returned to the client.</li>
* <li>{@link ConsentOperationStatusEnum#PROCEED}: The resource will be returned to the client. Any embedded resources contained within the resource will also be checked by {@link #willSeeResource(RequestDetails, IBaseResource, IConsentContextServices)}.</li>
* <li>{@link ConsentOperationStatusEnum#REJECT}: The resource will not be returned to the client. If the resource supplied to the </li>
* <li>{@link ConsentOperationStatusEnum#AUTHORIZED}: The resource will be returned to the client. If multiple consent service implementation are present, no further implementations will be invoked for this resource.</li>
* <li>{@link ConsentOperationStatusEnum#PROCEED}: The resource will be returned to the client.</li>
* <li>{@link ConsentOperationStatusEnum#REJECT}: The resource will not be returned to the client. If multiple consent service implementation are present, no further implementations will be invoked for this resource.</li>
* </ul>
*
* @param theRequestDetails Contains details about the operation that is
@ -114,6 +167,7 @@ public interface IConsentService {
* provides utility functions relevant to acting on
* consent directives.
* @return An outcome object. See method documentation for a description.
* @see #canSeeResource(RequestDetails, IBaseResource, IConsentContextServices) for a description of the difference between these two methods.
*/
default ConsentOutcome willSeeResource(RequestDetails theRequestDetails, IBaseResource theResource, IConsentContextServices theContextServices) {
return ConsentOutcome.PROCEED;

View File

@ -0,0 +1,107 @@
package ca.uhn.fhir.rest.server.util;
/*-
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2022 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%
*/
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.RuntimeResourceDefinition;
import ca.uhn.fhir.context.RuntimeSearchParam;
import ca.uhn.fhir.context.phonetic.IPhoneticEncoder;
import ca.uhn.fhir.i18n.Msg;
import org.apache.commons.lang3.Validate;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class FhirContextSearchParamRegistry implements ISearchParamRegistry {
private final List<RuntimeSearchParam> myExtraSearchParams = new ArrayList<>();
private final FhirContext myCtx;
/**
* Constructor
*/
public FhirContextSearchParamRegistry(@Nonnull FhirContext theCtx) {
Validate.notNull(theCtx, "theCtx must not be null");
myCtx = theCtx;
}
@Override
public void forceRefresh() {
// nothing
}
@Override
public RuntimeSearchParam getActiveSearchParam(String theResourceName, String theParamName) {
return getActiveSearchParams(theResourceName).get(theParamName);
}
@Override
public Map<String, RuntimeSearchParam> getActiveSearchParams(String theResourceName) {
Map<String, RuntimeSearchParam> sps = new HashMap<>();
RuntimeResourceDefinition nextResDef = myCtx.getResourceDefinition(theResourceName);
for (RuntimeSearchParam nextSp : nextResDef.getSearchParams()) {
sps.put(nextSp.getName(), nextSp);
}
for (RuntimeSearchParam next : myExtraSearchParams) {
sps.put(next.getName(), next);
}
return sps;
}
public void addSearchParam(RuntimeSearchParam theSearchParam) {
myExtraSearchParams.add(theSearchParam);
}
@Override
public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName, Set<String> theParamNames) {
throw new UnsupportedOperationException(Msg.code(2066));
}
@Nullable
@Override
public RuntimeSearchParam getActiveSearchParamByUrl(String theUrl) {
throw new UnsupportedOperationException(Msg.code(2067));
}
@Override
public List<RuntimeSearchParam> getActiveComboSearchParams(String theResourceName) {
throw new UnsupportedOperationException(Msg.code(2068));
}
@Override
public void requestRefresh() {
// nothing
}
@Override
public void setPhoneticEncoder(IPhoneticEncoder thePhoneticEncoder) {
// nothing
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

View File

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

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -3,9 +3,10 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hapi-fhir</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.config;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 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%
*/
import ca.uhn.fhir.model.api.IModelJson;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.apache.commons.lang3.Validate;

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -1,5 +1,25 @@
package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 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%
*/
import ca.uhn.fhir.model.api.IModelJson;
import com.fasterxml.jackson.annotation.JsonProperty;

View File

@ -2,7 +2,7 @@ package ca.uhn.fhir.batch2.jobs.imprt;
/*-
* #%L
* HAPI FHIR JPA Server
* hapi-fhir-storage-batch2-jobs
* %%
* Copyright (C) 2014 - 2022 Smile CDR, Inc.
* %%

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -1,7 +1,6 @@
package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.api.BundleInclusionRule;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.OperationParam;
@ -12,38 +11,32 @@ import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.param.StringParam;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.rest.server.interceptor.auth.SearchNarrowingInterceptorTest;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOperationStatusEnum;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentOutcome;
import ca.uhn.fhir.rest.server.interceptor.consent.IConsentService;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
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.IBaseParameters;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.OperationOutcome;
import org.hl7.fhir.r4.model.Parameters;
import org.hl7.fhir.r4.model.Patient;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
@ -51,7 +44,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.MatcherAssert.assertThat;
@ -65,36 +57,47 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
@ExtendWith(MockitoExtension.class)
public class ConsentInterceptorTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ConsentInterceptorTest.class);
private static CloseableHttpClient ourClient;
private static FhirContext ourCtx = FhirContext.forR4();
private static int ourPort;
private static RestfulServer ourServlet;
private static Server ourServer;
private static DummyPatientResourceProvider ourPatientProvider;
private static IGenericClient ourFhirClient;
private static DummySystemProvider ourSystemProvider;
@RegisterExtension
private final HttpClientExtension myClient = new HttpClientExtension();
private static final FhirContext ourCtx = FhirContext.forR4Cached();
private int myPort;
private static final DummyPatientResourceProvider ourPatientProvider = new DummyPatientResourceProvider(ourCtx);
private static final DummySystemProvider ourSystemProvider = new DummySystemProvider();
@Mock
@RegisterExtension
private static RestfulServerExtension ourServer = new RestfulServerExtension(ourCtx)
.registerProvider(ourPatientProvider)
.registerProvider(ourSystemProvider)
.withPagingProvider(new FifoMemoryPagingProvider(10));
@Mock(answer = Answers.CALLS_REAL_METHODS)
private IConsentService myConsentSvc;
@Mock(answer = Answers.CALLS_REAL_METHODS)
private IConsentService myConsentSvc2;
private ConsentInterceptor myInterceptor;
@Captor
private ArgumentCaptor<BaseServerResponseException> myExceptionCaptor;
private IGenericClient myFhirClient;
@AfterEach
public void after() {
ourServlet.unregisterInterceptor(myInterceptor);
ourServer.unregisterInterceptor(myInterceptor);
}
@BeforeEach
public void before() {
myPort = ourServer.getPort();
myFhirClient = ourServer.getFhirClient();
myInterceptor = new ConsentInterceptor(myConsentSvc);
ourServlet.registerInterceptor(myInterceptor);
ourServlet.setPagingProvider(new FifoMemoryPagingProvider(10));
ourServer.registerInterceptor(myInterceptor);
ourPatientProvider.clear();
}
@ -118,9 +121,9 @@ public class ConsentInterceptorTest {
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -148,8 +151,8 @@ public class ConsentInterceptorTest {
HttpGet httpGet;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_total=accurate");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_total=accurate");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(400, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -159,16 +162,16 @@ public class ConsentInterceptorTest {
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_total=estimated");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_total=estimated");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
assertThat(responseContent, not(containsString("\"total\"")));
}
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_total=none");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_total=none");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -194,8 +197,8 @@ public class ConsentInterceptorTest {
HttpGet httpGet;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_summary=count");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_summary=count");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(400, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -205,8 +208,8 @@ public class ConsentInterceptorTest {
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_summary=data");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_summary=data");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -214,8 +217,8 @@ public class ConsentInterceptorTest {
}
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.AUTHORIZED);
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_summary=data");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_summary=data");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -226,15 +229,15 @@ public class ConsentInterceptorTest {
@Test
public void testMetadataCallHasChecksSkipped() throws IOException{
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/metadata");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
}
httpGet = new HttpGet("http://localhost:" + ourPort + "/$meta");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
httpGet = new HttpGet("http://localhost:" + myPort + "/$meta");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -246,6 +249,35 @@ public class ConsentInterceptorTest {
verify(myConsentSvc, times(2)).completeOperationSuccess(any(), any());
}
@Test
public void testSearch_ShouldProcessCanSeeResourcesReturnsFalse() throws IOException {
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
ourPatientProvider.store((Patient) new Patient().setActive(false).setId("PTB"));
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.shouldProcessCanSeeResource(any(),any())).thenReturn(false);
when(myConsentSvc.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t-> ConsentOutcome.AUTHORIZED);
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
assertThat(responseContent, containsString("PTA"));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@Test
public void testSearch_SeeResourceAuthorizesOuterBundle() throws IOException {
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
@ -255,9 +287,9 @@ public class ConsentInterceptorTest {
when(myConsentSvc.canSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t-> ConsentOutcome.PROCEED);
when(myConsentSvc.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t-> ConsentOutcome.AUTHORIZED);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -265,6 +297,7 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
@ -286,9 +319,9 @@ public class ConsentInterceptorTest {
return new ConsentOutcome(ConsentOperationStatusEnum.REJECT, oo);
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -296,6 +329,7 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, timeout(10000).times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(10000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(10000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(10000).times(1)).completeOperationSuccess(any(), any());
@ -315,15 +349,16 @@ public class ConsentInterceptorTest {
return ConsentOutcome.REJECT;
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(204, status.getStatusLine().getStatusCode());
assertNull(status.getEntity());
assertNull(status.getFirstHeader(Constants.HEADER_CONTENT_TYPE));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any()); // the two patients + the bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
@ -349,9 +384,9 @@ public class ConsentInterceptorTest {
return ConsentOutcome.PROCEED;
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -363,6 +398,7 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, timeout(1000).times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, timeout(1000).times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(1000).times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, timeout(1000).times(1)).completeOperationSuccess(any(), any());
@ -388,9 +424,9 @@ public class ConsentInterceptorTest {
return ConsentOutcome.PROCEED;
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -403,6 +439,7 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(4)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
@ -425,9 +462,9 @@ public class ConsentInterceptorTest {
return ConsentOutcome.PROCEED;
});
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -440,6 +477,7 @@ public class ConsentInterceptorTest {
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
@ -457,8 +495,8 @@ public class ConsentInterceptorTest {
when(myConsentSvc.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenReturn(ConsentOutcome.PROCEED);
String nextPageLink;
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=1");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?_count=1");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -481,7 +519,7 @@ public class ConsentInterceptorTest {
});
httpGet = new HttpGet(nextPageLink);
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -495,13 +533,215 @@ public class ConsentInterceptorTest {
}
@Test
public void testTwoServices_FirstRejectsCanSee() {
myInterceptor.registerConsentService(myConsentSvc2);
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.REJECT);
when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
Bundle response = myFhirClient
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.execute();
assertNull(response.getTotalElement().getValue());
assertEquals(0, response.getEntry().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@Test
public void testTwoServices_ShouldProcessCanSeeResourcesReturnsFalse_FirstSvcOnly() throws IOException {
// Setup
myInterceptor.registerConsentService(myConsentSvc2);
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
ourPatientProvider.store((Patient) new Patient().setActive(false).setId("PTB"));
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.shouldProcessCanSeeResource(any(),any())).thenReturn(false);
when(myConsentSvc2.shouldProcessCanSeeResource(any(),any())).thenReturn(true);
when(myConsentSvc2.canSeeResource(any(),any(),any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t-> ConsentOutcome.AUTHORIZED);
when(myConsentSvc2.willSeeResource(any(RequestDetails.class), any(IBaseResource.class), any())).thenAnswer(t-> ConsentOutcome.REJECT);
// Execute
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient");
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
// Verify
assertEquals(200, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
assertThat(responseContent, not(containsString("\"entry\"")));
}
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(2)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(3)).willSeeResource(any(), any(), any());
verify(myConsentSvc2, times(2)).willSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
}
@Test
public void testTwoServices_FirstAuthorizesCanSee() {
myInterceptor.registerConsentService(myConsentSvc2);
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.AUTHORIZED);
when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
Bundle response = myFhirClient
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.execute();
assertNull(response.getTotalElement().getValue());
assertEquals(1, response.getEntry().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(0)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@Test
public void testTwoServices_SecondRejectsCanSee() {
myInterceptor.registerConsentService(myConsentSvc2);
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.REJECT);
when(myConsentSvc.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.willSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
Bundle response = myFhirClient
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.execute();
assertNull(response.getTotalElement().getValue());
assertEquals(0, response.getEntry().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(1)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@Test
public void testTwoServices_ModificationsInWillSee() {
myInterceptor.registerConsentService(myConsentSvc2);
ourPatientProvider.store((Patient) new Patient().setActive(true).setId("PTA"));
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.canSeeResource(any(), any(), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc.willSeeResource(any(), any(Patient.class), any())).thenAnswer(t->new ConsentOutcome(ConsentOperationStatusEnum.PROCEED, t.getArgument(1, Patient.class).addIdentifier(new Identifier().setSystem("FOO"))));
when(myConsentSvc2.willSeeResource(any(), any(Patient.class), any())).thenAnswer(t->new ConsentOutcome(ConsentOperationStatusEnum.PROCEED, t.getArgument(1, Patient.class).addIdentifier(new Identifier().setSystem("FOO"))));
when(myConsentSvc.willSeeResource(any(), any(Bundle.class), any())).thenReturn(ConsentOutcome.PROCEED);
when(myConsentSvc2.willSeeResource(any(), any(Bundle.class), any())).thenReturn(ConsentOutcome.PROCEED);
Bundle response = myFhirClient
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.execute();
assertNull(response.getTotalElement().getValue());
assertEquals(1, response.getEntry().size());
Patient patient = (Patient) response.getEntry().get(0).getResource();
assertEquals(2, patient.getIdentifier().size());
verify(myConsentSvc, times(1)).startOperation(any(), any());
verify(myConsentSvc2, times(1)).startOperation(any(), any());
verify(myConsentSvc, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc2, times(1)).shouldProcessCanSeeResource(any(), any());
verify(myConsentSvc, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc2, times(1)).canSeeResource(any(), any(), any());
verify(myConsentSvc, times(2)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc2, times(2)).willSeeResource(any(), any(), any()); // On bundle
verify(myConsentSvc, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc2, times(1)).completeOperationSuccess(any(), any());
verify(myConsentSvc, times(0)).completeOperationFailure(any(), any(), any());
verify(myConsentSvc2, times(0)).completeOperationFailure(any(), any(), any());
verifyNoMoreInteractions(myConsentSvc);
verifyNoMoreInteractions(myConsentSvc2);
}
@Test
public void testOutcomeException() throws IOException {
when(myConsentSvc.startOperation(any(), any())).thenReturn(ConsentOutcome.PROCEED);
HttpGet httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?searchThrowNullPointerException=1");
HttpGet httpGet = new HttpGet("http://localhost:" + myPort + "/Patient?searchThrowNullPointerException=1");
try (CloseableHttpResponse status = ourClient.execute(httpGet)) {
try (CloseableHttpResponse status = myClient.execute(httpGet)) {
assertEquals(500, status.getStatusLine().getStatusCode());
String responseContent = IOUtils.toString(status.getEntity().getContent(), Charsets.UTF_8);
ourLog.info("Response: {}", responseContent);
@ -513,6 +753,33 @@ public class ConsentInterceptorTest {
assertEquals(Msg.code(389) + "Failed to call access method: java.lang.NullPointerException: A MESSAGE", myExceptionCaptor.getValue().getMessage());
}
@Test
public void testNoServicesRegistered() throws IOException {
myInterceptor.unregisterConsentService(myConsentSvc);
Patient patientA = new Patient();
patientA.setId("Patient/A");
patientA.setActive(true);
patientA.addName().setFamily("FAMILY").addGiven("GIVEN");
patientA.addIdentifier().setSystem("SYSTEM").setValue("VALUEA");
ourPatientProvider.store(patientA);
Patient patientB = new Patient();
patientB.setId("Patient/B");
patientB.setActive(true);
patientB.addName().setFamily("FAMILY").addGiven("GIVEN");
patientB.addIdentifier().setSystem("SYSTEM").setValue("VALUEB");
ourPatientProvider.store(patientB);
Bundle response = myFhirClient
.search()
.forResource(Patient.class)
.returnBundle(Bundle.class)
.execute();
assertEquals(2, response.getTotal());
}
public static class DummyPatientResourceProvider extends HashMapResourceProvider<Patient> {
public DummyPatientResourceProvider(FhirContext theFhirContext) {
@ -529,40 +796,6 @@ public class ConsentInterceptorTest {
}
@AfterAll
public static void afterClass() throws Exception {
JettyUtil.closeServer(ourServer);
ourClient.close();
}
@BeforeAll
public static void beforeClass() throws Exception {
ourServer = new Server(0);
ourPatientProvider = new DummyPatientResourceProvider(ourCtx);
ourSystemProvider = new DummySystemProvider();
ServletHandler servletHandler = new ServletHandler();
ourServlet = new RestfulServer(ourCtx);
ourServlet.setDefaultPrettyPrint(true);
ourServlet.setResourceProviders(ourPatientProvider);
ourServlet.registerProvider(ourSystemProvider);
ourServlet.setBundleInclusionRule(BundleInclusionRule.BASED_ON_RESOURCE_PRESENCE);
ServletHolder servletHolder = new ServletHolder(ourServlet);
servletHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(servletHandler);
ourServer.start();
ourPort = JettyUtil.getPortForStartedServer(ourServer);
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
HttpClientBuilder builder = HttpClientBuilder.create();
builder.setConnectionManager(connectionManager);
ourClient = builder.build();
ourFhirClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
}
private static class DummySystemProvider{
@Operation(name = "$meta", idempotent = true, returnParameters = {

View File

@ -0,0 +1,533 @@
package ca.uhn.fhir.rest.server.interceptor;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.Operation;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.SimpleBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizedList;
import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule;
import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder;
import ca.uhn.fhir.rest.server.interceptor.auth.SearchNarrowingConsentService;
import ca.uhn.fhir.rest.server.interceptor.auth.SearchNarrowingInterceptor;
import ca.uhn.fhir.rest.server.interceptor.consent.ConsentInterceptor;
import ca.uhn.fhir.rest.server.provider.HashMapResourceProvider;
import ca.uhn.fhir.test.utilities.HttpClientExtension;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import org.hl7.fhir.instance.model.api.IBaseResource;
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.ValueSet;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import javax.annotation.Nonnull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static ca.uhn.fhir.test.utilities.SearchTestUtil.toUnqualifiedVersionlessIdValues;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
/**
* A test to ensure that Search Narrowing with Consent Interceptor for post filtering and AuthorizationInterceptor for
* final checking all works.
*/
@ExtendWith(MockitoExtension.class)
public class SearchNarrowingWithConsentAndAuthInterceptorTest {
public static final String VALUESET_1_URL = "http://valueset-1";
public static final String CODESYSTEM_URL = "http://codesystem-1";
public static final String CODE_PREFIX = "code";
public static final String nonMatchingCode = CODE_PREFIX + 99;
public static final String matchingCode = CODE_PREFIX + 1;
private static final FhirContext ourCtx = FhirContext.forR4Cached();
@RegisterExtension
private final HttpClientExtension myClient = new HttpClientExtension();
private final ObservationHashMapResourceProvider myObservationProvider = new ObservationHashMapResourceProvider();
private final MyPatientProvider myPatientProvider = new MyPatientProvider();
private final ConsentInterceptor myConsentInterceptor = new ConsentInterceptor();
@Mock
private IValidationSupport myValidationSupport;
private List<IAuthRule> myAuthorizationInterceptorRuleList;
private final AuthorizationInterceptor myAuthorizationInterceptor = new AuthorizationInterceptor() {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
assertNotNull(myAuthorizationInterceptorRuleList);
return myAuthorizationInterceptorRuleList;
}
};
private AuthorizedList mySearchNarrowingInterceptorAuthorizedList;
private final SearchNarrowingInterceptor mySearchNarrowingInterceptor = new SearchNarrowingInterceptor() {
@Override
protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) {
assertNotNull(mySearchNarrowingInterceptorAuthorizedList);
return mySearchNarrowingInterceptorAuthorizedList;
}
};
@RegisterExtension
private final RestfulServerExtension myServer = new RestfulServerExtension(ourCtx)
.registerInterceptor(myAuthorizationInterceptor)
.registerInterceptor(myConsentInterceptor)
.registerInterceptor(mySearchNarrowingInterceptor)
.registerProvider(myPatientProvider)
.registerProvider(myObservationProvider);
private IGenericClient myFhirClient;
private List<IBaseResource> myNextPatientResponse;
@BeforeEach
public void before() {
myConsentInterceptor.registerConsentService(new SearchNarrowingConsentService(myValidationSupport, ourCtx));
myAuthorizationInterceptor.setValidationSupport(myValidationSupport);
mySearchNarrowingInterceptor.setValidationSupport(myValidationSupport);
myFhirClient = myServer.getFhirClient();
myObservationProvider.clear();
myNextPatientResponse = null;
when(myValidationSupport.getFhirContext()).thenReturn(ourCtx);
}
@Test
public void testSearch_AllowOnlyCodeInValueSet_LargeCodeSystem() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", VALUESET_1_URL);
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.expandValueSet(any(), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.ValueSetExpansionOutcome(createValueSet()));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
myObservationProvider.store(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myObservationProvider.store(obs1);
// Execute
Bundle response = myFhirClient
.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.execute();
// Verify
assertEquals(1, myObservationProvider.getRequestParams().size());
assertTrue(myObservationProvider.getRequestParams().get(0).isEmpty(), myObservationProvider.getRequestParams().toString());
assertThat(toUnqualifiedVersionlessIdValues(response), contains("Observation/O0"));
}
@Test
public void testSearch_AllowOnlyCodeInValueSet_LargeCodeSystem_MultipleCodesWithOnlySomeInValueSet() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", VALUESET_1_URL);
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.expandValueSet(any(), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.ValueSetExpansionOutcome(createValueSet()));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myObservationProvider.store(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myObservationProvider.store(obs1);
// Execute
Bundle response = myFhirClient
.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.execute();
// Verify
assertEquals(1, myObservationProvider.getRequestParams().size());
assertTrue(myObservationProvider.getRequestParams().get(0).isEmpty(), myObservationProvider.getRequestParams().toString());
assertThat(toUnqualifiedVersionlessIdValues(response), contains("Observation/O0"));
}
@Test
public void testSearch_AllowOnlyCodeNotInValueSet_LargeCodeSystem() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList()
.addCodeNotInValueSet("Observation", "code", VALUESET_1_URL);
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeNotInValueSet("code", VALUESET_1_URL).andThen()
.deny().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.expandValueSet(any(), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.ValueSetExpansionOutcome(createValueSet()));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
myObservationProvider.store(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myObservationProvider.store(obs1);
// Execute
Bundle response = myFhirClient
.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.execute();
// Verify
assertEquals(1, myObservationProvider.getRequestParams().size());
assertTrue(myObservationProvider.getRequestParams().get(0).isEmpty(), myObservationProvider.getRequestParams().toString());
assertThat(toUnqualifiedVersionlessIdValues(response), contains("Observation/O1"));
}
@Test
public void testSearch_AllowOnlyCodeNotInValueSet_LargeCodeSystem_MultipleCodesWithOnlySomeInValueSet() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList()
.addCodeNotInValueSet("Observation", "code", VALUESET_1_URL);
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeNotInValueSet("code", VALUESET_1_URL).andThen()
.deny().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.expandValueSet(any(), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.ValueSetExpansionOutcome(createValueSet()));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myObservationProvider.store(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myObservationProvider.store(obs1);
// Execute
Bundle response = myFhirClient
.search()
.forResource(Observation.class)
.returnBundle(Bundle.class)
.execute();
// Verify
assertEquals(1, myObservationProvider.getRequestParams().size());
assertTrue(myObservationProvider.getRequestParams().get(0).isEmpty(), myObservationProvider.getRequestParams().toString());
assertThat(toUnqualifiedVersionlessIdValues(response), contains("Observation/O1"));
}
@Test
public void testSearchWithRevInclude_AllowOnlyCodeInValueSet_LargeCodeSystem() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", VALUESET_1_URL);
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.allow().read().resourcesOfType("Patient").withAnyId().andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
myNextPatientResponse = new ArrayList<>();
Patient p = new Patient();
p.setId("Patient/P0");
p.setActive(true);
myNextPatientResponse.add(p);
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
myNextPatientResponse.add(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myNextPatientResponse.add(obs1);
// Execute
Bundle response = myFhirClient
.search()
.forResource(Patient.class)
.revInclude(Patient.INCLUDE_ALL)
.returnBundle(Bundle.class)
.execute();
// Verify
assertThat(toUnqualifiedVersionlessIdValues(response), contains("Patient/P0", "Observation/O0"));
}
@Test
public void testEverythingOperation_AllowOnlyCodeInValueSet_LargeCodeSystem() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", VALUESET_1_URL);
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.allow().read().resourcesOfType("Patient").withAnyId().andThen()
.allow().operation().named("$everything").onAnyInstance().andRequireExplicitResponseAuthorization().andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
myNextPatientResponse = new ArrayList<>();
Patient p = new Patient();
p.setId("Patient/P0");
p.setActive(true);
myNextPatientResponse.add(p);
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
myNextPatientResponse.add(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myNextPatientResponse.add(obs1);
// Execute
Bundle response = myFhirClient
.operation()
.onInstance(new IdType("Patient/P0"))
.named("$everything")
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
// Verify
assertThat(toUnqualifiedVersionlessIdValues(response), contains("Patient/P0", "Observation/O0"));
}
@Test
public void testEverythingOperation_AllowOnlyCodeInValueSet_NoNarrowing() {
// Setup
mySearchNarrowingInterceptorAuthorizedList = new AuthorizedList();
myAuthorizationInterceptorRuleList = new RuleBuilder()
.allow().read().resourcesOfType("Observation").withCodeInValueSet("code", VALUESET_1_URL).andThen()
.allow().read().resourcesOfType("Patient").withAnyId().andThen()
.allow().operation().named("$everything").onAnyInstance().andRequireExplicitResponseAuthorization().andThen()
.denyAll()
.build();
mySearchNarrowingInterceptor.setPostFilterLargeValueSetThreshold(5);
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(matchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setCode(matchingCode));
when(myValidationSupport.validateCode(any(), any(), eq(CODESYSTEM_URL), eq(nonMatchingCode), any(), eq(VALUESET_1_URL))).thenReturn(new IValidationSupport.CodeValidationResult()
.setSeverity(IValidationSupport.IssueSeverity.ERROR)
.setMessage("Code could not be found!"));
myNextPatientResponse = new ArrayList<>();
Patient p = new Patient();
p.setId("Patient/P0");
p.setActive(true);
myNextPatientResponse.add(p);
Observation obs0 = new Observation();
obs0.setId("Observation/O0");
obs0.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(matchingCode);
myNextPatientResponse.add(obs0);
Observation obs1 = new Observation();
obs1.setId("Observation/O1");
obs1.getCode().addCoding().setSystem(CODESYSTEM_URL).setCode(nonMatchingCode);
myNextPatientResponse.add(obs1);
// Execute
try {
myFhirClient
.operation()
.onInstance(new IdType("Patient/P0"))
.named("$everything")
.withNoParameters(Parameters.class)
.returnResourceType(Bundle.class)
.execute();
fail();
} catch (ForbiddenOperationException e) {
// Verify
assertThat(e.getMessage(), containsString("Access denied by"));
}
}
@Nonnull
private ValueSet createValueSet() {
ValueSet vs = new ValueSet();
vs.setUrl(VALUESET_1_URL);
for (int i = 0; i < 10; i++) {
vs.getExpansion().addContains().setSystem(CODESYSTEM_URL).setCode(CODE_PREFIX + i);
}
return vs;
}
private class MyPatientProvider implements IResourceProvider {
@Search(allowUnknownParams = true)
public List<IBaseResource> searchAll(RequestDetails theRequestDetails, @IncludeParam(reverse = true) Set<Include> theRevIncludes) {
assertNotNull(myNextPatientResponse);
myNextPatientResponse = ServerInterceptorUtil.fireStoragePreshowResource(myNextPatientResponse, theRequestDetails, myServer.getRestfulServer().getInterceptorService());
return myNextPatientResponse;
}
@Override
public Class<Patient> getResourceType() {
return Patient.class;
}
@Operation(name = "$everything")
public IBundleProvider everything(RequestDetails theRequestDetails, @IdParam IdType theId) {
assertNotNull(myNextPatientResponse);
myNextPatientResponse = ServerInterceptorUtil.fireStoragePreshowResource(myNextPatientResponse, theRequestDetails, myServer.getRestfulServer().getInterceptorService());
return new SimpleBundleProvider(myNextPatientResponse);
}
}
private static class ObservationHashMapResourceProvider extends HashMapResourceProvider<Observation> {
private final List<Map<String, String[]>> myRequestParams = new ArrayList<>();
public ObservationHashMapResourceProvider() {
super(ourCtx, Observation.class);
}
public List<Map<String, String[]>> getRequestParams() {
return myRequestParams;
}
@Override
public synchronized void clear() {
super.clear();
if (myRequestParams != null) {
myRequestParams.clear();
}
}
@Search(allowUnknownParams = true)
@Override
public synchronized List<IBaseResource> searchAll(RequestDetails theRequestDetails) {
myRequestParams.add(theRequestDetails.getParameters());
return super.searchAll(theRequestDetails);
}
}
}

View File

@ -1,6 +1,7 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
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.OptionalParam;
@ -10,10 +11,7 @@ import ca.uhn.fhir.rest.annotation.TransactionParam;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.client.interceptor.LoggingInterceptor;
import ca.uhn.fhir.rest.gclient.ICriterion;
import ca.uhn.fhir.rest.gclient.TokenClientParam;
import ca.uhn.fhir.rest.param.BaseAndListParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.StringAndListParam;
@ -22,14 +20,9 @@ import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.param.TokenParamModifier;
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.test.utilities.JettyUtil;
import ca.uhn.fhir.test.utilities.server.RestfulServerExtension;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.util.UrlUtil;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.ServletHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.hamcrest.Matchers;
import org.hl7.fhir.instance.model.api.IAnyResource;
import org.hl7.fhir.instance.model.api.IBaseResource;
@ -37,18 +30,22 @@ import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nonnull;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static ca.uhn.fhir.util.UrlUtil.escapeUrlParam;
@ -58,12 +55,17 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class SearchNarrowingInterceptorTest {
private static final Logger ourLog = LoggerFactory.getLogger(SearchNarrowingInterceptorTest.class);
private static final FhirContext ourCtx = FhirContext.forR4Cached();
public static final String FOO_CS_URL = "http://foo";
public static final String CODE_PREFIX = "CODE";
private static String ourLastHitMethod;
private static FhirContext ourCtx;
private static TokenAndListParam ourLastIdParam;
private static TokenAndListParam ourLastCodeParam;
private static ReferenceAndListParam ourLastSubjectParam;
@ -71,11 +73,18 @@ public class SearchNarrowingInterceptorTest {
private static ReferenceAndListParam ourLastPerformerParam;
private static StringAndListParam ourLastNameParam;
private static List<Resource> ourReturn;
private static Server ourServer;
private static IGenericClient ourClient;
private static AuthorizedList ourNextAuthorizedList;
private static Bundle.BundleEntryRequestComponent ourLastBundleRequest;
private IGenericClient myClient;
@Mock
private IValidationSupport myValidationSupport;
private MySearchNarrowingInterceptor myInterceptor;
@RegisterExtension
private RestfulServerExtension myRestfulServerExtension = new RestfulServerExtension(ourCtx)
.registerProvider(new DummyObservationResourceProvider())
.registerProvider(new DummyPatientResourceProvider())
.registerProvider(new DummySystemProvider())
.withPagingProvider(new FifoMemoryPagingProvider(100));
@BeforeEach
public void before() {
@ -88,6 +97,16 @@ public class SearchNarrowingInterceptorTest {
ourLastPerformerParam = null;
ourLastCodeParam = null;
ourNextAuthorizedList = null;
myInterceptor = new MySearchNarrowingInterceptor();
myRestfulServerExtension.registerInterceptor(myInterceptor);
myClient = myRestfulServerExtension.getFhirClient();
}
@AfterEach
public void afterEach() {
myRestfulServerExtension.unregisterInterceptor(myInterceptor);
}
@Test
@ -95,7 +114,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = null;
ourClient
myClient
.search()
.forResource("Patient")
.execute();
@ -113,7 +132,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList()
.addCodeNotInValueSet("Observation", "code", "http://myvs");
ourClient
myClient
.search()
.forResource("Observation")
.execute();
@ -130,13 +149,12 @@ public class SearchNarrowingInterceptorTest {
assertNull(ourLastIdParam);
}
@Test
public void testNarrowCode_InSelected_ClientRequestedNoParams() {
ourNextAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", "http://myvs");
ourClient
myClient
.search()
.forResource("Observation")
.execute();
@ -153,6 +171,30 @@ public class SearchNarrowingInterceptorTest {
assertNull(ourLastIdParam);
}
@Test
public void testNarrowCode_InSelected_ClientRequestedNoParams_LargeValueSet() {
myInterceptor.setPostFilterLargeValueSetThreshold(50);
myInterceptor.setValidationSupport(myValidationSupport);
when(myValidationSupport.getFhirContext()).thenReturn(ourCtx);
when(myValidationSupport.expandValueSet(any(), any(), eq("http://large-vs")))
.thenReturn(createValueSetWithCodeCount(100));
ourNextAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", "http://large-vs");
myClient
.search()
.forResource("Observation")
.execute();
assertEquals("Observation.search", ourLastHitMethod);
assertNull(ourLastCodeParam);
assertNull(ourLastSubjectParam);
assertNull(ourLastPerformerParam);
assertNull(ourLastPatientParam);
assertNull(ourLastIdParam);
}
@Test
public void testNarrowCode_InSelected_ClientRequestedBundleWithNoParams() {
ourNextAuthorizedList = new AuthorizedList()
@ -163,7 +205,7 @@ public class SearchNarrowingInterceptorTest {
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Observation?subject=Patient/123");
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
ourClient
myClient
.transaction()
.withBundle(bundle)
.execute();
@ -184,8 +226,8 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Observation", "code", "http://myvs");
ourClient.registerInterceptor(new LoggingInterceptor(false));
ourClient
myClient.registerInterceptor(new LoggingInterceptor(false));
myClient
.search()
.forResource("Observation")
.where(singletonMap("code", singletonList(new TokenParam("http://othervs").setModifier(TokenParamModifier.IN))))
@ -212,7 +254,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList()
.addCodeInValueSet("Procedure", "code", "http://myvs");
ourClient
myClient
.search()
.forResource("Observation")
.execute();
@ -226,7 +268,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList()
.addCompartments("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Observation")
.execute();
@ -249,7 +291,7 @@ public class SearchNarrowingInterceptorTest {
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Patient");
ourLog.info(ourCtx.newJsonParser().setPrettyPrint(true).encodeResourceToString(bundle));
ourClient
myClient
.transaction()
.withBundle(bundle)
.execute();
@ -263,7 +305,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Patient")
.execute();
@ -278,7 +320,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Patient")
.where(IAnyResource.RES_ID.exactly().codes("Patient/123", "Patient/999"))
@ -294,7 +336,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("Patient/456", "Patient/777"))
@ -314,7 +356,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("456", "777"))
@ -334,7 +376,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.SUBJECT.hasAnyOfIds("Patient/456", "Patient/777"))
@ -355,7 +397,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
try {
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("Patient/111", "Patient/777"))
@ -376,7 +418,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
try {
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.SUBJECT.hasAnyOfIds("Patient/111", "Patient/777"))
@ -397,7 +439,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
try {
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("Patient/"))
@ -417,7 +459,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList().addCompartments("Patient/");
try {
ourClient
myClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("Patient/111", "Patient/777"))
@ -439,7 +481,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Observation")
.execute();
@ -457,7 +499,7 @@ public class SearchNarrowingInterceptorTest {
ourNextAuthorizedList = new AuthorizedList()
.addResources("Patient/123", "Patient/456");
ourClient
myClient
.search()
.forResource("Patient")
.execute();
@ -483,6 +525,26 @@ public class SearchNarrowingInterceptorTest {
.collect(Collectors.toList());
}
@Nonnull
private static IValidationSupport.ValueSetExpansionOutcome createValueSetWithCodeCount(int theCount) {
ValueSet valueSet = new ValueSet();
valueSet.getExpansion().setTotal(theCount);
for (int i = 0; i < theCount; i++) {
valueSet
.getExpansion()
.addContains()
.setSystem(FOO_CS_URL)
.setCode(CODE_PREFIX + i);
}
return new IValidationSupport.ValueSetExpansionOutcome(valueSet);
}
@AfterAll
public static void afterClassClearContext() throws Exception {
TestUtil.randomizeLocaleAndTimezone();
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
@ -549,39 +611,5 @@ public class SearchNarrowingInterceptorTest {
}
}
@AfterAll
public static void afterClassClearContext() throws Exception {
JettyUtil.closeServer(ourServer);
TestUtil.randomizeLocaleAndTimezone();
}
@BeforeAll
public static void beforeClass() throws Exception {
ourCtx = FhirContext.forR4();
ourServer = new Server(0);
DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider();
DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider();
DummySystemProvider systemProv = new DummySystemProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer ourServlet = new RestfulServer(ourCtx);
ourServlet.setFhirContext(ourCtx);
ourServlet.registerProviders(systemProv);
ourServlet.setResourceProviders(patProvider, obsProv);
ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100));
ourServlet.registerInterceptor(new MySearchNarrowingInterceptor());
ServletHolder servletHolder = new ServletHolder(ourServlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
JettyUtil.startServer(ourServer);
int ourPort = JettyUtil.getPortForStartedServer(ourServer);
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourCtx.getRestfulClientFactory().setSocketTimeout(1000000);
ourClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
}
}

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-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>6.0.0-PRE4-SNAPSHOT</version>
<version>6.0.0-PRE5-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -21,8 +21,6 @@ package ca.uhn.fhir.test.utilities;
*/
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;

Some files were not shown because too many files have changed in this diff Show More