Merge branch 'master' into subscription-bugfix

This commit is contained in:
James Agnew 2019-01-11 09:05:37 -06:00 committed by GitHub
commit 068117138e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 2334 additions and 1342 deletions

View File

@ -1,15 +1,11 @@
package example;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import java.util.List;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import ca.uhn.fhir.model.dstu2.resource.Patient;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.annotation.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.ResourceParam;
import ca.uhn.fhir.rest.annotation.Update;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
@ -17,6 +13,12 @@ import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.*;
import org.hl7.fhir.dstu3.model.IdType;
import org.hl7.fhir.instance.model.api.IBaseResource;
import java.util.List;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@SuppressWarnings("unused")
public class AuthorizationInterceptors {
@ -158,4 +160,47 @@ public class AuthorizationInterceptors {
//END SNIPPET: patchAll
}
//START SNIPPET: narrowing
public class MyPatientSearchNarrowingInterceptor extends SearchNarrowingInterceptor {
/**
* This method must be overridden to provide the list of compartments
* and/or resources that the current user should have access to
*/
@Override
protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) {
// 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.
String authHeader = theRequestDetails.getHeader("Authorization");
if ("Bearer dfw98h38r".equals(authHeader)) {
// This user will have access to two compartments
return new AuthorizedList()
.addCompartment("Patient/123")
.addCompartment("Patient/456");
} else if ("Bearer 39ff939jgg".equals(authHeader)) {
// This user has access to everything
return new AuthorizedList();
} else {
throw new AuthenticationException("Unknown bearer token");
}
}
}
//END SNIPPET: narrowing
}

View File

@ -1,8 +1,10 @@
package ca.uhn.fhir.rest.gclient;
import ca.uhn.fhir.context.FhirContext;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Arrays;
import java.util.Collection;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -98,6 +100,18 @@ public class ReferenceClientParam extends BaseClientParam implements IParam {
return new StringCriterion<>(getParamName(), theIds);
}
/**
* Match the referenced resource if the resource has ANY of the given IDs
* (this is an OR search, not an AND search), (this can be the logical ID or
* the absolute URL of the resource). Note that to specify an AND search,
* simply add a subsequent {@link IQuery#where(ICriterion) where} criteria
* with the same parameter.
*/
public ICriterion<ReferenceClientParam> hasAnyOfIds(String... theIds) {
Validate.notNull(theIds, "theIds must not be null");
return hasAnyOfIds(Arrays.asList(theIds));
}
private static class ReferenceChainCriterion implements ICriterion<ReferenceClientParam>, ICriterionInternal {
private final String myResourceTypeQualifier;

View File

@ -62,5 +62,10 @@ public abstract class BaseAndListParam<T extends IQueryParameterOr<?>> implement
return myValues.toString();
}
/**
* Returns the number of AND parameters
*/
public int size() {
return myValues.size();
}
}

View File

@ -20,8 +20,10 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
/*
* #%L
@ -208,6 +210,13 @@ public class ParameterUtil {
|| IPrimitiveType.class.isAssignableFrom(theClass);
}
public static String escapeAndJoinOrList(Collection<String> theValues) {
return theValues
.stream()
.map(ParameterUtil::escape)
.collect(Collectors.joining(","));
}
public static int nonEscapedIndexOf(String theString, char theCharacter) {
for (int i = 0; i < theString.length(); i++) {
if (theString.charAt(i) == theCharacter) {

View File

@ -29,14 +29,14 @@ public interface IAnyResource extends IBaseResource {
* Search parameter constant for <b>_language</b>
*/
@SearchParamDefinition(name="_language", path="", description="The language of the resource", type="string" )
public static final String SP_RES_LANGUAGE = "_language";
String SP_RES_LANGUAGE = "_language";
/**
* Search parameter constant for <b>_id</b>
*/
@SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="token" )
public static final String SP_RES_ID = "_id";
String SP_RES_ID = "_id";
/**
* <b>Fluent Client</b> search parameter constant for <b>_id</b>
@ -46,7 +46,7 @@ public interface IAnyResource extends IBaseResource {
* Path: <b>Resource._id</b><br>
* </p>
*/
public static final TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID);
TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID);
String getId();
@ -55,11 +55,11 @@ public interface IAnyResource extends IBaseResource {
IPrimitiveType<String> getLanguageElement();
public Object getUserData(String name);
Object getUserData(String name);
@Override
IAnyResource setId(String theId);
public void setUserData(String name, Object value);
void setUserData(String name, Object value);
}

View File

