Auth interceptor early checks (#995)
* Validator fix * Enhance AuthorizationInterceptor so that it tries to deny access earlier for compartment searches that are outside the allowable compartment. * FIx DSTU2 test that wasn't updated * More test fixes
This commit is contained in:
parent
903df68d2a
commit
cc0e836680
|
@ -47,7 +47,7 @@ public class AuthorizationInterceptorResourceProviderDstu3Test extends BaseResou
|
|||
* See #778
|
||||
*/
|
||||
@Test
|
||||
public void testReadingObservationAccessRight() throws IOException {
|
||||
public void testReadingObservationAccessRight() {
|
||||
Practitioner practitioner1 = new Practitioner();
|
||||
final IIdType practitionerId1 = ourClient.create().resource(practitioner1).execute().getId().toUnqualifiedVersionless();
|
||||
|
||||
|
@ -105,7 +105,7 @@ public class AuthorizationInterceptorResourceProviderDstu3Test extends BaseResou
|
|||
* See #667
|
||||
*/
|
||||
@Test
|
||||
public void testBlockUpdatingPatientUserDoesnNotHaveAccessTo() throws IOException {
|
||||
public void testBlockUpdatingPatientUserDoesnNotHaveAccessTo() {
|
||||
Patient pt1 = new Patient();
|
||||
pt1.setActive(true);
|
||||
final IIdType pid1 = ourClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless();
|
||||
|
|
|
@ -44,7 +44,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
|
|||
* See #778
|
||||
*/
|
||||
@Test
|
||||
public void testReadingObservationAccessRight() throws IOException {
|
||||
public void testReadingObservationAccessRight() {
|
||||
Practitioner practitioner1 = new Practitioner();
|
||||
final IIdType practitionerId1 = myClient.create().resource(practitioner1).execute().getId().toUnqualifiedVersionless();
|
||||
|
||||
|
@ -102,7 +102,7 @@ public class AuthorizationInterceptorResourceProviderR4Test extends BaseResource
|
|||
* See #667
|
||||
*/
|
||||
@Test
|
||||
public void testBlockUpdatingPatientUserDoesnNotHaveAccessTo() throws IOException {
|
||||
public void testBlockUpdatingPatientUserDoesnNotHaveAccessTo() {
|
||||
Patient pt1 = new Patient();
|
||||
pt1.setActive(true);
|
||||
final IIdType pid1 = myClient.create().resource(pt1).execute().getId().toUnqualifiedVersionless();
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
package ca.uhn.fhir.rest.server.interceptor.auth;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* @see AuthorizationInterceptor#setFlags(Collection)
|
||||
*/
|
||||
public enum AuthorizationFlagsEnum {
|
||||
|
||||
/**
|
||||
* If this flag is set, attempts to perform read operations
|
||||
* (read/search/history) will be matched by the interceptor before
|
||||
* the method handler is called.
|
||||
* <p>
|
||||
* For example, suppose a rule set is in place that only allows read
|
||||
* access to compartment <code>Patient/123</code>. With this flag set,
|
||||
* any attempts
|
||||
* to perform a FHIR read/search/history operation will be permitted
|
||||
* to proceed to the method handler, and responses will be blocked
|
||||
* by the AuthorizationInterceptor if the response contains a resource
|
||||
* that is not in the given compartment.
|
||||
* </p>
|
||||
* <p>
|
||||
* Setting this flag is less secure, since the interceptor can potentially leak
|
||||
* information about the existence of data, but it is useful in some
|
||||
* scenarios.
|
||||
* </p>
|
||||
*
|
||||
* @since This flag has existed since HAPI FHIR 3.5.0. Prior to this
|
||||
* version, this flag was the default and there was no ability to
|
||||
* proactively block compartment read access.
|
||||
*/
|
||||
NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS;
|
||||
|
||||
}
|
|
@ -28,6 +28,7 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
|
|||
import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.ServerOperationInterceptorAdapter;
|
||||
import ca.uhn.fhir.util.CoverageIgnore;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
@ -35,12 +36,12 @@ import org.hl7.fhir.instance.model.api.IBaseBundle;
|
|||
import org.hl7.fhir.instance.model.api.IBaseParameters;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.defaultString;
|
||||
|
||||
|
@ -56,9 +57,10 @@ import static org.apache.commons.lang3.StringUtils.defaultString;
|
|||
*/
|
||||
public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter implements IRuleApplier {
|
||||
|
||||
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AuthorizationInterceptor.class);
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(AuthorizationInterceptor.class);
|
||||
|
||||
private PolicyEnum myDefaultPolicy = PolicyEnum.DENY;
|
||||
private Set<AuthorizationFlagsEnum> myFlags = Collections.emptySet();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
|
@ -92,11 +94,12 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
|
|||
public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId,
|
||||
IBaseResource theOutputResource) {
|
||||
List<IAuthRule> rules = buildRuleList(theRequestDetails);
|
||||
Set<AuthorizationFlagsEnum> flags = getFlags();
|
||||
ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation);
|
||||
|
||||
Verdict verdict = null;
|
||||
for (IAuthRule nextRule : rules) {
|
||||
verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, this);
|
||||
verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, this, flags);
|
||||
if (verdict != null) {
|
||||
ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision());
|
||||
break;
|
||||
|
@ -105,7 +108,7 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
|
|||
|
||||
if (verdict == null) {
|
||||
ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy);
|
||||
return new Verdict(myDefaultPolicy, null);
|
||||
return new Verdict(getDefaultPolicy(), null);
|
||||
}
|
||||
|
||||
return verdict;
|
||||
|
@ -206,6 +209,28 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
|
|||
myDefaultPolicy = theDefaultPolicy;
|
||||
}
|
||||
|
||||
/**
|
||||
* This property configures any flags affecting how authorization is
|
||||
* applied. By default no flags are applied.
|
||||
*
|
||||
* @see #setFlags(Collection)
|
||||
*/
|
||||
public Set<AuthorizationFlagsEnum> getFlags() {
|
||||
return Collections.unmodifiableSet(myFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* This property configures any flags affecting how authorization is
|
||||
* applied. By default no flags are applied.
|
||||
*
|
||||
* @param theFlags The flags (must not be null)
|
||||
* @see #setFlags(Collection)
|
||||
*/
|
||||
public AuthorizationInterceptor setFlags(AuthorizationFlagsEnum... theFlags) {
|
||||
Validate.notNull(theFlags, "theFlags must not be null");
|
||||
return setFlags(Lists.newArrayList(theFlags));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an access control verdict of {@link PolicyEnum#DENY}.
|
||||
* <p>
|
||||
|
@ -325,6 +350,19 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
|
|||
handleUserOperation(theRequest, theNewResource, RestOperationTypeEnum.UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* This property configures any flags affecting how authorization is
|
||||
* applied. By default no flags are applied.
|
||||
*
|
||||
* @param theFlags The flags (must not be null)
|
||||
* @see #setFlags(AuthorizationFlagsEnum...)
|
||||
*/
|
||||
public AuthorizationInterceptor setFlags(Collection<AuthorizationFlagsEnum> theFlags) {
|
||||
Validate.notNull(theFlags, "theFlags must not be null");
|
||||
myFlags = new HashSet<>(theFlags);
|
||||
return this;
|
||||
}
|
||||
|
||||
private static UnsupportedOperationException failForDstu1() {
|
||||
return new UnsupportedOperationException("Use of this interceptor on DSTU1 servers is not supportd");
|
||||
}
|
||||
|
|
|
@ -27,6 +27,15 @@ import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
|||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Note: At this time, this interface is considered internal API to HAPI FHIR,
|
||||
* and is subject to change without warning. Create your own implementations at
|
||||
* your own risk. If you have use cases that are not met by the current
|
||||
* implementation, please consider raising them on the HAPI FHIR
|
||||
* Google Group.
|
||||
*/
|
||||
public interface IAuthRule {
|
||||
|
||||
/**
|
||||
|
@ -44,9 +53,10 @@ public interface IAuthRule {
|
|||
* @param theRuleApplier
|
||||
* The rule applying module (this can be used by rules to apply the rule set to
|
||||
* nested objects in the request, such as nested requests in a transaction)
|
||||
* @param theFlags
|
||||
* @return Returns a policy decision, or <code>null</code> if the rule does not apply
|
||||
*/
|
||||
Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier);
|
||||
Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags);
|
||||
|
||||
/**
|
||||
* Returns a name for this rule, to be used in logs and error messages
|
||||
|
|
|
@ -29,10 +29,11 @@ import org.hl7.fhir.instance.model.api.IIdType;
|
|||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
class OperationRule extends BaseRule implements IAuthRule {
|
||||
|
||||
private RuleBuilder.ITenantApplicabilityChecker myTenentApplicabilityChecker;
|
||||
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
|
||||
private String myOperationName;
|
||||
private boolean myAppliesToServer;
|
||||
private HashSet<Class<? extends IBaseResource>> myAppliesToTypes;
|
||||
|
@ -75,17 +76,25 @@ class OperationRule extends BaseRule implements IAuthRule {
|
|||
}
|
||||
|
||||
@Override
|
||||
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier) {
|
||||
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
|
||||
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
|
||||
|
||||
if (myTenentApplicabilityChecker != null) {
|
||||
if (!myTenentApplicabilityChecker.applies(theRequestDetails)) {
|
||||
if (myTenantApplicabilityChecker != null) {
|
||||
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
boolean applies = false;
|
||||
switch (theOperation) {
|
||||
case ADD_TAGS:
|
||||
case DELETE_TAGS:
|
||||
case GET_TAGS:
|
||||
case GET_PAGE:
|
||||
case GRAPHQL_REQUEST:
|
||||
// These things can't be tracked by the AuthorizationInterceptor
|
||||
// at this time
|
||||
return null;
|
||||
case EXTENDED_OPERATION_SERVER:
|
||||
if (myAppliesToServer || myAppliesAtAnyLevel) {
|
||||
applies = true;
|
||||
|
@ -130,6 +139,40 @@ class OperationRule extends BaseRule implements IAuthRule {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case CREATE:
|
||||
break;
|
||||
case DELETE:
|
||||
break;
|
||||
case HISTORY_INSTANCE:
|
||||
break;
|
||||
case HISTORY_SYSTEM:
|
||||
break;
|
||||
case HISTORY_TYPE:
|
||||
break;
|
||||
case READ:
|
||||
break;
|
||||
case SEARCH_SYSTEM:
|
||||
break;
|
||||
case SEARCH_TYPE:
|
||||
break;
|
||||
case TRANSACTION:
|
||||
break;
|
||||
case UPDATE:
|
||||
break;
|
||||
case VALIDATE:
|
||||
break;
|
||||
case VREAD:
|
||||
break;
|
||||
case METADATA:
|
||||
break;
|
||||
case META_ADD:
|
||||
break;
|
||||
case META:
|
||||
break;
|
||||
case META_DELETE:
|
||||
break;
|
||||
case PATCH:
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -160,8 +203,8 @@ class OperationRule extends BaseRule implements IAuthRule {
|
|||
myOperationName = theOperationName;
|
||||
}
|
||||
|
||||
public void setTenentApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenentApplicabilityChecker) {
|
||||
myTenentApplicabilityChecker = theTenentApplicabilityChecker;
|
||||
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
|
||||
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -174,7 +174,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
|
|||
myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
|
||||
}
|
||||
if (myOperationRule != null) {
|
||||
myOperationRule.setTenentApplicabilityChecker(myTenantApplicabilityChecker);
|
||||
myOperationRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
|
|||
|
||||
@Override
|
||||
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
|
||||
IRuleApplier theRuleApplier) {
|
||||
IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
|
||||
|
||||
if (theInputResourceId != null) {
|
||||
return null;
|
||||
|
|
|
@ -1,5 +1,28 @@
|
|||
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.RequestTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.BundleUtil.BundleEntryParts;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
import org.apache.commons.codec.binary.StringUtils;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
||||
|
||||
/*
|
||||
|
@ -22,26 +45,6 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
|
|||
* #L%
|
||||
*/
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseBundle;
|
||||
import org.hl7.fhir.instance.model.api.IBaseResource;
|
||||
import org.hl7.fhir.instance.model.api.IIdType;
|
||||
|
||||
import ca.uhn.fhir.context.FhirContext;
|
||||
import ca.uhn.fhir.context.RuntimeResourceDefinition;
|
||||
import ca.uhn.fhir.rest.api.*;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict;
|
||||
import ca.uhn.fhir.util.BundleUtil;
|
||||
import ca.uhn.fhir.util.BundleUtil.BundleEntryParts;
|
||||
import ca.uhn.fhir.util.FhirTerser;
|
||||
|
||||
class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
||||
|
||||
private AppliesTypeEnum myAppliesTo;
|
||||
|
@ -54,13 +57,16 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
private List<IIdType> myAppliesToInstances;
|
||||
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public RuleImplOp(String theRuleName) {
|
||||
super(theRuleName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
|
||||
IRuleApplier theRuleApplier) {
|
||||
IRuleApplier theRuleApplier, Set<AuthorizationFlagsEnum> theFlags) {
|
||||
|
||||
if (myTenantApplicabilityChecker != null) {
|
||||
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
|
||||
|
@ -73,9 +79,14 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
IBaseResource appliesToResource;
|
||||
IIdType appliesToResourceId = null;
|
||||
String appliesToResourceType = null;
|
||||
Map<String, String[]> appliesToSearchParams = null;
|
||||
switch (myOp) {
|
||||
case READ:
|
||||
if (theOutputResource == null) {
|
||||
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (theOperation) {
|
||||
case READ:
|
||||
case VREAD:
|
||||
|
@ -83,14 +94,51 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
appliesToResourceType = theInputResourceId.getResourceType();
|
||||
break;
|
||||
case SEARCH_SYSTEM:
|
||||
case SEARCH_TYPE:
|
||||
case HISTORY_INSTANCE:
|
||||
case HISTORY_SYSTEM:
|
||||
case HISTORY_TYPE:
|
||||
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
|
||||
return null;
|
||||
}
|
||||
if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
break;
|
||||
case SEARCH_TYPE:
|
||||
if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
appliesToResourceType = theRequestDetails.getResourceName();
|
||||
appliesToSearchParams = theRequestDetails.getParameters();
|
||||
break;
|
||||
case HISTORY_TYPE:
|
||||
if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
appliesToResourceType = theRequestDetails.getResourceName();
|
||||
break;
|
||||
case HISTORY_INSTANCE:
|
||||
if (theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
appliesToResourceId = theInputResourceId;
|
||||
break;
|
||||
case GET_PAGE:
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
|
||||
// None of the following are checked on the way in
|
||||
case ADD_TAGS:
|
||||
case DELETE_TAGS:
|
||||
case GET_TAGS:
|
||||
case GRAPHQL_REQUEST:
|
||||
case EXTENDED_OPERATION_SERVER:
|
||||
case EXTENDED_OPERATION_TYPE:
|
||||
case EXTENDED_OPERATION_INSTANCE:
|
||||
case CREATE:
|
||||
case DELETE:
|
||||
case TRANSACTION:
|
||||
case UPDATE:
|
||||
case VALIDATE:
|
||||
case METADATA:
|
||||
case META_ADD:
|
||||
case META:
|
||||
case META_DELETE:
|
||||
case PATCH:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
@ -244,28 +292,38 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
|
||||
return null;
|
||||
}
|
||||
if (myClassifierType == ClassifierTypeEnum.ANY_ID) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TYPES:
|
||||
if (appliesToResource != null) {
|
||||
if (myClassifierType == ClassifierTypeEnum.ANY_ID) {
|
||||
if (myAppliesToTypes.contains(appliesToResource.getClass()) == false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (myClassifierType == ClassifierTypeEnum.ANY_ID) {
|
||||
if (appliesToResourceId != null && appliesToResourceId.hasResourceType()) {
|
||||
Class<? extends IBaseResource> type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceId.getResourceType()).getImplementingClass();
|
||||
if (myAppliesToTypes.contains(type) == false) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// }
|
||||
if (appliesToResourceType != null) {
|
||||
Class<? extends IBaseResource> type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceType).getImplementingClass();
|
||||
if (myAppliesToTypes.contains(type)) {
|
||||
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
|
||||
return null;
|
||||
}
|
||||
if (myClassifierType == ClassifierTypeEnum.ANY_ID) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
} else if (myClassifierType == ClassifierTypeEnum.IN_COMPARTMENT) {
|
||||
// ok we'll check below
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -292,6 +350,49 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If we're trying to read a resource that could potentially be
|
||||
* in the given compartment, we'll let the request through and
|
||||
* catch any issues on the response.
|
||||
*
|
||||
* This is less than perfect, but it's the best we can do-
|
||||
* If the user is allowed to see compartment "Patient/123" and
|
||||
* the client is requesting to read a CarePlan, there is nothing
|
||||
* in the request URL that indicates whether or not the CarePlan
|
||||
* might be in the given compartment.
|
||||
*/
|
||||
if (isNotBlank(appliesToResourceType)) {
|
||||
RuntimeResourceDefinition sourceDef = theRequestDetails.getFhirContext().getResourceDefinition(appliesToResourceType);
|
||||
String compartmentOwnerResourceType = next.getResourceType();
|
||||
if (!StringUtils.equals(appliesToResourceType, compartmentOwnerResourceType)) {
|
||||
List<RuntimeSearchParam> params = sourceDef.getSearchParamsForCompartmentName(compartmentOwnerResourceType);
|
||||
if (params.isEmpty() == false) {
|
||||
|
||||
/*
|
||||
* If this is a search, we can at least check whether
|
||||
* the client has requested a search parameter that
|
||||
* would match the given compartment. In this case, this
|
||||
* is a very effective mechanism.
|
||||
*/
|
||||
if (appliesToSearchParams != null && !theFlags.contains(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS)) {
|
||||
for (RuntimeSearchParam nextRuntimeSearchParam : params) {
|
||||
String[] values = appliesToSearchParams.get(nextRuntimeSearchParam.getName());
|
||||
if (values != null) {
|
||||
for (String nextParameterValue : values) {
|
||||
if (nextParameterValue.equals(next.getValue())) {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return new Verdict(PolicyEnum.ALLOW, this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!foundMatch) {
|
||||
return null;
|
||||
|
@ -308,22 +409,12 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
return newVerdict();
|
||||
}
|
||||
|
||||
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
|
||||
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
|
||||
public TransactionAppliesToEnum getTransactionAppliesToOp() {
|
||||
return myTransactionAppliesToOp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
builder.append("op", myOp);
|
||||
builder.append("transactionAppliesToOp", myTransactionAppliesToOp);
|
||||
builder.append("appliesTo", myAppliesTo);
|
||||
builder.append("appliesToTypes", myAppliesToTypes);
|
||||
builder.append("appliesToTenant", myTenantApplicabilityChecker);
|
||||
builder.append("classifierCompartmentName", myClassifierCompartmentName);
|
||||
builder.append("classifierCompartmentOwners", myClassifierCompartmentOwners);
|
||||
builder.append("classifierType", myClassifierType);
|
||||
return builder.toString();
|
||||
public void setTransactionAppliesToOp(TransactionAppliesToEnum theOp) {
|
||||
myTransactionAppliesToOp = theOp;
|
||||
}
|
||||
|
||||
private boolean requestAppliesToTransaction(FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) {
|
||||
|
@ -343,14 +434,14 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
}
|
||||
}
|
||||
|
||||
public TransactionAppliesToEnum getTransactionAppliesToOp() {
|
||||
return myTransactionAppliesToOp;
|
||||
}
|
||||
|
||||
public void setAppliesTo(AppliesTypeEnum theAppliesTo) {
|
||||
myAppliesTo = theAppliesTo;
|
||||
}
|
||||
|
||||
public void setAppliesToInstances(List<IIdType> theAppliesToInstances) {
|
||||
myAppliesToInstances = theAppliesToInstances;
|
||||
}
|
||||
|
||||
public void setAppliesToTypes(Set<?> theAppliesToTypes) {
|
||||
myAppliesToTypes = theAppliesToTypes;
|
||||
}
|
||||
|
@ -372,12 +463,22 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
|
|||
return this;
|
||||
}
|
||||
|
||||
public void setTransactionAppliesToOp(TransactionAppliesToEnum theOp) {
|
||||
myTransactionAppliesToOp = theOp;
|
||||
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
|
||||
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
|
||||
}
|
||||
|
||||
public void setAppliesToInstances(List<IIdType> theAppliesToInstances) {
|
||||
myAppliesToInstances = theAppliesToInstances;
|
||||
@Override
|
||||
public String toString() {
|
||||
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
|
||||
builder.append("op", myOp);
|
||||
builder.append("transactionAppliesToOp", myTransactionAppliesToOp);
|
||||
builder.append("appliesTo", myAppliesTo);
|
||||
builder.append("appliesToTypes", myAppliesToTypes);
|
||||
builder.append("appliesToTenant", myTenantApplicabilityChecker);
|
||||
builder.append("classifierCompartmentName", myClassifierCompartmentName);
|
||||
builder.append("classifierCompartmentOwners", myClassifierCompartmentOwners);
|
||||
builder.append("classifierType", myClassifierType);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import java.nio.charset.StandardCharsets;
|
|||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpResponse;
|
||||
|
@ -65,7 +66,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
@Before
|
||||
public void before() {
|
||||
ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.NEVER);
|
||||
for (IServerInterceptor next : new ArrayList<IServerInterceptor>(ourServlet.getInterceptors())) {
|
||||
for (IServerInterceptor next : new ArrayList<>(ourServlet.getInterceptors())) {
|
||||
ourServlet.unregisterInterceptor(next);
|
||||
}
|
||||
ourReturn = null;
|
||||
|
@ -1166,8 +1167,8 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
}
|
||||
|
||||
|
@ -1177,7 +1178,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1"))
|
||||
.allow("Rule 1").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdDt("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
@ -1187,13 +1188,13 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
String respString;
|
||||
Bundle respBundle;
|
||||
|
||||
ourReturn = new ArrayList<IResource>();
|
||||
ourReturn = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ourReturn.add(createPatient(1));
|
||||
ourReturn.add(createObservation(i, "Patient/1"));
|
||||
}
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=5&_format=json");
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_count=5&_format=json&subject=Patient/1");
|
||||
status = ourClient.execute(httpGet);
|
||||
respString = extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
|
@ -1201,7 +1202,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, respString);
|
||||
assertEquals(5, respBundle.getEntry().size());
|
||||
assertEquals(10, respBundle.getTotal().intValue());
|
||||
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Observation/0", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertNotNull(respBundle.getLink("next"));
|
||||
|
||||
// Load next page
|
||||
|
@ -1215,7 +1216,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, respString);
|
||||
assertEquals(5, respBundle.getEntry().size());
|
||||
assertEquals(10, respBundle.getTotal().intValue());
|
||||
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Observation/5", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertNull(respBundle.getLink("next"));
|
||||
|
||||
}
|
||||
|
@ -1226,7 +1227,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1"))
|
||||
.allow("Rule 1").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdDt("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
@ -1236,16 +1237,16 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
String respString;
|
||||
Bundle respBundle;
|
||||
|
||||
ourReturn = new ArrayList<IResource>();
|
||||
ourReturn = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ourReturn.add(createPatient(1));
|
||||
ourReturn.add(createObservation(i, "Patient/1"));
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ourReturn.add(createPatient(2));
|
||||
for (int i = 5; i < 10; i++) {
|
||||
ourReturn.add(createObservation(i, "Patient/2"));
|
||||
}
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=5&_format=json");
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_count=5&_format=json&subject=Patient/1");
|
||||
status = ourClient.execute(httpGet);
|
||||
respString = extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
|
@ -1253,7 +1254,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, respString);
|
||||
assertEquals(5, respBundle.getEntry().size());
|
||||
assertEquals(10, respBundle.getTotal().intValue());
|
||||
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Observation/0", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertNotNull(respBundle.getLink("next"));
|
||||
|
||||
// Load next page
|
||||
|
@ -1261,7 +1262,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
ourHitMethod = false;
|
||||
httpGet = new HttpGet(respBundle.getLink("next").getUrl());
|
||||
status = ourClient.execute(httpGet);
|
||||
respString = extractResponseAndClose(status);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
|
@ -1283,7 +1284,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
HttpResponse status;
|
||||
String response;
|
||||
|
||||
ourReturn = Arrays.asList(createPatient(2));
|
||||
ourReturn = Collections.singletonList(createPatient(2));
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2");
|
||||
status = ourClient.execute(httpGet);
|
||||
|
@ -1291,9 +1292,9 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
ourLog.info(response);
|
||||
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourReturn = Arrays.asList(createObservation(10, "Patient/2"));
|
||||
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
|
@ -1303,7 +1304,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
ourReturn = Arrays.asList(createCarePlan(10, "Patient/2"));
|
||||
ourReturn = Collections.singletonList(createCarePlan(10, "Patient/2"));
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
|
@ -1321,7 +1322,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
ourLog.info(response);
|
||||
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourReturn = Arrays.asList(createPatient(2), createObservation(10, "Patient/1"));
|
||||
ourHitMethod = false;
|
||||
|
@ -1331,7 +1332,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
ourLog.info(response);
|
||||
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
}
|
||||
|
||||
|
@ -1359,7 +1360,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
HttpPost httpPost;
|
||||
HttpResponse status;
|
||||
|
||||
ourReturn = Arrays.asList((IResource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
ourHitMethod = false;
|
||||
httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
|
@ -1419,6 +1420,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
httpPost.setEntity(createFhirResourceEntity(createObservation(null, "Patient/1")));
|
||||
status = ourClient.execute(httpPost);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.debug(response);
|
||||
assertEquals(201, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
}
|
||||
|
@ -1469,7 +1471,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
HttpDelete httpDelete;
|
||||
HttpResponse status;
|
||||
|
||||
ourReturn = Arrays.asList(createPatient(1));
|
||||
ourReturn = Collections.singletonList(createPatient(1));
|
||||
|
||||
ourHitMethod = false;
|
||||
httpDelete = new HttpDelete("http://localhost:" + ourPort + "/Patient?foo=bar");
|
||||
|
@ -1497,7 +1499,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
HttpDelete httpDelete;
|
||||
HttpResponse status;
|
||||
|
||||
ourReturn = Arrays.asList(createPatient(1));
|
||||
ourReturn = Collections.singletonList(createPatient(1));
|
||||
|
||||
ourHitMethod = false;
|
||||
httpDelete = new HttpDelete("http://localhost:" + ourPort + "/Patient?foo=bar");
|
||||
|
@ -1525,7 +1527,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
HttpResponse status;
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Arrays.asList(createPatient(2));
|
||||
ourReturn = Collections.singletonList(createPatient(2));
|
||||
httpDelete = new HttpDelete("http://localhost:" + ourPort + "/Patient/2");
|
||||
status = ourClient.execute(httpDelete);
|
||||
extractResponseAndClose(status);
|
||||
|
@ -1533,7 +1535,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
assertTrue(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Arrays.asList(createPatient(1));
|
||||
ourReturn = Collections.singletonList(createPatient(1));
|
||||
httpDelete = new HttpDelete("http://localhost:" + ourPort + "/Patient/1");
|
||||
status = ourClient.execute(httpDelete);
|
||||
extractResponseAndClose(status);
|
||||
|
@ -1665,6 +1667,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
httpPost.setEntity(createFhirResourceEntity(createPatient(null)));
|
||||
status = ourClient.execute(httpPost);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.debug(response);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
|
@ -1703,6 +1706,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
httpPost.setEntity(createFhirResourceEntity(createPatient(null)));
|
||||
status = ourClient.execute(httpPost);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.debug(response);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
|
@ -1718,7 +1722,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidInstanceIds() throws Exception {
|
||||
public void testInvalidInstanceIds() {
|
||||
try {
|
||||
new RuleBuilder().allow("Rule 1").write().instance((String) null);
|
||||
fail();
|
||||
|
@ -2009,7 +2013,7 @@ public class AuthorizationInterceptorDstu2Test {
|
|||
}
|
||||
|
||||
@Search()
|
||||
public List<IResource> search() {
|
||||
public List<IResource> search(@OptionalParam(name="subject")ReferenceParam theSubject) {
|
||||
ourHitMethod = true;
|
||||
return ourReturn;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,9 +8,11 @@ import ca.uhn.fhir.rest.api.Constants;
|
|||
import ca.uhn.fhir.rest.api.*;
|
||||
import ca.uhn.fhir.rest.api.server.IRequestOperationCallback;
|
||||
import ca.uhn.fhir.rest.api.server.RequestDetails;
|
||||
import ca.uhn.fhir.rest.param.ReferenceParam;
|
||||
import ca.uhn.fhir.rest.server.FifoMemoryPagingProvider;
|
||||
import ca.uhn.fhir.rest.server.IResourceProvider;
|
||||
import ca.uhn.fhir.rest.server.RestfulServer;
|
||||
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
|
||||
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails;
|
||||
import ca.uhn.fhir.rest.server.interceptor.auth.*;
|
||||
import ca.uhn.fhir.rest.server.tenant.UrlBaseTenantIdentificationStrategy;
|
||||
|
@ -287,6 +289,68 @@ public class AuthorizationInterceptorR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowByCompartmentUsingUnqualifiedIds() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder().allow().read().resourcesOfType(CarePlan.class).inCompartment("Patient", new IdType("Patient/123")).andThen().denyAll()
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
||||
HttpGet httpGet;
|
||||
HttpResponse status;
|
||||
|
||||
Patient patient;
|
||||
CarePlan carePlan;
|
||||
|
||||
// Unqualified
|
||||
patient = new Patient();
|
||||
patient.setId("123");
|
||||
carePlan = new CarePlan();
|
||||
carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE);
|
||||
carePlan.getSubject().setResource(patient);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Collections.singletonList(carePlan);
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/135154");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
// Qualified
|
||||
patient = new Patient();
|
||||
patient.setId("Patient/123");
|
||||
carePlan = new CarePlan();
|
||||
carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE);
|
||||
carePlan.getSubject().setResource(patient);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Collections.singletonList(carePlan);
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/135154");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
// Wrong one
|
||||
patient = new Patient();
|
||||
patient.setId("456");
|
||||
carePlan = new CarePlan();
|
||||
carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE);
|
||||
carePlan.getSubject().setResource(patient);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Collections.singletonList(carePlan);
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/135154");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* #528
|
||||
*/
|
||||
|
@ -390,69 +454,6 @@ public class AuthorizationInterceptorR4Test {
|
|||
assertTrue(ourHitMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllowByCompartmentUsingUnqualifiedIds() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder().allow().read().resourcesOfType(CarePlan.class).inCompartment("Patient", new IdType("Patient/123")).andThen().denyAll()
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
||||
HttpGet httpGet;
|
||||
HttpResponse status;
|
||||
|
||||
Patient patient;
|
||||
CarePlan carePlan;
|
||||
|
||||
// Unqualified
|
||||
patient = new Patient();
|
||||
patient.setId("123");
|
||||
carePlan = new CarePlan();
|
||||
carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE);
|
||||
carePlan.getSubject().setResource(patient);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Collections.singletonList(carePlan);
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/135154");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
// Qualified
|
||||
patient = new Patient();
|
||||
patient.setId("Patient/123");
|
||||
carePlan = new CarePlan();
|
||||
carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE);
|
||||
carePlan.getSubject().setResource(patient);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Collections.singletonList(carePlan);
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/135154");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
// Wrong one
|
||||
patient = new Patient();
|
||||
patient.setId("456");
|
||||
carePlan = new CarePlan();
|
||||
carePlan.setStatus(CarePlan.CarePlanStatus.ACTIVE);
|
||||
carePlan.getSubject().setResource(patient);
|
||||
|
||||
ourHitMethod = false;
|
||||
ourReturn = Collections.singletonList(carePlan);
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/135154");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testBatchWhenOnlyTransactionAllowed() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
|
@ -477,7 +478,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
HttpPost httpPost;
|
||||
HttpResponse status;
|
||||
|
||||
ourReturn = Collections.singletonList((Resource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
ourHitMethod = false;
|
||||
httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
|
@ -510,7 +511,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
HttpPost httpPost;
|
||||
HttpResponse status;
|
||||
|
||||
ourReturn = Collections.singletonList((Resource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
ourHitMethod = false;
|
||||
httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
|
@ -544,7 +545,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
HttpResponse status;
|
||||
String response;
|
||||
|
||||
ourReturn = Collections.singletonList((Resource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
ourHitMethod = false;
|
||||
httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
|
@ -1841,13 +1842,86 @@ public class AuthorizationInterceptorR4Test {
|
|||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
|
||||
status = ourClient.execute(httpGet);
|
||||
extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByCompartmentWrong() throws Exception {
|
||||
public void testReadByCompartmentWrongAllTypesProactiveBlockEnabledNoResponse() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/1")).andThen()
|
||||
.build();
|
||||
}
|
||||
}.setFlags());
|
||||
|
||||
HttpGet httpGet;
|
||||
HttpResponse status;
|
||||
String response;
|
||||
|
||||
ourReturn = Collections.emptyList();
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(404, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(404, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/_history");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_history");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/999/_history");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByCompartmentWrongProactiveBlockDisabled() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
|
@ -1856,7 +1930,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
.allow("Rule 2").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
}.setFlags(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS));
|
||||
|
||||
HttpGet httpGet;
|
||||
HttpResponse status;
|
||||
|
@ -1870,7 +1944,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
ourLog.info(response);
|
||||
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
|
||||
ourHitMethod = false;
|
||||
|
@ -1914,6 +1988,133 @@ public class AuthorizationInterceptorR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByCompartmentWrongProactiveBlockDisabledNoResponse() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdType("Patient/1")).andThen()
|
||||
.allow("Rule 2").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
}.setFlags(AuthorizationFlagsEnum.NO_NOT_PROACTIVELY_BLOCK_COMPARTMENT_READ_ACCESS));
|
||||
|
||||
HttpGet httpGet;
|
||||
HttpResponse status;
|
||||
String response;
|
||||
|
||||
ourReturn = Collections.emptyList();
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(404, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByCompartmentWrongProactiveBlockEnabledNoResponse() throws Exception {
|
||||
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
|
||||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdType("Patient/1")).andThen()
|
||||
.allow("Rule 2").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
}.setFlags());
|
||||
|
||||
HttpGet httpGet;
|
||||
HttpResponse status;
|
||||
String response;
|
||||
|
||||
ourReturn = Collections.emptyList();
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(404, status.getStatusLine().getStatusCode());
|
||||
assertTrue(ourHitMethod);
|
||||
|
||||
// CarePlan could potentially be in the Patient/1 compartment but we don't
|
||||
// have any rules explicitly allowing CarePlan so it's blocked
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/CarePlan/10");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/_history");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/_history");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/999/_history");
|
||||
status = ourClient.execute(httpGet);
|
||||
response = extractResponseAndClose(status);
|
||||
ourLog.info(response);
|
||||
assertEquals(403, status.getStatusLine().getStatusCode());
|
||||
assertFalse(ourHitMethod);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testReadByInstance() throws Exception {
|
||||
ourConditionalCreateId = "1";
|
||||
|
@ -1965,7 +2166,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.allow("Rule 1").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
@ -1977,11 +2178,11 @@ public class AuthorizationInterceptorR4Test {
|
|||
|
||||
ourReturn = new ArrayList<>();
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ourReturn.add(createPatient(1));
|
||||
ourReturn.add(createObservation(i, "Patient/1"));
|
||||
}
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=5&_format=json");
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_count=5&_format=json&subject=Patient/1");
|
||||
status = ourClient.execute(httpGet);
|
||||
respString = extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
|
@ -1989,7 +2190,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, respString);
|
||||
assertEquals(5, respBundle.getEntry().size());
|
||||
assertEquals(10, respBundle.getTotal());
|
||||
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Observation/0", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertNotNull(respBundle.getLink("next"));
|
||||
|
||||
// Load next page
|
||||
|
@ -2003,7 +2204,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, respString);
|
||||
assertEquals(5, respBundle.getEntry().size());
|
||||
assertEquals(10, respBundle.getTotal());
|
||||
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Observation/5", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertNull(respBundle.getLink("next"));
|
||||
|
||||
}
|
||||
|
@ -2014,7 +2215,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
@Override
|
||||
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
|
||||
return new RuleBuilder()
|
||||
.allow("Rule 1").read().resourcesOfType(Patient.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.allow("Rule 1").read().resourcesOfType(Observation.class).inCompartment("Patient", new IdType("Patient/1"))
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
@ -2026,14 +2227,14 @@ public class AuthorizationInterceptorR4Test {
|
|||
|
||||
ourReturn = new ArrayList<>();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ourReturn.add(createPatient(1));
|
||||
ourReturn.add(createObservation(i, "Patient/1"));
|
||||
}
|
||||
for (int i = 0; i < 5; i++) {
|
||||
ourReturn.add(createPatient(2));
|
||||
for (int i = 5; i < 10; i++) {
|
||||
ourReturn.add(createObservation(i, "Patient/2"));
|
||||
}
|
||||
|
||||
ourHitMethod = false;
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient?_count=5&_format=json");
|
||||
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation?_count=5&_format=json&subject=Patient/1");
|
||||
status = ourClient.execute(httpGet);
|
||||
respString = extractResponseAndClose(status);
|
||||
assertEquals(200, status.getStatusLine().getStatusCode());
|
||||
|
@ -2041,7 +2242,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
respBundle = ourCtx.newJsonParser().parseResource(Bundle.class, respString);
|
||||
assertEquals(5, respBundle.getEntry().size());
|
||||
assertEquals(10, respBundle.getTotal());
|
||||
assertEquals("Patient/1", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertEquals("Observation/0", respBundle.getEntry().get(0).getResource().getIdElement().toUnqualifiedVersionless().getValue());
|
||||
assertNotNull(respBundle.getLink("next"));
|
||||
|
||||
// Load next page
|
||||
|
@ -2078,7 +2279,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
Bundle input = createTransactionWithPlaceholdersRequestBundle();
|
||||
Bundle output = createTransactionWithPlaceholdersResponseBundle();
|
||||
|
||||
ourReturn = Collections.singletonList((Resource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
CloseableHttpResponse status = ourClient.execute(httpPost);
|
||||
|
@ -2111,7 +2312,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
Bundle input = createTransactionWithPlaceholdersRequestBundle();
|
||||
Bundle output = createTransactionWithPlaceholdersResponseBundle();
|
||||
|
||||
ourReturn = Collections.singletonList((Resource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
HttpPost httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
CloseableHttpResponse status = ourClient.execute(httpPost);
|
||||
|
@ -2146,7 +2347,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
HttpPost httpPost;
|
||||
HttpResponse status;
|
||||
|
||||
ourReturn = Collections.singletonList((Resource) output);
|
||||
ourReturn = Collections.singletonList(output);
|
||||
ourHitMethod = false;
|
||||
httpPost = new HttpPost("http://localhost:" + ourPort + "/");
|
||||
httpPost.setEntity(createFhirResourceEntity(input));
|
||||
|
@ -2640,6 +2841,9 @@ public class AuthorizationInterceptorR4Test {
|
|||
@Read(version = true)
|
||||
public CarePlan read(@IdParam IdType theId) {
|
||||
ourHitMethod = true;
|
||||
if (ourReturn.isEmpty()) {
|
||||
throw new ResourceNotFoundException(theId);
|
||||
}
|
||||
return (CarePlan) ourReturn.get(0);
|
||||
}
|
||||
|
||||
|
@ -2733,11 +2937,14 @@ public class AuthorizationInterceptorR4Test {
|
|||
@Read(version = true)
|
||||
public Observation read(@IdParam IdType theId) {
|
||||
ourHitMethod = true;
|
||||
if (ourReturn.isEmpty()) {
|
||||
throw new ResourceNotFoundException(theId);
|
||||
}
|
||||
return (Observation) ourReturn.get(0);
|
||||
}
|
||||
|
||||
@Search()
|
||||
public List<Resource> search() {
|
||||
public List<Resource> search(@OptionalParam(name = "subject") ReferenceParam theSubject) {
|
||||
ourHitMethod = true;
|
||||
return ourReturn;
|
||||
}
|
||||
|
@ -2764,6 +2971,7 @@ public class AuthorizationInterceptorR4Test {
|
|||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static class DummyPatientResourceProvider implements IResourceProvider {
|
||||
|
||||
@Create()
|
||||
|
@ -2861,6 +3069,9 @@ public class AuthorizationInterceptorR4Test {
|
|||
@Read(version = true)
|
||||
public Patient read(@IdParam IdType theId) {
|
||||
ourHitMethod = true;
|
||||
if (ourReturn.isEmpty()) {
|
||||
throw new ResourceNotFoundException(theId);
|
||||
}
|
||||
return (Patient) ourReturn.get(0);
|
||||
}
|
||||
|
||||
|
|
|
@ -1917,7 +1917,7 @@ public class InstanceValidator extends BaseValidator implements IResourceValidat
|
|||
bundle.getNamedChildren("entry", entries);
|
||||
Element match = null;
|
||||
for (Element we : entries) {
|
||||
if (we.getChildValue("fullUrl").equals(targetUrl)) {
|
||||
if (targetUrl.equals(we.getChildValue("fullUrl"))) {
|
||||
Element r = we.getNamedChild("resource");
|
||||
if (version.isEmpty()) {
|
||||
rule(errors, IssueType.FORBIDDEN, -1, -1, path, match==null, "Multiple matches in bundle for reference " + ref);
|
||||
|
|
|
@ -317,6 +317,15 @@ public class FhirInstanceValidatorR4Test {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateBundleWithNoFullUrl() throws IOException {
|
||||
String encoded = IOUtils.toString(FhirInstanceValidatorR4Test.class.getResourceAsStream("/r4/r4-caredove-bundle.json"));
|
||||
|
||||
ValidationResult output = myVal.validateWithResult(encoded);
|
||||
List<SingleValidationMessage> errors = logResultsAndReturnNonInformationalOnes(output);
|
||||
assertEquals(44, errors.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBase64Valid() {
|
||||
Base64BinaryType value = new Base64BinaryType(new byte[] {2, 3, 4, 5, 6, 7, 8, 9, 8, 7, 6, 5, 4, 3, 2, 1});
|
||||
|
|
|
@ -0,0 +1,815 @@
|
|||
{
|
||||
|
||||
"resourceType": "Bundle",
|
||||
|
||||
"type": "transaction",
|
||||
|
||||
"timestamp": "2018-03-09T15:21:51.2112Z",
|
||||
|
||||
"entry": [
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "ServiceRequest",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"text": {
|
||||
|
||||
"status": "generated",
|
||||
|
||||
"div": "Human readable HTML narrative of entire ServiceRequest and related resources goes here. For brevity sake, it is omitted here, but can be included in an actual transmission"
|
||||
|
||||
},
|
||||
|
||||
"status": "active",
|
||||
|
||||
"intent": "proposal",
|
||||
|
||||
"priority": "routine",
|
||||
|
||||
"subject": {
|
||||
|
||||
"reference": "Patient/1"
|
||||
|
||||
},
|
||||
|
||||
"authoredOn": "2018-03-09T15:21:51Z",
|
||||
|
||||
"requester": {
|
||||
|
||||
"reference": "PractitionerRole/1"
|
||||
|
||||
},
|
||||
|
||||
"performer": {
|
||||
|
||||
"reference": "https://www.caredove.com/FHIR3/HealthcareService/8654"
|
||||
|
||||
},
|
||||
|
||||
"reasonCode": [
|
||||
|
||||
{
|
||||
|
||||
"text": "Reason for referral narrative goes here"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"supportingInfo": [
|
||||
|
||||
{
|
||||
|
||||
"reference": "DocumentReference/1"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"note": [
|
||||
|
||||
{
|
||||
|
||||
"text": "Allergies: Penicillin \nSocial History: History of family conflict \nlow social interaction \nFood Allergies: Peanuts"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "ServiceRequest"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Patient",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"identifier": [
|
||||
|
||||
{
|
||||
|
||||
"type": {
|
||||
|
||||
"coding": [
|
||||
|
||||
{
|
||||
|
||||
"code": "JHN",
|
||||
|
||||
"system": "http://hl7.org/fhir/v2/0203"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"text": "Ontario PHN"
|
||||
|
||||
},
|
||||
|
||||
"value": "4455 044 033",
|
||||
|
||||
"system": "http://ehealthontario.ca/API/FHIR/NamingSystem/ca-on-patient-hcn",
|
||||
|
||||
"extension": [
|
||||
|
||||
{
|
||||
|
||||
"url": "https://www.caredove.com/FHIR3/StructureDefinition/caredove-healthcardversion",
|
||||
|
||||
"valueString": "H"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"name": [
|
||||
|
||||
{
|
||||
|
||||
"given": [
|
||||
|
||||
"John",
|
||||
|
||||
"Scott"
|
||||
|
||||
],
|
||||
|
||||
"family": "Smith"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"telecom": [
|
||||
|
||||
{
|
||||
|
||||
"system": "phone",
|
||||
|
||||
"value": "(555) 111-1111",
|
||||
|
||||
"use": "mobile",
|
||||
|
||||
"rank": 1
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"system": "phone",
|
||||
|
||||
"value": "(555) 222-2222",
|
||||
|
||||
"rank": 2
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"system": "email",
|
||||
|
||||
"value": "testpatient@caredove.com"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"gender": "male",
|
||||
|
||||
"birthDate": "1928-06-29",
|
||||
|
||||
"address": [
|
||||
|
||||
{
|
||||
|
||||
"use": "home",
|
||||
|
||||
"type": "physical",
|
||||
|
||||
"line": [
|
||||
|
||||
"Unit 2",
|
||||
|
||||
"50 Albert St."
|
||||
|
||||
],
|
||||
|
||||
"city": "Waterloo",
|
||||
|
||||
"state": "ON",
|
||||
|
||||
"postalCode": "K8N 1N1",
|
||||
|
||||
"country": "Can"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"maritalStatus": {
|
||||
|
||||
"coding": [
|
||||
|
||||
{
|
||||
|
||||
"code": "M",
|
||||
|
||||
"display": "Married"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"text": "Married"
|
||||
|
||||
},
|
||||
|
||||
"contact": [
|
||||
|
||||
{
|
||||
|
||||
"relationship": [
|
||||
|
||||
{
|
||||
|
||||
"text": "Alternate Contact"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"name": {
|
||||
|
||||
"given": [
|
||||
|
||||
"Shemergency",
|
||||
|
||||
"Scott"
|
||||
|
||||
],
|
||||
|
||||
"family": "McContact"
|
||||
|
||||
},
|
||||
|
||||
"telecom": [
|
||||
|
||||
{
|
||||
|
||||
"system": "phone",
|
||||
|
||||
"value": "(555) 111-1111",
|
||||
|
||||
"use": "mobile",
|
||||
|
||||
"rank": 1
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"system": "phone",
|
||||
|
||||
"value": "(555) 222-2222",
|
||||
|
||||
"rank": 2
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"system": "email",
|
||||
|
||||
"value": "testcontact@caredove.com"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"address": {
|
||||
|
||||
"use": "home",
|
||||
|
||||
"type": "physical",
|
||||
|
||||
"line": [
|
||||
|
||||
"Unit 2",
|
||||
|
||||
"50 Albert St."
|
||||
|
||||
],
|
||||
|
||||
"city": "Waterloo",
|
||||
|
||||
"state": "ON",
|
||||
|
||||
"postalCode": "32819",
|
||||
|
||||
"country": "Can"
|
||||
|
||||
},
|
||||
|
||||
"gender": "female"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"communication": [
|
||||
|
||||
{
|
||||
|
||||
"language": {
|
||||
|
||||
"coding": [
|
||||
|
||||
{
|
||||
|
||||
"code": "en",
|
||||
|
||||
"display": "English"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"text": "English"
|
||||
|
||||
},
|
||||
|
||||
"preferred": true
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"generalPractitioner": [
|
||||
|
||||
{
|
||||
|
||||
"reference": "PractitionerRole/2"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Patient"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "PractitionerRole",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"practitioner": {
|
||||
|
||||
"reference": "Practitioner/1"
|
||||
|
||||
},
|
||||
|
||||
"organization": {
|
||||
|
||||
"reference": "Organization/1"
|
||||
|
||||
},
|
||||
|
||||
"location": [
|
||||
|
||||
{
|
||||
|
||||
"reference": "Location/1"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"telecom": [
|
||||
|
||||
{
|
||||
|
||||
"system": "phone",
|
||||
|
||||
"value": "(555) 111-1111",
|
||||
|
||||
"use": "work"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"system": "email",
|
||||
|
||||
"value": "testsender@caredove.com",
|
||||
|
||||
"use": "work"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "PractitionerRole"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Practitioner",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"name": [
|
||||
|
||||
{
|
||||
|
||||
"given": [
|
||||
|
||||
"Requesty"
|
||||
|
||||
],
|
||||
|
||||
"family": "McSenderson"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Practitioner"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Organization",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"name": "North Sender Clinic"
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Organization"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Location",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"name": "Downtown Sender Hub",
|
||||
|
||||
"address": {
|
||||
|
||||
"use": "work",
|
||||
|
||||
"type": "physical",
|
||||
|
||||
"line": [
|
||||
|
||||
"Suite 11",
|
||||
|
||||
"11 King st. West"
|
||||
|
||||
],
|
||||
|
||||
"city": "Kitchener",
|
||||
|
||||
"state": "ON",
|
||||
|
||||
"postalCode": "N2L 1T1",
|
||||
|
||||
"country": "Can"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Location"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "PractitionerRole",
|
||||
|
||||
"id": 2,
|
||||
|
||||
"practitioner": {
|
||||
|
||||
"reference": "Practitioner/2"
|
||||
|
||||
},
|
||||
|
||||
"organization": {
|
||||
|
||||
"reference": "Organization/2"
|
||||
|
||||
},
|
||||
|
||||
"location": [
|
||||
|
||||
{
|
||||
|
||||
"reference": "Location/2"
|
||||
|
||||
}
|
||||
|
||||
],
|
||||
|
||||
"telecom": [
|
||||
|
||||
{
|
||||
|
||||
"system": "phone",
|
||||
|
||||
"value": "(555) 222-2222",
|
||||
|
||||
"use": "work"
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"system": "email",
|
||||
|
||||
"value": "familydoc@caredove.com",
|
||||
|
||||
"use": "work"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "PractitionerRole"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Practitioner",
|
||||
|
||||
"id": 2,
|
||||
|
||||
"name": [
|
||||
|
||||
{
|
||||
|
||||
"given": [
|
||||
|
||||
"Dr. Prim"
|
||||
|
||||
],
|
||||
|
||||
"family": "Caredoc"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Practitioner"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Organization",
|
||||
|
||||
"id": 2,
|
||||
|
||||
"name": "Star Family Health Team"
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Organization"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "DocumentReference",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"status": "current",
|
||||
|
||||
"created": "2018-03-09T15:21:51.2112Z",
|
||||
|
||||
"description": "Filename or Document Title goes here",
|
||||
|
||||
"content": [
|
||||
|
||||
{
|
||||
|
||||
"attachment": "NEEDS WORK - ATTACHMENT DATA TYPE",
|
||||
|
||||
"format": "NEEDS WORK - FORMAT INFO"
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Practitioner"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Location",
|
||||
|
||||
"id": 2,
|
||||
|
||||
"name": "West Side GP Office",
|
||||
|
||||
"address": {
|
||||
|
||||
"use": "work",
|
||||
|
||||
"type": "physical",
|
||||
|
||||
"line": [
|
||||
|
||||
"22 Weber st. East"
|
||||
|
||||
],
|
||||
|
||||
"city": "Kitchener",
|
||||
|
||||
"state": "ON",
|
||||
|
||||
"postalCode": "N2L 2T2",
|
||||
|
||||
"country": "Can"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Location"
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
"resource": {
|
||||
|
||||
"resourceType": "Task",
|
||||
|
||||
"id": 1,
|
||||
|
||||
"basedOn" : {
|
||||
|
||||
"reference" : "ServiceRequest/1"
|
||||
|
||||
},
|
||||
|
||||
"status" : "requested",
|
||||
|
||||
"businessStatus " : {
|
||||
|
||||
"text" : "Waiting for preliminary review"
|
||||
|
||||
},
|
||||
|
||||
"intent" : "proposal",
|
||||
|
||||
"code" : {
|
||||
|
||||
"text" : "Process Request"
|
||||
|
||||
},
|
||||
|
||||
"description" : "Process and close this referral request",
|
||||
|
||||
"authoredOn" : "2018-03-09T15:21:51Z",
|
||||
|
||||
"lastModified" : "2018-03-09T15:21:51Z"
|
||||
|
||||
},
|
||||
|
||||
"request": {
|
||||
|
||||
"method": "POST",
|
||||
|
||||
"url": "Task"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
]
|
||||
|
||||
}
|
|
@ -42,6 +42,30 @@
|
|||
Spring-data (used by the JPA server) has been upgraded to version 2.0.7
|
||||
(from version 1.11.6). Thanks to Roman Doboni for the pull request!
|
||||
</action>
|
||||
<action type="fix">
|
||||
A crash in the validator was fixed: Validating a Bundle that did not have fullUrl entries
|
||||
in a required spot caused a NullPointerException.
|
||||
</action>
|
||||
<action type="add">
|
||||
AuthorizationInterceptor now examines requests more closely in order
|
||||
to block requests early that are not possibly going to return
|
||||
allowable results when compartment rules are used. For example,
|
||||
if an AuthorizationInterceptor is configured to allow only
|
||||
<![CDATA[<b>read</b>]]>
|
||||
access to compartment
|
||||
<![CDATA[<code>Patient/123</code>]]>,
|
||||
a search for
|
||||
<![CDATA[<code>Observation?subject=987</code>]]>
|
||||
will now be blocked before the method handler is called. Previously
|
||||
the search was performed and the results were examined in order to
|
||||
determine whether they were all in the appropriate compartment, but
|
||||
this incurs a performance cost, and means that this search would
|
||||
successfully return an empty Bundle if no matches were present.
|
||||
<![CDATA[<br/><br/>]]>
|
||||
A new setting on AuthorizationInterceptor called
|
||||
<![CDATA[<code>setFlags(flags)</code>]]>
|
||||
can be used to maintain the previous behaviour.
|
||||
</action>
|
||||
</release>
|
||||
<release version="3.4.0" date="2018-05-28">
|
||||
<action type="add">
|
||||
|
|
Loading…
Reference in New Issue