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:
James Agnew 2018-06-10 17:48:20 -04:00 committed by GitHub
parent 903df68d2a
commit cc0e836680
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1680 additions and 2592 deletions

View File

@ -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();

View File

@ -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();

View File

@ -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;
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -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");
}
@ -333,7 +371,7 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
if (theResponseObject == null) {
return Collections.emptyList();
}
List<IBaseResource> retVal;
boolean isContainer = false;
@ -342,11 +380,11 @@ public class AuthorizationInterceptor extends ServerOperationInterceptorAdapter
} else if (theResponseObject instanceof IBaseParameters) {
isContainer = true;
}
if (!isContainer) {
return Collections.singletonList(theResponseObject);
}
retVal = fhirContext.newTerser().getAllPopulatedChildElementsOfType(theResponseObject, IBaseResource.class);
// Exclude the container

View File

@ -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

View File

@ -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;
}
}

View File

@ -174,7 +174,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
}
if (myOperationRule != null) {
myOperationRule.setTenentApplicabilityChecker(myTenantApplicabilityChecker);
myOperationRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
}
}

View File

@ -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;

View File

@ -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;
/*
@ -11,9 +34,9 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -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,232 +79,327 @@ 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) {
switch (theOperation) {
case READ:
case VREAD:
appliesToResourceId = theInputResourceId;
appliesToResourceType = theInputResourceId.getResourceType();
break;
case SEARCH_SYSTEM:
case SEARCH_TYPE:
case HISTORY_INSTANCE:
case HISTORY_SYSTEM:
case HISTORY_TYPE:
case READ:
if (theOutputResource == null) {
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return new Verdict(PolicyEnum.ALLOW, this);
default:
return null;
switch (theOperation) {
case READ:
case VREAD:
appliesToResourceId = theInputResourceId;
appliesToResourceType = theInputResourceId.getResourceType();
break;
case SEARCH_SYSTEM:
case HISTORY_SYSTEM:
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;
}
}
appliesToResource = theOutputResource;
if (theOutputResource != null) {
appliesToResourceId = theOutputResource.getIdElement();
}
}
appliesToResource = theOutputResource;
if (theOutputResource != null) {
appliesToResourceId = theOutputResource.getIdElement();
}
break;
case WRITE:
if (theInputResource == null && theInputResourceId == null) {
return null;
}
switch (theOperation) {
case CREATE:
case UPDATE:
case ADD_TAGS:
case DELETE_TAGS:
case META_ADD:
case META_DELETE:
case PATCH:
appliesToResource = theInputResource;
appliesToResourceId = theInputResourceId;
break;
default:
return null;
}
break;
case DELETE:
if (theOperation == RestOperationTypeEnum.DELETE) {
if (theInputResource == null) {
return newVerdict();
}
appliesToResource = theInputResource;
} else {
return null;
}
break;
case BATCH:
case TRANSACTION:
if (!(theOperation == RestOperationTypeEnum.TRANSACTION)) {
return null;
}
if (theInputResource != null && requestAppliesToTransaction(ctx, myOp, theInputResource)) {
if (getMode() == PolicyEnum.DENY) {
return new Verdict(PolicyEnum.DENY, this);
}
List<BundleEntryParts> inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource);
Verdict verdict = null;
for (BundleEntryParts nextPart : inputResources) {
IBaseResource inputResource = nextPart.getResource();
RestOperationTypeEnum operation = null;
if (nextPart.getRequestType() == RequestTypeEnum.GET) {
continue;
}
if (nextPart.getRequestType() == RequestTypeEnum.POST) {
operation = RestOperationTypeEnum.CREATE;
} else if (nextPart.getRequestType() == RequestTypeEnum.PUT) {
operation = RestOperationTypeEnum.UPDATE;
} else {
throw new InvalidRequestException("Can not handle transaction with operation of type " + nextPart.getRequestType());
}
/*
* This is basically just being conservative - Be careful of transactions containing
* nested operations and nested transactions. We block the by default. At some point
* it would be nice to be more nuanced here.
*/
RuntimeResourceDefinition resourceDef = ctx.getResourceDefinition(nextPart.getResource());
if ("Parameters".equals(resourceDef.getName()) || "Bundle".equals(resourceDef.getName())) {
throw new InvalidRequestException("Can not handle transaction with nested resource of type " + resourceDef.getName());
}
Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(operation, theRequestDetails, inputResource, null, null);
if (newVerdict == null) {
continue;
} else if (verdict == null) {
verdict = newVerdict;
} else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) {
verdict = newVerdict;
}
}
return verdict;
} else if (theOutputResource != null) {
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(theOutputResource, theRequestDetails.getFhirContext());
Verdict verdict = null;
for (IBaseResource nextResource : outputResources) {
if (nextResource == null) {
continue;
}
Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(RestOperationTypeEnum.READ, theRequestDetails, null, null, nextResource);
if (newVerdict == null) {
continue;
} else if (verdict == null) {
verdict = newVerdict;
} else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) {
verdict = newVerdict;
}
}
return verdict;
} else {
return null;
}
case ALLOW_ALL:
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return new Verdict(PolicyEnum.ALLOW, this);
case DENY_ALL:
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return new Verdict(PolicyEnum.DENY, this);
case METADATA:
if (theOperation == RestOperationTypeEnum.METADATA) {
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
case WRITE:
if (theInputResource == null && theInputResourceId == null) {
return null;
}
return newVerdict();
}
return null;
default:
// Should not happen
throw new IllegalStateException("Unable to apply security to event of type " + theOperation);
}
switch (theOperation) {
case CREATE:
case UPDATE:
case ADD_TAGS:
case DELETE_TAGS:
case META_ADD:
case META_DELETE:
case PATCH:
appliesToResource = theInputResource;
appliesToResourceId = theInputResourceId;
break;
default:
return null;
}
break;
case DELETE:
if (theOperation == RestOperationTypeEnum.DELETE) {
if (theInputResource == null) {
return newVerdict();
}
appliesToResource = theInputResource;
} else {
return null;
}
break;
case BATCH:
case TRANSACTION:
if (!(theOperation == RestOperationTypeEnum.TRANSACTION)) {
return null;
}
if (theInputResource != null && requestAppliesToTransaction(ctx, myOp, theInputResource)) {
if (getMode() == PolicyEnum.DENY) {
return new Verdict(PolicyEnum.DENY, this);
}
List<BundleEntryParts> inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource);
Verdict verdict = null;
for (BundleEntryParts nextPart : inputResources) {
switch (myAppliesTo) {
case INSTANCES:
if (appliesToResourceId != null) {
for (IIdType next : myAppliesToInstances) {
if (isNotBlank(next.getResourceType())) {
if (!next.getResourceType().equals(appliesToResourceId.getResourceType())) {
IBaseResource inputResource = nextPart.getResource();
RestOperationTypeEnum operation = null;
if (nextPart.getRequestType() == RequestTypeEnum.GET) {
continue;
}
if (nextPart.getRequestType() == RequestTypeEnum.POST) {
operation = RestOperationTypeEnum.CREATE;
} else if (nextPart.getRequestType() == RequestTypeEnum.PUT) {
operation = RestOperationTypeEnum.UPDATE;
} else {
throw new InvalidRequestException("Can not handle transaction with operation of type " + nextPart.getRequestType());
}
/*
* This is basically just being conservative - Be careful of transactions containing
* nested operations and nested transactions. We block the by default. At some point
* it would be nice to be more nuanced here.
*/
RuntimeResourceDefinition resourceDef = ctx.getResourceDefinition(nextPart.getResource());
if ("Parameters".equals(resourceDef.getName()) || "Bundle".equals(resourceDef.getName())) {
throw new InvalidRequestException("Can not handle transaction with nested resource of type " + resourceDef.getName());
}
Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(operation, theRequestDetails, inputResource, null, null);
if (newVerdict == null) {
continue;
} else if (verdict == null) {
verdict = newVerdict;
} else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) {
verdict = newVerdict;
}
}
if (!next.getIdPart().equals(appliesToResourceId.getIdPart())) {
continue;
return verdict;
} else if (theOutputResource != null) {
List<IBaseResource> outputResources = AuthorizationInterceptor.toListOfResourcesAndExcludeContainer(theOutputResource, theRequestDetails.getFhirContext());
Verdict verdict = null;
for (IBaseResource nextResource : outputResources) {
if (nextResource == null) {
continue;
}
Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(RestOperationTypeEnum.READ, theRequestDetails, null, null, nextResource);
if (newVerdict == null) {
continue;
} else if (verdict == null) {
verdict = newVerdict;
} else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) {
verdict = newVerdict;
}
}
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return newVerdict();
return verdict;
} else {
return null;
}
}
return null;
case ALL_RESOURCES:
if (appliesToResourceType != null) {
case ALLOW_ALL:
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return new Verdict(PolicyEnum.ALLOW, this);
}
break;
case TYPES:
if (appliesToResource != null) {
if (myAppliesToTypes.contains(appliesToResource.getClass()) == false) {
case DENY_ALL:
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
}
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)) {
return new Verdict(PolicyEnum.DENY, this);
case METADATA:
if (theOperation == RestOperationTypeEnum.METADATA) {
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return new Verdict(PolicyEnum.ALLOW, this);
return newVerdict();
}
}
break;
default:
throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo);
return null;
default:
// Should not happen
throw new IllegalStateException("Unable to apply security to event of type " + theOperation);
}
switch (myAppliesTo) {
case INSTANCES:
if (appliesToResourceId != null) {
for (IIdType next : myAppliesToInstances) {
if (isNotBlank(next.getResourceType())) {
if (!next.getResourceType().equals(appliesToResourceId.getResourceType())) {
continue;
}
}
if (!next.getIdPart().equals(appliesToResourceId.getIdPart())) {
continue;
}
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
return null;
}
return newVerdict();
}
}
return null;
case ALL_RESOURCES:
if (appliesToResourceType != null) {
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;
default:
throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo);
}
switch (myClassifierType) {
case ANY_ID:
break;
case IN_COMPARTMENT:
FhirTerser t = ctx.newTerser();
boolean foundMatch = false;
for (IIdType next : myClassifierCompartmentOwners) {
if (appliesToResource != null) {
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, appliesToResource, next)) {
foundMatch = true;
break;
case ANY_ID:
break;
case IN_COMPARTMENT:
FhirTerser t = ctx.newTerser();
boolean foundMatch = false;
for (IIdType next : myClassifierCompartmentOwners) {
if (appliesToResource != null) {
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, appliesToResource, next)) {
foundMatch = true;
break;
}
}
if (appliesToResourceId != null && appliesToResourceId.hasResourceType() && appliesToResourceId.hasIdPart()) {
if (appliesToResourceId.toUnqualifiedVersionless().getValue().equals(next.toUnqualifiedVersionless().getValue())) {
foundMatch = true;
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 (appliesToResourceId != null && appliesToResourceId.hasResourceType() && appliesToResourceId.hasIdPart()) {
if (appliesToResourceId.toUnqualifiedVersionless().getValue().equals(next.toUnqualifiedVersionless().getValue())) {
foundMatch = true;
break;
}
if (!foundMatch) {
return null;
}
}
if (!foundMatch) {
return null;
}
break;
default:
throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo);
break;
default:
throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo);
}
if (!applyTesters(theOperation, theRequestDetails, theInputResourceId, theInputResource, theOutputResource)) {
@ -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) {
@ -334,23 +425,23 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
IBaseBundle request = (IBaseBundle) theInputResource;
String bundleType = BundleUtil.getBundleType(theContext, request);
switch (theOp) {
case TRANSACTION:
return "transaction".equals(bundleType);
case BATCH:
return "batch".equals(bundleType);
default:
return false;
case TRANSACTION:
return "transaction".equals(bundleType);
case BATCH:
return "batch".equals(bundleType);
default:
return false;
}
}
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();
}
}

View File

@ -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,8 +1178,8 @@ 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"))
.build();
.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,8 +1227,8 @@ 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"))
.build();
.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;
}

View File

@ -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);
}

View File

@ -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);

View File

@ -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});

View File

@ -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"
}
}
]
}

View File

@ -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">