@ -301,7 +301,7 @@ public abstract class RestfulClientFactory implements IRestfulClientFactory {
conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute();
} catch (FhirClientConnectionException e) {
if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3) && e.getCause() instanceof DataFormatException) {
capabilityStatementResourceName = "Conformance";
capabilityStatementResourceName = "CapabilityStatement";
implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass();
conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute();
} else {

View File

@ -81,7 +81,7 @@ public abstract class BaseQueryParameter implements IParameter {
String paramName = isNotBlank(qualifier) ? getName() + qualifier : getName();
List<String> paramValues = theTargetQueryArguments.get(paramName);
if (paramValues == null) {
paramValues = new ArrayList<String>(value.size());
paramValues = new ArrayList<>(value.size());
theTargetQueryArguments.put(paramName, paramValues);
}

View File

@ -233,7 +233,7 @@ public class FhirSystemDaoDstu2 extends BaseHapiFhirSystemDao<Bundle, MetaDt> {
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
Entry nextRespEntry = response.getEntry().get(originalOrder);
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails();
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(theRequestDetails);
requestDetails.setServletRequest(theRequestDetails.getServletRequest());
requestDetails.setRequestType(RequestTypeEnum.GET);
requestDetails.setServer(theRequestDetails.getServer());

View File

@ -384,7 +384,7 @@ public class TransactionProcessor<BUNDLE extends IBaseBundle, BUNDLEENTRY> {
Integer originalOrder = originalRequestOrder.get(nextReqEntry);
BUNDLEENTRY nextRespEntry = myVersionAdapter.getEntries(response).get(originalOrder);
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails();
ServletSubRequestDetails requestDetails = new ServletSubRequestDetails(theRequestDetails);
requestDetails.setServletRequest(theRequestDetails.getServletRequest());
requestDetails.setRequestType(RequestTypeEnum.GET);
requestDetails.setServer(theRequestDetails.getServer());

View File

@ -29,11 +29,25 @@ import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
public class ServletSubRequestDetails extends ServletRequestDetails {
private Map<String, ArrayList<String>> myHeaders = new HashMap<>();
private Map<String, List<String>> myHeaders = new HashMap<>();
/**
* Constructor
*
* @param theRequestDetails The parent request details
*/
public ServletSubRequestDetails(ServletRequestDetails theRequestDetails) {
if (theRequestDetails != null) {
Map<String, List<String>> headers = theRequestDetails.getHeaders();
for (Map.Entry<String, List<String>> next : headers.entrySet()) {
myHeaders.put(next.getKey().toLowerCase(), next.getValue());
}
}
}
public void addHeader(String theName, String theValue) {
String lowerCase = theName.toLowerCase();
ArrayList<String> list = myHeaders.get(lowerCase);
List<String> list = myHeaders.get(lowerCase);
if (list == null) {
list = new ArrayList<>();
myHeaders.put(lowerCase, list);
@ -43,7 +57,7 @@ public class ServletSubRequestDetails extends ServletRequestDetails {
@Override
public String getHeader(String theName) {
ArrayList<String> list = myHeaders.get(theName.toLowerCase());
List<String> list = myHeaders.get(theName.toLowerCase());
if (list == null || list.isEmpty()) {
return null;
}
@ -52,7 +66,7 @@ public class ServletSubRequestDetails extends ServletRequestDetails {
@Override
public List<String> getHeaders(String theName) {
ArrayList<String> list = myHeaders.get(theName.toLowerCase());
List<String> list = myHeaders.get(theName.toLowerCase());
if (list == null || list.isEmpty()) {
return null;
}

View File

@ -64,7 +64,13 @@ public class SubscriptionInterceptorLoader {
mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class);
}
ourLog.info("Registering subscription matcher interceptor");
if (mySubscriptionMatcherInterceptor == null) {
mySubscriptionMatcherInterceptor = myAppicationContext.getBean(SubscriptionMatcherInterceptor.class);
}
myDaoConfig.registerInterceptor(mySubscriptionMatcherInterceptor);
}
}

View File

@ -42,10 +42,7 @@ public class DaoSubscriptionProvider implements ISubscriptionProvider {
@Override
public IBundleProvider search(SearchParameterMap theMap) {
IFhirResourceDao subscriptionDao = myDaoRegistry.getSubscriptionDao();
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
return subscriptionDao.search(theMap, req);
return subscriptionDao.search(theMap);
}
@Override

View File

@ -72,12 +72,9 @@ public class DaoSubscriptionMatcher implements ISubscriptionMatcher {
RuntimeResourceDefinition responseResourceDef = subscriptionDao.validateCriteriaAndReturnResourceDefinition(theCriteria);
SearchParameterMap responseCriteriaUrl = myMatchUrlService.translateMatchUrl(theCriteria, responseResourceDef);
RequestDetails req = new ServletSubRequestDetails();
req.setSubRequest(true);
IFhirResourceDao<? extends IBaseResource> responseDao = myDaoRegistry.getResourceDao(responseResourceDef.getImplementingClass());
responseCriteriaUrl.setLoadSynchronousUpTo(1);
return responseDao.search(responseCriteriaUrl, req);
return responseDao.search(responseCriteriaUrl);
}
}

View File

@ -4,6 +4,8 @@ import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.client.interceptor.SimpleRequestHeaderInterceptor;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
@ -28,6 +30,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.Matchers.in;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.*;
@ -148,6 +151,58 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
}
@Test
public void testReadInTransaction() {
Patient patient = new Patient();
patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100");
patient.addName().setFamily("Tester").addGiven("Raghad");
IIdType id = ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute().getId().toUnqualifiedVersionless();
ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
String authHeader = theRequestDetails.getHeader("Authorization");
if (!"Bearer AAA".equals(authHeader)) {
throw new AuthenticationException("Invalid auth header: " + authHeader);
}
return new RuleBuilder()
.allow().transaction().withAnyOperation().andApplyNormalRules().andThen()
.allow().read().resourcesOfType(Patient.class).withAnyId()
.build();
}
});
SimpleRequestHeaderInterceptor interceptor = new SimpleRequestHeaderInterceptor("Authorization", "Bearer AAA");
try {
ourClient.registerInterceptor(interceptor);
Bundle bundle;
Bundle responseBundle;
// Read
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl(id.getValue());
responseBundle = ourClient.transaction().withBundle(bundle).execute();
patient = (Patient) responseBundle.getEntry().get(0).getResource();
assertEquals("Tester", patient.getNameFirstRep().getFamily());
// Search
bundle = new Bundle();
bundle.setType(Bundle.BundleType.TRANSACTION);
bundle.addEntry().getRequest().setMethod(Bundle.HTTPVerb.GET).setUrl("Patient?");
responseBundle = ourClient.transaction().withBundle(bundle).execute();
responseBundle = (Bundle) responseBundle.getEntry().get(0).getResource();
patient = (Patient) responseBundle.getEntry().get(0).getResource();
assertEquals("Tester", patient.getNameFirstRep().getFamily());
} finally {
ourClient.unregisterInterceptor(interceptor);
}
}
/**
* See #751
*/

View File

@ -1,5 +1,6 @@
package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View File

@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu2;
@ -72,6 +73,11 @@ public class TdlDstu2Config extends BaseJavaConfigDstu2 {
return retVal;
}
@Bean
public ModelConfig modelConfig() {
return daoConfig().getModelConfig();
}
@Bean(name = "myPersistenceDataSourceDstu1", destroyMethod = "close")
public DataSource dataSource() {
BasicDataSource retVal = new BasicDataSource();

View File

@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
import ca.uhn.fhir.jpa.util.SubscriptionsRequireManualActivationInterceptorDstu3;
@ -59,6 +60,11 @@ public class TdlDstu3Config extends BaseJavaConfigDstu3 {
return retVal;
}
@Bean
public ModelConfig modelConfig() {
return daoConfig().getModelConfig();
}
@Bean(name = "myPersistenceDataSourceDstu3", destroyMethod = "close")
public DataSource dataSource() {
BasicDataSource retVal = new BasicDataSource();

View File

@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu2;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
@ -71,6 +72,11 @@ public class TestDstu2Config extends BaseJavaConfigDstu2 {
return retVal;
}
@Bean
public ModelConfig modelConfig() {
return daoConfig().getModelConfig();
}
@Bean(name = "myPersistenceDataSourceDstu1", destroyMethod = "close")
public DataSource dataSource() {
BasicDataSource retVal = new BasicDataSource();

View File

@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.config.BaseJavaConfigDstu3;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
@ -64,6 +65,12 @@ public class TestDstu3Config extends BaseJavaConfigDstu3 {
return retVal;
}
@Bean
public ModelConfig modelConfig() {
return daoConfig().getModelConfig();
}
@Override
@Bean(autowire = Autowire.BY_TYPE)
public DatabaseBackedPagingProvider databaseBackedPagingProvider() {

View File

@ -2,6 +2,7 @@ package ca.uhn.fhirtest.config;
import ca.uhn.fhir.jpa.config.BaseJavaConfigR4;
import ca.uhn.fhir.jpa.dao.DaoConfig;
import ca.uhn.fhir.jpa.model.entity.ModelConfig;
import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider;
import ca.uhn.fhir.jpa.search.LuceneSearchMappingFactory;
import ca.uhn.fhir.jpa.util.DerbyTenSevenHapiFhirDialect;
@ -63,6 +64,12 @@ public class TestR4Config extends BaseJavaConfigR4 {
return retVal;
}
@Bean
public ModelConfig modelConfig() {
return daoConfig().getModelConfig();
}
@Bean(name = "myPersistenceDataSourceR4", destroyMethod = "close")
public DataSource dataSource() {
BasicDataSource retVal = new BasicDataSource();

View File

@ -17,7 +17,7 @@ public class UhnFhirTestApp {
public static void main(String[] args) throws Exception {
int myPort = 8888;
int myPort = 8889;
String base = "http://localhost:" + myPort + "/baseDstu2";
// new File("target/testdb").mkdirs();

View File

@ -59,6 +59,10 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
</dependency>
</dependencies>

View File

@ -54,6 +54,8 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
* <a href="http://jamesagnew.github.io/hapi-fhir/doc_rest_server_security.html">Documentation on Server Security</a>
* for information on how to use this interceptor.
* </p>
*
* @see SearchNarrowingInterceptor
*/
public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter implements IRuleApplier {

View File

@ -0,0 +1,84 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.Validate;
import java.util.ArrayList;
import java.util.List;
/**
* Return type for {@link SearchNarrowingInterceptor#buildAuthorizedList(RequestDetails)}
*/
public class AuthorizedList {
private List<String> myCompartments;
private List<String> myResources;
List<String> getCompartments() {
return myCompartments;
}
List<String> getResources() {
return myResources;
}
/**
* Adds a compartment that the user should be allowed to access
*
* @param theCompartment The compartment name, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 as well as Observations where Observation.subject="Patient/123"m, etc.
* @return Returns <code>this</code> for easy method chaining
*/
public AuthorizedList addCompartment(String theCompartment) {
Validate.notNull(theCompartment, "theCompartment must not be null");
if (myCompartments == null) {
myCompartments = new ArrayList<>();
}
myCompartments.add(theCompartment);
return this;
}
/**
* Adds a compartment that the user should be allowed to access
*
* @param theCompartments The compartment names, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 as well as Observations where Observation.subject="Patient/123"m, etc.
* @return Returns <code>this</code> for easy method chaining
*/
public AuthorizedList addCompartments(String... theCompartments) {
Validate.notNull(theCompartments, "theCompartments must not be null");
for (String next : theCompartments) {
addCompartment(next);
}
return this;
}
/**
* Adds a resource that the user should be allowed to access
*
* @param theResource The resource name, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 but not Observations where Observation.subject="Patient/123"m, etc.
* @return Returns <code>this</code> for easy method chaining
*/
public AuthorizedList addResource(String theResource) {
Validate.notNull(theResource, "theResource must not be null");
if (myResources == null) {
myResources = new ArrayList<>();
}
myResources.add(theResource);
return this;
}
/**
* Adds a resource that the user should be allowed to access
*
* @param theResources The resource names, e.g. "Patient/123" (in this example the user would be allowed to access Patient/123 but not Observations where Observation.subject="Patient/123"m, etc.
* @return Returns <code>this</code> for easy method chaining
*/
public AuthorizedList addResources(String... theResources) {
Validate.notNull(theResources, "theResources must not be null");
for (String next : theResources) {
addResource(next);
}
return this;
}
}

View File

@ -0,0 +1,198 @@
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.rest.api.QualifiedParamList;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.param.ParameterUtil;
import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter;
import org.apache.commons.collections4.ListUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
/**
* This interceptor can be used to automatically narrow the scope of searches in order to
* automatically restrict the searches to specific compartments.
* <p>
* For example, this interceptor
* could be used to restrict a user to only viewing data belonging to Patient/123 (i.e. data
* in the <code>Patient/123</code> compartment). In this case, a user performing a search
* for<br/>
* <code>http://baseurl/Observation?category=laboratory</code><br/>
* would receive results as though they had requested<br/>
* <code>http://baseurl/Observation?subject=Patient/123&category=laboratory</code>
* </p>
* <p>
* Note that this interceptor should be used in combination with {@link AuthorizationInterceptor}
* if you are restricting results because of a security restriction. This interceptor is not
* intended to be a failsafe way of preventing users from seeing the wrong data (that is the
* purpose of AuthorizationInterceptor). This interceptor is simply intended as a convenience to
* help users simplify their queries while not receiving security errors for to trying to access
* data they do not have access to see.
* </p>
*
* @see AuthorizationInterceptor
*/
public abstract class SearchNarrowingInterceptor extends InterceptorAdapter {
/**
* Subclasses should override this method to supply the set of compartments that
* the user making the request should actually have access to.
* <p>
* Typically this is done by examining <code>theRequestDetails</code> to find
* out who the current user is and then building a list of Strings.
* </p>
*
* @param theRequestDetails The individual request currently being applied
*/
protected AuthorizedList buildAuthorizedList(@SuppressWarnings("unused") RequestDetails theRequestDetails) {
return new AuthorizedList();
}
@Override
public boolean incomingRequestPostProcessed(RequestDetails theRequestDetails, HttpServletRequest theRequest, HttpServletResponse theResponse) throws AuthenticationException {
// We don't support this operation type yet
Validate.isTrue(theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_SYSTEM);
if (theRequestDetails.getRestOperationType() != RestOperationTypeEnum.SEARCH_TYPE) {
return true;
}
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
RuntimeResourceDefinition resDef = ctx.getResourceDefinition(theRequestDetails.getResourceName());
HashMap<String, List<String>> parameterToOrValues = new HashMap<>();
AuthorizedList authorizedList = buildAuthorizedList(theRequestDetails);
/*
* Create a map of search parameter values that need to be added to the
* given request
*/
Collection<String> compartments = authorizedList.getCompartments();
if (compartments != null) {
processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, compartments, true);
}
Collection<String> resources = authorizedList.getResources();
if (resources != null) {
processResourcesOrCompartments(theRequestDetails, resDef, parameterToOrValues, resources, false);
}
/*
* Add any param values to the actual request
*/
if (parameterToOrValues.size() > 0) {
Map<String, String[]> newParameters = new HashMap<>(theRequestDetails.getParameters());
for (Map.Entry<String, List<String>> nextEntry : parameterToOrValues.entrySet()) {
String nextParamName = nextEntry.getKey();
List<String> nextAllowedValues = nextEntry.getValue();
if (!newParameters.containsKey(nextParamName)) {
/*
* If we don't already have a parameter of the given type, add one
*/
String nextValuesJoined = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
String[] paramValues = {nextValuesJoined};
newParameters.put(nextParamName, paramValues);
} else {
/*
* If the client explicitly requested the given parameter already, we'll
* just update the request to have the intersection of the values that the client
* requested, and the values that the user is allowed to see
*/
String[] existingValues = newParameters.get(nextParamName);
boolean restrictedExistingList = false;
for (int i = 0; i < existingValues.length; i++) {
String nextExistingValue = existingValues[i];
List<String> nextRequestedValues = QualifiedParamList.splitQueryStringByCommasIgnoreEscape(null, nextExistingValue);
List<String> nextPermittedValues = ListUtils.intersection(nextRequestedValues, nextAllowedValues);
if (nextPermittedValues.size() > 0) {
restrictedExistingList = true;
existingValues[i] = ParameterUtil.escapeAndJoinOrList(nextPermittedValues);
}
}
/*
* If none of the values that were requested by the client overlap at all
* with the values that the user is allowed to see, we'll just add the permitted
* list as a new list. Ultimately this scenario actually means that the client
* shouldn't get *any* results back, and adding a new AND parameter (that doesn't
* overlap at all with the others) is one way of ensuring that.
*/
if (!restrictedExistingList) {
String[] newValues = Arrays.copyOf(existingValues, existingValues.length + 1);
newValues[existingValues.length] = ParameterUtil.escapeAndJoinOrList(nextAllowedValues);
newParameters.put(nextParamName, newValues);
}
}
}
theRequestDetails.setParameters(newParameters);
}
return true;
}
private void processResourcesOrCompartments(RequestDetails theRequestDetails, RuntimeResourceDefinition theResDef, HashMap<String, List<String>> theParameterToOrValues, Collection<String> theResourcesOrCompartments, boolean theAreCompartments) {
String lastCompartmentName = null;
String lastSearchParamName=null;
for (String nextCompartment : theResourcesOrCompartments) {
Validate.isTrue(StringUtils.countMatches(nextCompartment, '/') == 1, "Invalid compartment name (must be in form \"ResourceType/xxx\": %s", nextCompartment);
String compartmentName = nextCompartment.substring(0, nextCompartment.indexOf('/'));
String searchParamName = null;
if (compartmentName.equalsIgnoreCase(lastCompartmentName)) {
// Avoid doing a lookup for the same thing repeatedly
searchParamName = lastSearchParamName;
} else {
if (compartmentName.equalsIgnoreCase(theRequestDetails.getResourceName())) {
searchParamName = "_id";
} else if (theAreCompartments) {
List<RuntimeSearchParam> searchParams = theResDef.getSearchParamsForCompartmentName(compartmentName);
if (searchParams.size() > 0) {
// Resources like Observation have several fields that add the resource to
// the compartment. In the case of Observation, it's subject, patient and performer.
// For this kind of thing, we'll prefer the one called "patient".
RuntimeSearchParam searchParam =
searchParams
.stream()
.filter(t -> t.getName().equalsIgnoreCase(compartmentName))
.findFirst()
.orElse(searchParams.get(0));
searchParamName = searchParam.getName();
}
}
lastCompartmentName = compartmentName;
lastSearchParamName = searchParamName;
}
if (searchParamName != null) {
List<String> orValues = theParameterToOrValues.computeIfAbsent(searchParamName, t -> new ArrayList<>());
orValues.add(nextCompartment);
}
}
}
}

View File

@ -34,9 +34,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.*;
import java.util.zip.GZIPInputStream;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@ -145,4 +143,18 @@ public class ServletRequestDetails extends RequestDetails {
this.myServletResponse = myServletResponse;
}
public Map<String,List<String>> getHeaders() {
Map<String, List<String>> retVal = new HashMap<>();
Enumeration<String> names = myServletRequest.getHeaderNames();
while (names.hasMoreElements()) {
String nextName = names.nextElement();
ArrayList<String> headerValues = new ArrayList<>();
retVal.put(nextName, headerValues);
Enumeration<String> valuesEnum = myServletRequest.getHeaders(nextName);
while (valuesEnum.hasMoreElements()) {
headerValues.add(valuesEnum.nextElement());
}
}
return Collections.unmodifiableMap(retVal);
}
}

View File

@ -387,16 +387,24 @@ public class ServerConformanceProvider implements IServerConformanceProvider<Con
}
}
RestResourceSearchParam param = resource.addSearchParam();
String finalNextParamUnchainedName = nextParamUnchainedName;
RestResourceSearchParam param =
resource
.getSearchParam()
.stream()
.filter(t -> t.getName().equals(finalNextParamUnchainedName))
.findFirst()
.orElseGet(() -> resource.addSearchParam());
param.setName(nextParamUnchainedName);
if (StringUtils.isNotBlank(chain)) {
param.addChain(chain);
}
if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
for (String nextWhitelist : new TreeSet<String>(nextParameter.getQualifierWhitelist())) {
if (nextWhitelist.startsWith(".")) {
param.addChain(nextWhitelist.substring(1));
} else {
if (nextParameter.getParamType() == RestSearchParameterTypeEnum.REFERENCE) {
for (String nextWhitelist : new TreeSet<String>(nextParameter.getQualifierWhitelist())) {
if (nextWhitelist.startsWith(".")) {
param.addChain(nextWhitelist.substring(1));
}
}
}
}

View File

@ -579,6 +579,45 @@ public class ServerConformanceProviderDstu2Test {
assertEquals(2, param.getChain().size());
}
@Test
public void testSearchReferenceParameterWithExplicitChainsDocumentation() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new SearchProviderWithExplicitChains());
ServerConformanceProvider sc = new ServerConformanceProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
boolean found = false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings();
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().get(0);
assertEquals("The organization at which this person is a patient", param.getDescription());
found = true;
}
}
assertTrue(found);
Conformance conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
RestResource resource = findRestResource(conformance, "Patient");
assertEquals(1, resource.getSearchParam().size());
RestResourceSearchParam param = resource.getSearchParam().get(0);
assertEquals("organization", param.getName());
assertEquals("bar", param.getChain().get(0).getValue());
assertEquals("baz.bob", param.getChain().get(1).getValue());
assertEquals("foo", param.getChain().get(2).getValue());
assertEquals(3, param.getChain().size());
}
@Test
public void testSystemHistorySupported() throws Exception {
@ -851,6 +890,19 @@ public class ServerConformanceProviderDstu2Test {
}
public static class SearchProviderWithExplicitChains {
@Search(type = Patient.class)
public Patient findPatient1(
@Description(shortDefinition = "The organization at which this person is a patient")
@RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo,
@RequiredParam(name = "organization.bar") ReferenceAndListParam theBar,
@RequiredParam(name = "organization.baz.bob") ReferenceAndListParam theBazbob) {
return null;
}
}
public static class SystemHistoryProvider {
@History

View File

@ -1,56 +1,50 @@
package org.hl7.fhir.dstu3.hapi.rest.server;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.util.*;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import ca.uhn.fhir.model.primitive.InstantDt;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.CapabilityStatement.*;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.AfterClass;
import org.junit.Test;
import com.google.common.collect.Lists;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.model.api.annotation.Description;
import ca.uhn.fhir.model.primitive.InstantDt;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.MethodOutcome;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.param.*;
import ca.uhn.fhir.rest.server.*;
import ca.uhn.fhir.rest.server.method.*;
import ca.uhn.fhir.rest.server.IResourceProvider;
import ca.uhn.fhir.rest.server.ResourceBinding;
import ca.uhn.fhir.rest.server.RestfulServer;
import ca.uhn.fhir.rest.server.RestulfulServerConfiguration;
import ca.uhn.fhir.rest.server.method.BaseMethodBinding;
import ca.uhn.fhir.rest.server.method.IParameter;
import ca.uhn.fhir.rest.server.method.SearchMethodBinding;
import ca.uhn.fhir.rest.server.method.SearchParameter;
import ca.uhn.fhir.util.TestUtil;
import ca.uhn.fhir.validation.FhirValidator;
import ca.uhn.fhir.validation.ValidationResult;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;
import com.google.common.collect.Lists;
import org.hl7.fhir.dstu3.model.*;
import org.hl7.fhir.dstu3.model.CapabilityStatement.*;
import org.hl7.fhir.dstu3.model.Enumerations.PublicationStatus;
import org.hl7.fhir.dstu3.model.OperationDefinition.OperationDefinitionParameterComponent;
import org.hl7.fhir.dstu3.model.OperationDefinition.OperationKind;
import org.hl7.fhir.dstu3.model.OperationDefinition.OperationParameterUse;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.junit.AfterClass;
import org.junit.Ignore;
import org.junit.Test;
import javax.servlet.ServletConfig;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class ServerCapabilityStatementProviderDstu3Test {
private static FhirContext ourCtx;
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ServerCapabilityStatementProviderDstu3Test.class);
private static FhirContext ourCtx;
private static FhirValidator ourValidator;
static {
@ -88,6 +82,47 @@ public class ServerCapabilityStatementProviderDstu3Test {
return resource;
}
@Test
@Ignore
public void testSearchReferenceParameterWithExplicitChainsDocumentation() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
rs.setProviders(new SearchProviderWithExplicitChains());
ServerCapabilityStatementProvider sc = new ServerCapabilityStatementProvider(rs);
rs.setServerConformanceProvider(sc);
rs.init(createServletConfig());
boolean found = false;
Collection<ResourceBinding> resourceBindings = rs.getResourceBindings();
for (ResourceBinding resourceBinding : resourceBindings) {
if (resourceBinding.getResourceName().equals("Patient")) {
List<BaseMethodBinding<?>> methodBindings = resourceBinding.getMethodBindings();
SearchMethodBinding binding = (SearchMethodBinding) methodBindings.get(0);
SearchParameter param = (SearchParameter) binding.getParameters().get(0);
assertEquals("The organization at which this person is a patient", param.getDescription());
found = true;
}
}
assertTrue(found);
CapabilityStatement conformance = sc.getServerConformance(createHttpServletRequest());
String conf = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(conf);
CapabilityStatementRestResourceComponent resource = findRestResource(conformance, "Patient");
assertEquals(1, resource.getSearchParam().size());
CapabilityStatementRestResourceSearchParamComponent param = resource.getSearchParam().get(0);
assertEquals("organization", param.getName());
// assertEquals("bar", param.getChain().get(0).getValue());
// assertEquals("baz.bob", param.getChain().get(1).getValue());
// assertEquals("foo", param.getChain().get(2).getValue());
// assertEquals(3, param.getChain().size());
}
@Test
public void testConditionalOperations() throws Exception {
@ -235,7 +270,9 @@ public class ServerCapabilityStatementProviderDstu3Test {
assertNull(res.getConditionalUpdateElement().getValue());
}
/** See #379 */
/**
* See #379
*/
@Test
public void testOperationAcrossMultipleTypes() throws Exception {
RestfulServer rs = new RestfulServer(ourCtx);
@ -544,7 +581,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Test
public void testSearchReferenceParameterWithList() throws Exception {
RestfulServer rsNoType = new RestfulServer(ourCtx){
RestfulServer rsNoType = new RestfulServer(ourCtx) {
@Override
public RestulfulServerConfiguration createConfiguration() {
RestulfulServerConfiguration retVal = super.createConfiguration();
@ -561,7 +598,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
String confNoType = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(conformance);
ourLog.info(confNoType);
RestfulServer rsWithType = new RestfulServer(ourCtx){
RestfulServer rsWithType = new RestfulServer(ourCtx) {
@Override
public RestulfulServerConfiguration createConfiguration() {
RestulfulServerConfiguration retVal = super.createConfiguration();
@ -720,8 +757,8 @@ public class ServerCapabilityStatementProviderDstu3Test {
assertThat(param.getUse(), is(OperationParameterUse.IN));
CapabilityStatementRestResourceComponent patientResource = restComponent.getResource().stream()
.filter(r -> patientResourceName.equals(r.getType()))
.findAny().get();
.filter(r -> patientResourceName.equals(r.getType()))
.findAny().get();
assertThat("Named query parameters should not appear in the resource search params", patientResource.getSearchParam(), is(empty()));
}
@ -787,9 +824,17 @@ public class ServerCapabilityStatementProviderDstu3Test {
assertTrue(outcome, result.isSuccessful());
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
public static class SearchProviderWithExplicitChains {
@Search(type = Patient.class)
public Patient findPatient1(
@Description(shortDefinition = "The organization at which this person is a patient")
@RequiredParam(name = "organization.foo") ReferenceAndListParam theFoo,
@RequiredParam(name = "organization.bar") ReferenceAndListParam theBar,
@RequiredParam(name = "organization.baz.bob") ReferenceAndListParam theBazbob) {
return null;
}
}
@SuppressWarnings("unused")
@ -836,7 +881,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Search(type = Patient.class)
public Patient findPatient(@Description(shortDefinition = "The patient's identifier") @OptionalParam(name = Patient.SP_IDENTIFIER) TokenParam theIdentifier,
@Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) {
@Description(shortDefinition = "The patient's name") @OptionalParam(name = Patient.SP_NAME) StringParam theName) {
return null;
}
@ -847,7 +892,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Operation(name = "someOp")
public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId,
@OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) {
@OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Encounter theEnd) {
return null;
}
@ -868,7 +913,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Operation(name = "someOp")
public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId,
@OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) {
@OperationParam(name = "someOpParam1") DateType theStart, @OperationParam(name = "someOpParam2") Patient theEnd) {
return null;
}
@ -912,9 +957,9 @@ public class ServerCapabilityStatementProviderDstu3Test {
@SuppressWarnings("unused")
public static class PlainProviderWithExtendedOperationOnNoType {
@Operation(name = "plain", idempotent = true, returnParameters = { @OperationParam(min = 1, max = 2, name = "out1", type = StringType.class) })
@Operation(name = "plain", idempotent = true, returnParameters = {@OperationParam(min = 1, max = 2, name = "out1", type = StringType.class)})
public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart,
@OperationParam(name = "end") DateType theEnd) {
@OperationParam(name = "end") DateType theEnd) {
return null;
}
@ -925,7 +970,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Operation(name = "everything", idempotent = true)
public IBundleProvider everything(javax.servlet.http.HttpServletRequest theServletRequest, @IdParam IdType theId, @OperationParam(name = "start") DateType theStart,
@OperationParam(name = "end") DateType theEnd) {
@OperationParam(name = "end") DateType theEnd) {
return null;
}
@ -942,8 +987,8 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Description(shortDefinition = "This is a search for stuff!")
@Search
public List<DiagnosticReport> findDiagnosticReportsByPatient(@RequiredParam(name = DiagnosticReport.SP_SUBJECT + '.' + Patient.SP_IDENTIFIER) TokenParam thePatientId,
@OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange,
@IncludeParam(allow = { "DiagnosticReport.result" }) Set<Include> theIncludes) throws Exception {
@OptionalParam(name = DiagnosticReport.SP_CODE) TokenOrListParam theNames, @OptionalParam(name = DiagnosticReport.SP_DATE) DateRangeParam theDateRange,
@IncludeParam(allow = {"DiagnosticReport.result"}) Set<Include> theIncludes) throws Exception {
return null;
}
@ -974,7 +1019,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
@Search(type = Patient.class)
public Patient findPatient2(
@Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = { Patient.class }) ReferenceAndListParam theLink) {
@Description(shortDefinition = "All patients linked to the given patient") @OptionalParam(name = "link", targetTypes = {Patient.class}) ReferenceAndListParam theLink) {
return null;
}
@ -984,15 +1029,15 @@ public class ServerCapabilityStatementProviderDstu3Test {
public static class SearchProviderWithWhitelist {
@Search(type = Patient.class)
public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = { "foo",
"bar" }) ReferenceAndListParam theIdentifier) {
public Patient findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION, chainWhitelist = {"foo",
"bar"}) ReferenceAndListParam theIdentifier) {
return null;
}
}
@SuppressWarnings("unused")
public static class SearchProviderWithListNoType implements IResourceProvider {
public static class SearchProviderWithListNoType implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
@ -1000,7 +1045,6 @@ public class ServerCapabilityStatementProviderDstu3Test {
}
@Search()
public List<Patient> findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) {
return null;
@ -1009,7 +1053,7 @@ public class ServerCapabilityStatementProviderDstu3Test {
}
@SuppressWarnings("unused")
public static class SearchProviderWithListWithType implements IResourceProvider {
public static class SearchProviderWithListWithType implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
@ -1017,15 +1061,13 @@ public class ServerCapabilityStatementProviderDstu3Test {
}
@Search(type=Patient.class)
@Search(type = Patient.class)
public List<Patient> findPatient1(@Description(shortDefinition = "The organization at which this person is a patient") @RequiredParam(name = Patient.SP_ORGANIZATION) ReferenceAndListParam theIdentifier) {
return null;
}
}
public static class SystemHistoryProvider {
@History
@ -1110,4 +1152,9 @@ public class ServerCapabilityStatementProviderDstu3Test {
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -1,16 +1,19 @@
package ca.uhn.fhir.rest.client;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.*;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.annotation.IncludeParam;
import ca.uhn.fhir.rest.annotation.RequiredParam;
import ca.uhn.fhir.rest.annotation.Search;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.param.TokenAndListParam;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.TestUtil;
import com.google.common.base.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.input.ReaderInputStream;
import org.apache.http.HttpResponse;
@ -23,118 +26,144 @@ import org.apache.http.message.BasicStatusLine;
import org.hamcrest.Matchers;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Encounter;
import org.junit.*;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.Include;
import ca.uhn.fhir.rest.annotation.*;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.client.api.IBasicClient;
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
import ca.uhn.fhir.rest.param.TokenOrListParam;
import ca.uhn.fhir.rest.param.TokenParam;
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
import ca.uhn.fhir.util.TestUtil;
import java.io.StringReader;
import java.nio.charset.Charset;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public class SearchClientTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchClientTest.class);
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(SearchClientTest.class);
private FhirContext ourCtx;
private HttpClient ourHttpClient;
private HttpResponse ourHttpResponse;
private FhirContext ourCtx;
private HttpClient ourHttpClient;
private HttpResponse ourHttpResponse;
@Before
public void before() {
ourCtx = FhirContext.forR4();
@Before
public void before() {
ourCtx = FhirContext.forR4();
ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient);
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient);
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
}
@Test
public void testPostOnLongParamsList() throws Exception {
String resp = createBundle();
@Test
public void testPostOnLongParamsList() throws Exception {
String resp = createBundle();
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8")));
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenAnswer(t->new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8")));
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
Set<Include> includes = new HashSet<Include>();
includes.add(new Include("one"));
includes.add(new Include("two"));
TokenOrListParam params = new TokenOrListParam();
for (int i = 0; i < 1000; i++) {
params.add(new TokenParam("system", "value"));
}
List<Encounter> found = client.searchByList(params, includes);
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
Set<Include> includes = new HashSet<Include>();
includes.add(new Include("one"));
includes.add(new Include("two"));
TokenOrListParam params = new TokenOrListParam();
for (int i = 0; i < 1000; i++) {
params.add(new TokenParam("system", "value"));
}
assertEquals(1, found.size());
// With OR list
Encounter encounter = found.get(0);
assertNotNull(encounter.getSubject().getReference());
HttpUriRequest value = capt.getValue();
List<Encounter> found = client.searchByList(params, includes);
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
HttpPost post = (HttpPost) value;
String body = IOUtils.toString(post.getEntity().getContent());
ourLog.info(body);
assertThat(body, Matchers.containsString("_include=one"));
assertThat(body, Matchers.containsString("_include=two"));
}
assertEquals(1, found.size());
@Test
public void testReturnTypedList() throws Exception {
Encounter encounter = found.get(0);
assertNotNull(encounter.getSubject().getReference());
HttpUriRequest value = capt.getValue();
String resp = createBundle();
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
HttpPost post = (HttpPost) value;
String body = IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(body);
assertThat(body, Matchers.containsString("_include=one"));
assertThat(body, Matchers.containsString("_include=two"));
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8")));
// With AND list
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
List<Encounter> found = client.search();
assertEquals(1, found.size());
TokenAndListParam paramsAndList = new TokenAndListParam();
paramsAndList.addAnd(params);
found = client.searchByList(paramsAndList, includes);
Encounter encounter = found.get(0);
assertNotNull(encounter.getSubject().getReference());
}
assertEquals(1, found.size());
private String createBundle() {
Bundle bundle = new Bundle();
encounter = found.get(0);
assertNotNull(encounter.getSubject().getReference());
value = capt.getAllValues().get(1);
Encounter enc = new Encounter();
enc.getSubject().setReference("Patient/1");
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
post = (HttpPost) value;
body = IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8);
ourLog.info(body);
assertThat(body, Matchers.containsString("_include=one"));
assertThat(body, Matchers.containsString("_include=two"));
}
bundle.addEntry().setResource(enc);
@Test
public void testReturnTypedList() throws Exception {
String retVal = ourCtx.newXmlParser().encodeResourceToString(bundle);
return retVal;
}
String resp = createBundle();
private interface ITestClient extends IBasicClient {
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8"));
when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8")));
@Search
List<Encounter> search();
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
List<Encounter> found = client.search();
assertEquals(1, found.size());
@Search
List<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set<Include> theIncludes) throws BaseServerResponseException;
Encounter encounter = found.get(0);
assertNotNull(encounter.getSubject().getReference());
}
}
private String createBundle() {
Bundle bundle = new Bundle();
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
Encounter enc = new Encounter();
enc.getSubject().setReference("Patient/1");
bundle.addEntry().setResource(enc);
String retVal = ourCtx.newXmlParser().encodeResourceToString(bundle);
return retVal;
}
private interface ITestClient extends IBasicClient {
@Search
List<Encounter> search();
@Search
List<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set<Include> theIncludes) throws BaseServerResponseException;
@Search
List<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenAndListParam tokenOrListParam, @IncludeParam Set<Include> theIncludes) throws BaseServerResponseException;
}
@AfterClass
public static void afterClassClearContext() {
TestUtil.clearAllStaticFieldsForUnitTest();
}
}

View File

@ -0,0 +1,300 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.model.api.IQueryParameterOr;
import ca.uhn.fhir.model.api.IQueryParameterType;
import ca.uhn.fhir.rest.annotation.OptionalParam;
import ca.uhn.fhir.rest.annotation.Search;
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.param.BaseAndListParam;
import ca.uhn.fhir.rest.param.ReferenceAndListParam;
import ca.uhn.fhir.rest.param.StringAndListParam;
import ca.uhn.fhir.rest.param.TokenAndListParam;
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.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.lang3.Validate;
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;
import org.hl7.fhir.r4.model.Observation;
import org.hl7.fhir.r4.model.Patient;
import org.hl7.fhir.r4.model.Resource;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
public class SearchNarrowingInterceptorTest {
private static String ourLastHitMethod;
private static FhirContext ourCtx;
private static TokenAndListParam ourLastIdParam;
private static TokenAndListParam ourLastCodeParam;
private static ReferenceAndListParam ourLastSubjectParam;
private static ReferenceAndListParam ourLastPatientParam;
private static ReferenceAndListParam ourLastPerformerParam;
private static StringAndListParam ourLastNameParam;
private static List<Resource> ourReturn;
private static Server ourServer;
private static IGenericClient ourClient;
private static AuthorizedList ourNextCompartmentList;
@Before
public void before() {
ourLastHitMethod = null;
ourReturn = Collections.emptyList();
ourLastIdParam = null;
ourLastNameParam = null;
ourLastSubjectParam = null;
ourLastPatientParam = null;
ourLastPerformerParam = null;
ourLastCodeParam = null;
ourNextCompartmentList = null;
}
@Test
public void testNarrowObservationsByPatientContext_ClientRequestedNoParams() {
ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Observation")
.execute();
assertEquals("Observation.search", ourLastHitMethod);
assertNull(ourLastIdParam);
assertNull(ourLastCodeParam);
assertNull(ourLastSubjectParam);
assertNull(ourLastPerformerParam);
assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/123,Patient/456"));
}
/**
* Should not make any changes
*/
@Test
public void testNarrowObservationsByPatientResources_ClientRequestedNoParams() {
ourNextCompartmentList = new AuthorizedList().addResources("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Observation")
.execute();
assertEquals("Observation.search", ourLastHitMethod);
assertNull(ourLastIdParam);
assertNull(ourLastCodeParam);
assertNull(ourLastSubjectParam);
assertNull(ourLastPerformerParam);
assertNull(ourLastPatientParam);
}
@Test
public void testNarrowPatientByPatientResources_ClientRequestedNoParams() {
ourNextCompartmentList = new AuthorizedList().addResources("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Patient")
.execute();
assertEquals("Patient.search", ourLastHitMethod);
assertNull(ourLastCodeParam);
assertNull(ourLastSubjectParam);
assertNull(ourLastPerformerParam);
assertNull(ourLastPatientParam);
assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456"));
}
@Test
public void testNarrowPatientByPatientContext_ClientRequestedNoParams() {
ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Patient")
.execute();
assertEquals("Patient.search", ourLastHitMethod);
assertNull(ourLastNameParam);
assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123,Patient/456"));
}
@Test
public void testNarrowPatientByPatientContext_ClientRequestedSomeOverlap() {
ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Patient")
.where(IAnyResource.RES_ID.exactly().codes("Patient/123", "Patient/999"))
.execute();
assertEquals("Patient.search", ourLastHitMethod);
assertNull(ourLastNameParam);
assertThat(toStrings(ourLastIdParam), Matchers.contains("Patient/123"));
}
@Test
public void testNarrowObservationsByPatientContext_ClientRequestedSomeOverlap() {
ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("Patient/456", "Patient/777"))
.and(Observation.PATIENT.hasAnyOfIds("Patient/456", "Patient/888"))
.execute();
assertEquals("Observation.search", ourLastHitMethod);
assertNull(ourLastIdParam);
assertNull(ourLastCodeParam);
assertNull(ourLastSubjectParam);
assertNull(ourLastPerformerParam);
assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/456", "Patient/456"));
}
@Test
public void testNarrowObservationsByPatientContext_ClientRequestedNoOverlap() {
ourNextCompartmentList = new AuthorizedList().addCompartments("Patient/123", "Patient/456");
ourClient
.search()
.forResource("Observation")
.where(Observation.PATIENT.hasAnyOfIds("Patient/111", "Patient/777"))
.and(Observation.PATIENT.hasAnyOfIds("Patient/111", "Patient/888"))
.execute();
assertEquals("Observation.search", ourLastHitMethod);
assertNull(ourLastIdParam);
assertNull(ourLastCodeParam);
assertNull(ourLastSubjectParam);
assertNull(ourLastPerformerParam);
assertThat(toStrings(ourLastPatientParam), Matchers.contains("Patient/111,Patient/777", "Patient/111,Patient/888", "Patient/123,Patient/456"));
}
private List<String> toStrings(BaseAndListParam<? extends IQueryParameterOr<?>> theParams) {
List<? extends IQueryParameterOr<? extends IQueryParameterType>> valuesAsQueryTokens = theParams.getValuesAsQueryTokens();
return valuesAsQueryTokens
.stream()
.map(IQueryParameterOr::getValuesAsQueryTokens)
.map(t -> t
.stream()
.map(j -> j.getValueAsQueryToken(ourCtx))
.collect(Collectors.joining(",")))
.collect(Collectors.toList());
}
public static class DummyPatientResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Patient.class;
}
@Search()
public List<Resource> search(
@OptionalParam(name = "_id") TokenAndListParam theIdParam,
@OptionalParam(name = "name") StringAndListParam theNameParam
) {
ourLastHitMethod = "Patient.search";
ourLastIdParam = theIdParam;
ourLastNameParam = theNameParam;
return ourReturn;
}
}
public static class DummyObservationResourceProvider implements IResourceProvider {
@Override
public Class<? extends IBaseResource> getResourceType() {
return Observation.class;
}
@Search()
public List<Resource> search(
@OptionalParam(name = "_id") TokenAndListParam theIdParam,
@OptionalParam(name = Observation.SP_SUBJECT) ReferenceAndListParam theSubjectParam,
@OptionalParam(name = Observation.SP_PATIENT) ReferenceAndListParam thePatientParam,
@OptionalParam(name = Observation.SP_PERFORMER) ReferenceAndListParam thePerformerParam,
@OptionalParam(name = "code") TokenAndListParam theCodeParam
) {
ourLastHitMethod = "Observation.search";
ourLastIdParam = theIdParam;
ourLastSubjectParam = theSubjectParam;
ourLastPatientParam = thePatientParam;
ourLastPerformerParam = thePerformerParam;
ourLastCodeParam = theCodeParam;
return ourReturn;
}
}
private static class MySearchNarrowingInterceptor extends SearchNarrowingInterceptor {
@Override
protected AuthorizedList buildAuthorizedList(RequestDetails theRequestDetails) {
Validate.notNull(ourNextCompartmentList);
return ourNextCompartmentList;
}
}
@AfterClass
public static void afterClassClearContext() throws Exception {
ourServer.stop();
TestUtil.clearAllStaticFieldsForUnitTest();
}
@BeforeClass
public static void beforeClass() throws Exception {
ourCtx = FhirContext.forR4();
int ourPort = PortUtil.findFreePort();
ourServer = new Server(ourPort);
DummyPatientResourceProvider patProvider = new DummyPatientResourceProvider();
DummyObservationResourceProvider obsProv = new DummyObservationResourceProvider();
ServletHandler proxyHandler = new ServletHandler();
RestfulServer ourServlet = new RestfulServer(ourCtx);
ourServlet.setFhirContext(ourCtx);
ourServlet.setResourceProviders(patProvider, obsProv);
ourServlet.setPagingProvider(new FifoMemoryPagingProvider(100));
ourServlet.registerInterceptor(new MySearchNarrowingInterceptor());
ServletHolder servletHolder = new ServletHolder(ourServlet);
proxyHandler.addServletWithMapping(servletHolder, "/*");
ourServer.setHandler(proxyHandler);
ourServer.start();
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
ourCtx.getRestfulClientFactory().setSocketTimeout(1000000);
ourClient = ourCtx.newRestfulGenericClient("http://localhost:" + ourPort);
}
}

View File

@ -519,6 +519,7 @@
<commons_lang3_version>3.8.1</commons_lang3_version>
<derby_version>10.14.2.0</derby_version>
<error_prone_annotations_version>2.0.18</error_prone_annotations_version>
<error_prone_core_version>2.3.2</error_prone_core_version>
<guava_version>25.0-jre</guava_version>
<gson_version>2.8.5</gson_version>
<jaxb_bundle_version>2.2.11_1</jaxb_bundle_version>
@ -639,7 +640,7 @@
<dependency>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.3.2</version>
<version>${error_prone_core_version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
@ -766,7 +767,8 @@
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>mssql-jdbc</artifactId>
<version>6.2.2.jre8</version>
<!--<version>6.2.2.jre8</version>-->
<version>7.0.0.jre8</version>
</dependency>
<!--
<dependency>
@ -2308,7 +2310,7 @@
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>2.3.2</version>
<version>${error_prone_core_version}</version>
</path>
</annotationProcessorPaths>
</configuration>

View File

@ -264,6 +264,31 @@
OperationDefinitions are now created for named queries in server
module. Thanks to Stig Døssing for the pull request!
</action>
<action type="add">
A new server interceptor has been added called "SearchNarrowingInterceptor".
This interceptor can be used to automatically narrow the scope of searches
performed by the user to limit them to specific resources or compartments
that the user should have access to.
</action>
<action type="add">
In a DSTU2 server, if search parameters are expressed with chains directly in the
parameter name (e.g.
<![CDATA[<code>@RequiredParam(name="subject.name.family")</code>]]>) the second
part of the chain was lost when the chain was described in the server
CapabilityStatement. This has been corrected.
</action>
<action type="fix">
In the JPA server, search/read operations being performed within a transaction bundle
did not pass the client request HTTP headers to the sub-request. This meant that
AuthorizationInterceptor could not authorize these requests if it was depending on
headers being present.
</action>
<action type="fix">
When using a client in DSTU3/R4 mode, if the client attempted to validate the server
CapabilityStatement but was not able to parse the response, the client would throw
an exception with a misleading error about the Conformance resource not existing. This
has been corrected. Thanks to Shayaan Munshi for reporting and providing a test case!
</action>
</release>
<release version="3.6.0" date="2018-11-12" description="Food">
<action type="add">

View File

@ -96,10 +96,10 @@
</p>
<p class="doc_info_bubble">
AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet
been heavily tested. Use with caution, and do lots of testing! We welcome
feedback and suggestions on this feature. In addition, this documentation is
not yet complete. More examples and details will be added soon! Please get in
AuthorizationInterceptor has been well tested, but it is impossible to
predeict every scenario and environment in which HAPI FHIR will be used.
Use with caution, and do lots of testing! We welcome
feedback and suggestions on this feature. Please get in
touch if you'd like to help test, have suggestions, etc.
</p>
@ -254,6 +254,36 @@
</section>
<section name="Search Narrowind">
<p>
HAPI FHIR 3.7.0 introduced a new interceptor, the
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/auth/SearchNarrowingInterceptor.html">SearchNarrowingInterceptor</a>.
</p>
<p>
This interceptor is designed to be used in conjunction with AuthorizationInterceptor. It
uses a similar strategy where a dynamic list is built up for each request, but the
purpose of this interceptor is to modify client searches that are received (after
HAPI FHIR received the HTTP request, but before the search is actually performed)
to restrict the search to only search for specific resources or compartments that the
user has access to.
</p>
<p>
This could be used, for example, to allow the user to perform a search for<br/>
<code>http://baseurl/Observation?category=laboratory</code><br/>
and then receive results as though they had requested<br/>
<code>http://baseurl/Observation?subject=Patient/123&category=laboratory</code>.
</p>
<p>
An example of this interceptor follows:
</p>
<macro name="snippet">
<param name="id" value="narrowing" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
</section>
</body>
</document>