Add SearchNarrowingInterceptor
This commit is contained in:
parent
415ed80522
commit
ee52d6fb31
|
@ -1,15 +1,11 @@
|
||||||
package example;
|
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.dstu2.resource.Patient;
|
||||||
import ca.uhn.fhir.model.primitive.IdDt;
|
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.MethodOutcome;
|
||||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
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.exceptions.AuthenticationException;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
|
||||||
import ca.uhn.fhir.rest.server.interceptor.auth.*;
|
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")
|
@SuppressWarnings("unused")
|
||||||
public class AuthorizationInterceptors {
|
public class AuthorizationInterceptors {
|
||||||
|
@ -158,4 +160,47 @@ public class AuthorizationInterceptors {
|
||||||
//END SNIPPET: patchAll
|
//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
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package ca.uhn.fhir.rest.gclient;
|
package ca.uhn.fhir.rest.gclient;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
|
import org.apache.commons.lang3.Validate;
|
||||||
import org.hl7.fhir.instance.model.api.IIdType;
|
import org.hl7.fhir.instance.model.api.IIdType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
|
||||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||||
|
@ -97,7 +99,19 @@ public class ReferenceClientParam extends BaseClientParam implements IParam {
|
||||||
public ICriterion<ReferenceClientParam> hasAnyOfIds(Collection<String> theIds) {
|
public ICriterion<ReferenceClientParam> hasAnyOfIds(Collection<String> theIds) {
|
||||||
return new StringCriterion<>(getParamName(), theIds);
|
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 static class ReferenceChainCriterion implements ICriterion<ReferenceClientParam>, ICriterionInternal {
|
||||||
|
|
||||||
private final String myResourceTypeQualifier;
|
private final String myResourceTypeQualifier;
|
||||||
|
|
|
@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.param;
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
*
|
*
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
*
|
*
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@ -62,5 +62,10 @@ public abstract class BaseAndListParam<T extends IQueryParameterOr<?>> implement
|
||||||
return myValues.toString();
|
return myValues.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the number of AND parameters
|
||||||
|
*/
|
||||||
|
public int size() {
|
||||||
|
return myValues.size();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,10 @@ import org.hl7.fhir.instance.model.api.IPrimitiveType;
|
||||||
import java.lang.annotation.Annotation;
|
import java.lang.annotation.Annotation;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* #%L
|
* #%L
|
||||||
|
@ -208,6 +210,13 @@ public class ParameterUtil {
|
||||||
|| IPrimitiveType.class.isAssignableFrom(theClass);
|
|| 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) {
|
public static int nonEscapedIndexOf(String theString, char theCharacter) {
|
||||||
for (int i = 0; i < theString.length(); i++) {
|
for (int i = 0; i < theString.length(); i++) {
|
||||||
if (theString.charAt(i) == theCharacter) {
|
if (theString.charAt(i) == theCharacter) {
|
||||||
|
|
|
@ -29,14 +29,14 @@ public interface IAnyResource extends IBaseResource {
|
||||||
* Search parameter constant for <b>_language</b>
|
* Search parameter constant for <b>_language</b>
|
||||||
*/
|
*/
|
||||||
@SearchParamDefinition(name="_language", path="", description="The language of the resource", type="string" )
|
@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>
|
* Search parameter constant for <b>_id</b>
|
||||||
*/
|
*/
|
||||||
@SearchParamDefinition(name="_id", path="", description="The ID of the resource", type="token" )
|
@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>
|
* <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>
|
* Path: <b>Resource._id</b><br>
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public static final TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID);
|
TokenClientParam RES_ID = new TokenClientParam(IAnyResource.SP_RES_ID);
|
||||||
|
|
||||||
String getId();
|
String getId();
|
||||||
|
|
||||||
|
@ -55,11 +55,11 @@ public interface IAnyResource extends IBaseResource {
|
||||||
|
|
||||||
IPrimitiveType<String> getLanguageElement();
|
IPrimitiveType<String> getLanguageElement();
|
||||||
|
|
||||||
public Object getUserData(String name);
|
Object getUserData(String name);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
IAnyResource setId(String theId);
|
IAnyResource setId(String theId);
|
||||||
|
|
||||||
public void setUserData(String name, Object value);
|
void setUserData(String name, Object value);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,7 +81,7 @@ public abstract class BaseQueryParameter implements IParameter {
|
||||||
String paramName = isNotBlank(qualifier) ? getName() + qualifier : getName();
|
String paramName = isNotBlank(qualifier) ? getName() + qualifier : getName();
|
||||||
List<String> paramValues = theTargetQueryArguments.get(paramName);
|
List<String> paramValues = theTargetQueryArguments.get(paramName);
|
||||||
if (paramValues == null) {
|
if (paramValues == null) {
|
||||||
paramValues = new ArrayList<String>(value.size());
|
paramValues = new ArrayList<>(value.size());
|
||||||
theTargetQueryArguments.put(paramName, paramValues);
|
theTargetQueryArguments.put(paramName, paramValues);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,6 +59,10 @@
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-collections4</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
* <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.
|
* for information on how to use this interceptor.
|
||||||
* </p>
|
* </p>
|
||||||
|
*
|
||||||
|
* @see SearchNarrowingInterceptor
|
||||||
*/
|
*/
|
||||||
public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter implements IRuleApplier {
|
public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter implements IRuleApplier {
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,16 +1,19 @@
|
||||||
package ca.uhn.fhir.rest.client;
|
package ca.uhn.fhir.rest.client;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import ca.uhn.fhir.context.FhirContext;
|
||||||
import static org.junit.Assert.assertNotNull;
|
import ca.uhn.fhir.model.api.Include;
|
||||||
import static org.junit.Assert.assertThat;
|
import ca.uhn.fhir.rest.annotation.IncludeParam;
|
||||||
import static org.junit.Assert.assertTrue;
|
import ca.uhn.fhir.rest.annotation.RequiredParam;
|
||||||
import static org.mockito.Mockito.mock;
|
import ca.uhn.fhir.rest.annotation.Search;
|
||||||
import static org.mockito.Mockito.when;
|
import ca.uhn.fhir.rest.api.Constants;
|
||||||
|
import ca.uhn.fhir.rest.client.api.IBasicClient;
|
||||||
import java.io.StringReader;
|
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
||||||
import java.nio.charset.Charset;
|
import ca.uhn.fhir.rest.param.TokenAndListParam;
|
||||||
import java.util.*;
|
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.IOUtils;
|
||||||
import org.apache.commons.io.input.ReaderInputStream;
|
import org.apache.commons.io.input.ReaderInputStream;
|
||||||
import org.apache.http.HttpResponse;
|
import org.apache.http.HttpResponse;
|
||||||
|
@ -23,118 +26,144 @@ import org.apache.http.message.BasicStatusLine;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.hl7.fhir.r4.model.Bundle;
|
import org.hl7.fhir.r4.model.Bundle;
|
||||||
import org.hl7.fhir.r4.model.Encounter;
|
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.ArgumentCaptor;
|
||||||
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
|
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
|
||||||
|
|
||||||
import ca.uhn.fhir.context.FhirContext;
|
import java.io.StringReader;
|
||||||
import ca.uhn.fhir.model.api.Include;
|
import java.nio.charset.Charset;
|
||||||
import ca.uhn.fhir.rest.annotation.*;
|
import java.util.HashSet;
|
||||||
import ca.uhn.fhir.rest.api.Constants;
|
import java.util.List;
|
||||||
import ca.uhn.fhir.rest.client.api.IBasicClient;
|
import java.util.Set;
|
||||||
import ca.uhn.fhir.rest.client.api.ServerValidationModeEnum;
|
|
||||||
import ca.uhn.fhir.rest.param.TokenOrListParam;
|
import static org.junit.Assert.*;
|
||||||
import ca.uhn.fhir.rest.param.TokenParam;
|
import static org.mockito.Mockito.mock;
|
||||||
import ca.uhn.fhir.rest.server.exceptions.BaseServerResponseException;
|
import static org.mockito.Mockito.when;
|
||||||
import ca.uhn.fhir.util.TestUtil;
|
|
||||||
|
|
||||||
public class SearchClientTest {
|
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 FhirContext ourCtx;
|
||||||
private HttpClient ourHttpClient;
|
private HttpClient ourHttpClient;
|
||||||
private HttpResponse ourHttpResponse;
|
private HttpResponse ourHttpResponse;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void before() {
|
public void before() {
|
||||||
ourCtx = FhirContext.forR4();
|
ourCtx = FhirContext.forR4();
|
||||||
|
|
||||||
ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
|
ourHttpClient = mock(HttpClient.class, new ReturnsDeepStubs());
|
||||||
ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient);
|
ourCtx.getRestfulClientFactory().setHttpClient(ourHttpClient);
|
||||||
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
ourCtx.getRestfulClientFactory().setServerValidationMode(ServerValidationModeEnum.NEVER);
|
||||||
|
|
||||||
ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
|
ourHttpResponse = mock(HttpResponse.class, new ReturnsDeepStubs());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testPostOnLongParamsList() throws Exception {
|
public void testPostOnLongParamsList() throws Exception {
|
||||||
String resp = createBundle();
|
String resp = createBundle();
|
||||||
|
|
||||||
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
|
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
|
||||||
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
|
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
|
||||||
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
|
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().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")));
|
when(ourHttpResponse.getEntity().getContent()).thenAnswer(t->new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8")));
|
||||||
|
|
||||||
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
|
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
|
||||||
Set<Include> includes = new HashSet<Include>();
|
Set<Include> includes = new HashSet<Include>();
|
||||||
includes.add(new Include("one"));
|
includes.add(new Include("one"));
|
||||||
includes.add(new Include("two"));
|
includes.add(new Include("two"));
|
||||||
TokenOrListParam params = new TokenOrListParam();
|
TokenOrListParam params = new TokenOrListParam();
|
||||||
for (int i = 0; i < 1000; i++) {
|
for (int i = 0; i < 1000; i++) {
|
||||||
params.add(new TokenParam("system", "value"));
|
params.add(new TokenParam("system", "value"));
|
||||||
}
|
}
|
||||||
List<Encounter> found = client.searchByList(params, includes);
|
|
||||||
|
|
||||||
assertEquals(1, found.size());
|
// With OR list
|
||||||
|
|
||||||
Encounter encounter = found.get(0);
|
List<Encounter> found = client.searchByList(params, includes);
|
||||||
assertNotNull(encounter.getSubject().getReference());
|
|
||||||
HttpUriRequest value = capt.getValue();
|
|
||||||
|
|
||||||
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
|
assertEquals(1, found.size());
|
||||||
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"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
Encounter encounter = found.get(0);
|
||||||
public void testReturnTypedList() throws Exception {
|
assertNotNull(encounter.getSubject().getReference());
|
||||||
|
HttpUriRequest value = capt.getValue();
|
||||||
String resp = createBundle();
|
|
||||||
|
|
||||||
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
|
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
|
||||||
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
|
HttpPost post = (HttpPost) value;
|
||||||
when(ourHttpResponse.getStatusLine()).thenReturn(new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200, "OK"));
|
String body = IOUtils.toString(post.getEntity().getContent(), Charsets.UTF_8);
|
||||||
when(ourHttpResponse.getEntity().getContentType()).thenReturn(new BasicHeader("content-type", Constants.CT_FHIR_XML_NEW + "; charset=UTF-8"));
|
ourLog.info(body);
|
||||||
when(ourHttpResponse.getEntity().getContent()).thenReturn(new ReaderInputStream(new StringReader(resp), Charset.forName("UTF-8")));
|
assertThat(body, Matchers.containsString("_include=one"));
|
||||||
|
assertThat(body, Matchers.containsString("_include=two"));
|
||||||
|
|
||||||
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
|
// With AND list
|
||||||
List<Encounter> found = client.search();
|
|
||||||
assertEquals(1, found.size());
|
|
||||||
|
|
||||||
Encounter encounter = found.get(0);
|
TokenAndListParam paramsAndList = new TokenAndListParam();
|
||||||
assertNotNull(encounter.getSubject().getReference());
|
paramsAndList.addAnd(params);
|
||||||
}
|
found = client.searchByList(paramsAndList, includes);
|
||||||
|
|
||||||
private String createBundle() {
|
assertEquals(1, found.size());
|
||||||
Bundle bundle = new Bundle();
|
|
||||||
|
|
||||||
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 {
|
encounter = found.get(0);
|
||||||
|
assertNotNull(encounter.getSubject().getReference());
|
||||||
|
value = capt.getAllValues().get(1);
|
||||||
|
|
||||||
@Search
|
assertTrue("Expected request of type POST on long params list", value instanceof HttpPost);
|
||||||
List<Encounter> search();
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
@Search
|
@Test
|
||||||
List<Encounter> searchByList(@RequiredParam(name = Encounter.SP_IDENTIFIER) TokenOrListParam tokenOrListParam, @IncludeParam Set<Include> theIncludes) throws BaseServerResponseException;
|
public void testReturnTypedList() throws Exception {
|
||||||
|
|
||||||
}
|
String resp = createBundle();
|
||||||
|
|
||||||
@AfterClass
|
ArgumentCaptor<HttpUriRequest> capt = ArgumentCaptor.forClass(HttpUriRequest.class);
|
||||||
public static void afterClassClearContext() {
|
when(ourHttpClient.execute(capt.capture())).thenReturn(ourHttpResponse);
|
||||||
TestUtil.clearAllStaticFieldsForUnitTest();
|
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")));
|
||||||
|
|
||||||
|
ITestClient client = ourCtx.newRestfulClient(ITestClient.class, "http://foo");
|
||||||
|
List<Encounter> found = client.search();
|
||||||
|
assertEquals(1, found.size());
|
||||||
|
|
||||||
|
Encounter encounter = found.get(0);
|
||||||
|
assertNotNull(encounter.getSubject().getReference());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createBundle() {
|
||||||
|
Bundle bundle = new Bundle();
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
5
pom.xml
5
pom.xml
|
@ -519,6 +519,7 @@
|
||||||
<commons_lang3_version>3.8.1</commons_lang3_version>
|
<commons_lang3_version>3.8.1</commons_lang3_version>
|
||||||
<derby_version>10.14.2.0</derby_version>
|
<derby_version>10.14.2.0</derby_version>
|
||||||
<error_prone_annotations_version>2.0.18</error_prone_annotations_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>
|
<guava_version>25.0-jre</guava_version>
|
||||||
<gson_version>2.8.5</gson_version>
|
<gson_version>2.8.5</gson_version>
|
||||||
<jaxb_bundle_version>2.2.11_1</jaxb_bundle_version>
|
<jaxb_bundle_version>2.2.11_1</jaxb_bundle_version>
|
||||||
|
@ -639,7 +640,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.errorprone</groupId>
|
<groupId>com.google.errorprone</groupId>
|
||||||
<artifactId>error_prone_core</artifactId>
|
<artifactId>error_prone_core</artifactId>
|
||||||
<version>2.3.2</version>
|
<version>${error_prone_core_version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.guava</groupId>
|
<groupId>com.google.guava</groupId>
|
||||||
|
@ -2308,7 +2309,7 @@
|
||||||
<path>
|
<path>
|
||||||
<groupId>com.google.errorprone</groupId>
|
<groupId>com.google.errorprone</groupId>
|
||||||
<artifactId>error_prone_core</artifactId>
|
<artifactId>error_prone_core</artifactId>
|
||||||
<version>2.3.2</version>
|
<version>${error_prone_core_version}</version>
|
||||||
</path>
|
</path>
|
||||||
</annotationProcessorPaths>
|
</annotationProcessorPaths>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
|
|
@ -264,6 +264,12 @@
|
||||||
OperationDefinitions are now created for named queries in server
|
OperationDefinitions are now created for named queries in server
|
||||||
module. Thanks to Stig Døssing for the pull request!
|
module. Thanks to Stig Døssing for the pull request!
|
||||||
</action>
|
</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>
|
||||||
</release>
|
</release>
|
||||||
<release version="3.6.0" date="2018-11-12" description="Food">
|
<release version="3.6.0" date="2018-11-12" description="Food">
|
||||||
<action type="add">
|
<action type="add">
|
||||||
|
|
|
@ -96,10 +96,10 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="doc_info_bubble">
|
<p class="doc_info_bubble">
|
||||||
AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet
|
AuthorizationInterceptor has been well tested, but it is impossible to
|
||||||
been heavily tested. Use with caution, and do lots of testing! We welcome
|
predeict every scenario and environment in which HAPI FHIR will be used.
|
||||||
feedback and suggestions on this feature. In addition, this documentation is
|
Use with caution, and do lots of testing! We welcome
|
||||||
not yet complete. More examples and details will be added soon! Please get in
|
feedback and suggestions on this feature. Please get in
|
||||||
touch if you'd like to help test, have suggestions, etc.
|
touch if you'd like to help test, have suggestions, etc.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -253,7 +253,37 @@
|
||||||
</subsection>
|
</subsection>
|
||||||
|
|
||||||
</section>
|
</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>
|
</body>
|
||||||
|
|
||||||
</document>
|
</document>
|
||||||
|
|
Loading…
Reference in New Issue