From eee168ced653522f80e42f3e9fcde8167a9f6381 Mon Sep 17 00:00:00 2001 From: James Agnew Date: Mon, 15 Aug 2016 13:35:50 -0400 Subject: [PATCH 1/3] Add support for conditional operations to AuthorizationInterceptor --- .../ca/uhn/fhir/context/FhirVersionEnum.java | 4 + ...turningMethodBindingWithResourceParam.java | 4 + .../rest/method/ConditionalParamBinder.java | 24 +- .../fhir/rest/method/CreateMethodBinding.java | 12 +- .../uhn/fhir/rest/method/RequestDetails.java | 93 +- .../fhir/rest/method/UpdateMethodBinding.java | 27 +- .../auth/AuthorizationInterceptor.java | 47 +- .../server/interceptor/auth/BaseRule.java | 4 +- .../server/interceptor/auth/IAuthRule.java | 4 +- .../auth/IAuthRuleBuilderAppliesTo.java | 17 + .../auth/IAuthRuleBuilderRule.java | 52 +- .../auth/IAuthRuleBuilderRuleConditional.java | 5 + ...hRuleBuilderRuleConditionalClassifier.java | 27 + .../auth/IAuthRuleBuilderRuleOp.java | 36 +- ...thRuleBuilderRuleOpClassifierFinished.java | 15 +- .../interceptor/auth/IAuthRuleFinished.java | 17 + .../server/interceptor/auth/IRuleApplier.java | 3 +- .../interceptor/auth/OperationRule.java | 4 +- .../server/interceptor/auth/RuleBuilder.java | 210 ++-- .../interceptor/auth/RuleImplConditional.java | 63 ++ .../auth/{Rule.java => RuleImplOp.java} | 55 +- ...bstractJaxRsResourceProviderDstu3Test.java | 4 +- .../AbstractJaxRsResourceProviderTest.java | 4 +- .../fhir/jpa/dao/BaseHapiFhirResourceDao.java | 5 +- .../dao/dstu3/FhirResourceDaoDstu3Test.java | 24 +- ...nInterceptorResourceProviderDstu3Test.java | 1 + .../dstu3/ResourceProviderDstu3Test.java | 114 ++- .../model/dstu2/resource/BaseResource.java | 2 + .../AuthorizationInterceptorDstu2Test.java | 928 +++++++++++------- .../uhn/fhir/rest/server/UpdateDstu3Test.java | 29 +- src/changes/changes.xml | 15 + src/site/xdoc/doc_rest_server_security.xml | 4 +- 32 files changed, 1209 insertions(+), 644 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderAppliesTo.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditional.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditionalClassifier.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java rename hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/{Rule.java => RuleImplOp.java} (81%) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java index 94496f2bfd6..00deaf72057 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/context/FhirVersionEnum.java @@ -88,6 +88,10 @@ public enum FhirVersionEnum { return ordinal() > theVersion.ordinal(); } + public boolean isOlderThan(FhirVersionEnum theVersion) { + return ordinal() < theVersion.ordinal(); + } + /** * Returns true if the given version is present on the classpath */ diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java index 5b2fdcf569c..7bab0439cd3 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/BaseOutcomeReturningMethodBindingWithResourceParam.java @@ -104,6 +104,10 @@ abstract class BaseOutcomeReturningMethodBindingWithResourceParam extends BaseOu if (getContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) { resource.setId(urlId); } else { + if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3) == false) { + resource.setId(theRequest.getId()); + } + String matchUrl = null; if (myConditionalUrlIndex != -1) { matchUrl = (String) theParams[myConditionalUrlIndex]; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java index 073bd67a09c..69266d0efb2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/ConditionalParamBinder.java @@ -67,29 +67,7 @@ class ConditionalParamBinder implements IParameter { @Override public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding theMethodBinding) throws InternalErrorException, InvalidRequestException { - - if (myOperationType == RestOperationTypeEnum.CREATE) { - String retVal = theRequest.getHeader(Constants.HEADER_IF_NONE_EXIST); - if (isBlank(retVal)) { - return null; - } - if (retVal.startsWith(theRequest.getFhirServerBase())) { - retVal = retVal.substring(theRequest.getFhirServerBase().length()); - } - return retVal; - } else if (myOperationType != RestOperationTypeEnum.DELETE && myOperationType != RestOperationTypeEnum.UPDATE) { - return null; - } - - if (theRequest.getId() != null && theRequest.getId().hasIdPart()) { - return null; - } - - int questionMarkIndex = theRequest.getCompleteUrl().indexOf('?'); - if (questionMarkIndex == -1) { - return null; - } - return theRequest.getResourceName() + theRequest.getCompleteUrl().substring(questionMarkIndex); + return theRequest.getConditionalUrl(myOperationType); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java index de189c87b08..1eb140ad27b 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/CreateMethodBinding.java @@ -27,8 +27,10 @@ import java.util.Collections; import java.util.Set; 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.FhirVersionEnum; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.api.RequestTypeEnum; @@ -79,9 +81,13 @@ public class CreateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "idInUrlForCreate", theUrlId); throw new InvalidRequestException(msg); } - if (isNotBlank(theResourceId)) { - String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "idInBodyForCreate", theResourceId); - throw new InvalidRequestException(msg); + if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { + if (isNotBlank(theResourceId)) { + String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "idInBodyForCreate", theResourceId); + throw new InvalidRequestException(msg); + } + } else { + theResource.setId((IIdType)null); } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java index 0087d56c442..08180502c07 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/RequestDetails.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.rest.method; +import static org.apache.commons.lang3.StringUtils.isBlank; + import java.io.IOException; import java.io.InputStream; import java.io.Reader; @@ -38,6 +40,7 @@ import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IRestfulResponse; import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; @@ -52,6 +55,7 @@ public abstract class RequestDetails { private String myOperation; private Map myParameters; private byte[] myRequestContents; + private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); private String myRequestPath; private RequestTypeEnum myRequestType; private String myResourceName; @@ -59,7 +63,6 @@ public abstract class RequestDetails { private IRestfulResponse myResponse; private RestOperationTypeEnum myRestOperationType; private String mySecondaryOperation; - private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback(); private Map> myUnqualifiedToQualifiedNames; private Map myUserData; protected abstract byte[] getByteStreamRequestContents(); @@ -76,6 +79,40 @@ public abstract class RequestDetails { return myCompleteUrl; } + /** + * Returns the conditional URL if this request has one, or null otherwise. For an + * update or delete method, this is the part of the URL after the ?. For a create, this + * is the value of the If-None-Exist header. + * + * @param theOperationType The operation type to find the conditional URL for + * @return Returns the conditional URL if this request has one, or null otherwise + */ + public String getConditionalUrl(RestOperationTypeEnum theOperationType) { + if (theOperationType == RestOperationTypeEnum.CREATE) { + String retVal = this.getHeader(Constants.HEADER_IF_NONE_EXIST); + if (isBlank(retVal)) { + return null; + } + if (retVal.startsWith(this.getFhirServerBase())) { + retVal = retVal.substring(this.getFhirServerBase().length()); + } + return retVal; + } else if (theOperationType != RestOperationTypeEnum.DELETE && theOperationType != RestOperationTypeEnum.UPDATE) { + return null; + } + + if (this.getId() != null && this.getId().hasIdPart()) { + return null; + } + + int questionMarkIndex = this.getCompleteUrl().indexOf('?'); + if (questionMarkIndex == -1) { + return null; + } + + return this.getResourceName() + this.getCompleteUrl().substring(questionMarkIndex); + } + /** * The fhir server base url, independant of the query being executed * @@ -138,6 +175,15 @@ public abstract class RequestDetails { */ public abstract Reader getReader() throws IOException; + /** + * Returns an invoker that can be called from user code to advise the server interceptors + * of any nested operations being invoked within operations. This invoker acts as a proxy for + * all interceptors + */ + public IRequestOperationCallback getRequestOperationCallback() { + return myRequestOperationCallback; + } + /** * The part of the request URL that comes after the server base. *

@@ -175,15 +221,6 @@ public abstract class RequestDetails { */ public abstract String getServerBaseForRequest(); - /** - * Returns an invoker that can be called from user code to advise the server interceptors - * of any nested operations being invoked within operations. This invoker acts as a proxy for - * all interceptors - */ - public IRequestOperationCallback getRequestOperationCallback() { - return myRequestOperationCallback; - } - public Map> getUnqualifiedToQualifiedNames() { return myUnqualifiedToQualifiedNames; } @@ -290,13 +327,20 @@ public abstract class RequestDetails { public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { myRestOperationType = theRestOperationType; } - + public void setSecondaryOperation(String theSecondaryOperation) { mySecondaryOperation = theSecondaryOperation; } - + private class RequestOperationCallback implements IRequestOperationCallback { + private List getInterceptors() { + if (getServer() == null) { + return Collections.emptyList(); + } + return getServer().getInterceptors(); + } + @Override public void resourceCreated(IBaseResource theResource) { for (IServerInterceptor next : getInterceptors()) { @@ -306,13 +350,6 @@ public abstract class RequestDetails { } } - private List getInterceptors() { - if (getServer() == null) { - return Collections.emptyList(); - } - return getServer().getInterceptors(); - } - @Override public void resourceDeleted(IBaseResource theResource) { for (IServerInterceptor next : getInterceptors()) { @@ -322,15 +359,6 @@ public abstract class RequestDetails { } } - @Override - public void resourceUpdated(IBaseResource theResource) { - for (IServerInterceptor next : getInterceptors()) { - if (next instanceof IServerOperationInterceptor) { - ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theResource); - } - } - } - @Override public void resourcesCreated(Collection theResource) { for (IBaseResource next : theResource) { @@ -352,6 +380,15 @@ public abstract class RequestDetails { } } + @Override + public void resourceUpdated(IBaseResource theResource) { + for (IServerInterceptor next : getInterceptors()) { + if (next instanceof IServerOperationInterceptor) { + ((IServerOperationInterceptor) next).resourceUpdated(RequestDetails.this, theResource); + } + } + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java index acef735e3e6..d30f57b4554 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/method/UpdateMethodBinding.java @@ -11,7 +11,7 @@ import static org.apache.commons.lang3.StringUtils.isBlank; * 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 + * 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, @@ -30,6 +30,7 @@ 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.FhirVersionEnum; import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.Update; @@ -62,7 +63,8 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe id.setValue(locationHeader); if (isNotBlank(id.getResourceType())) { if (!getResourceName().equals(id.getResourceType())) { - throw new InvalidRequestException("Attempting to update '" + getResourceName() + "' but content-location header specifies different resource type '" + id.getResourceType() + "' - header value: " + locationHeader); + throw new InvalidRequestException( + "Attempting to update '" + getResourceName() + "' but content-location header specifies different resource type '" + id.getResourceType() + "' - header value: " + locationHeader); } } } @@ -140,17 +142,20 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "noIdInUrlForUpdate"); throw new InvalidRequestException(msg); } - if (isBlank(theResourceId)) { -// String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "noIdInBodyForUpdate"); - ourLog.warn("No resource ID found in resource body for update"); - theResource.setId(theUrlId); - } else { - if (!theResourceId.equals(theUrlId)) { - String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "incorrectIdForUpdate", theResourceId, theUrlId); - throw new InvalidRequestException(msg); + if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { + if (isBlank(theResourceId)) { + ourLog.warn("No resource ID found in resource body for update"); + theResource.setId(theUrlId); + } else { + if (!theResourceId.equals(theUrlId)) { + String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "incorrectIdForUpdate", theResourceId, theUrlId); + throw new InvalidRequestException(msg); + } } } + } else { + theResource.setId((IIdType)null); } + } - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 89ab445fd65..37cc777853a 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -32,6 +32,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.Validate; 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.model.api.Bundle; @@ -78,8 +79,8 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer setDefaultPolicy(theDefaultPolicy); } - private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { - Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theOutputResource); + private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) { + Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource); if (decision.getDecision() == PolicyEnum.ALLOW) { return; @@ -89,13 +90,13 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } @Override - public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { + public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) { List rules = buildRuleList(theRequestDetails); 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, theOutputResource, this); + verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, this); if (verdict != null) { ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision()); break; @@ -126,7 +127,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } - private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation) { + private OperationExamineDirection determineOperationDirection(RestOperationTypeEnum theOperation, IBaseResource theRequestResource) { switch (theOperation) { case ADD_TAGS: case DELETE_TAGS: @@ -147,6 +148,13 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer case CREATE: case UPDATE: +// if (theRequestResource != null) { +// if (theRequestResource.getIdElement() != null) { +// if (theRequestResource.getIdElement().hasIdPart() == false) { +// return OperationExamineDirection.IN_UNCATEGORIZED; +// } +// } +// } return OperationExamineDirection.IN; case META: @@ -204,14 +212,26 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } private void handleUserOperation(RequestDetails theRequest, IBaseResource theResource, RestOperationTypeEnum operation) { - applyRulesAndFailIfDeny(operation, theRequest, theResource, null); + applyRulesAndFailIfDeny(operation, theRequest, theResource, theResource.getIdElement(), null); } @Override public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theProcessedRequest) { - switch (determineOperationDirection(theOperation)) { + IBaseResource inputResource = null; + IIdType inputResourceId = null; + + switch (determineOperationDirection(theOperation, theProcessedRequest.getResource())) { + case IN_UNCATEGORIZED: + inputResourceId = theProcessedRequest.getId(); + if (inputResourceId == null || inputResourceId.hasIdPart() == false) { + return; + } else { + break; + } case IN: case BOTH: + inputResource = theProcessedRequest.getResource(); + inputResourceId = theProcessedRequest.getId(); break; case NONE: case OUT: @@ -219,7 +239,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } RequestDetails requestDetails = theProcessedRequest.getRequestDetails(); - applyRulesAndFailIfDeny(theOperation, requestDetails, theProcessedRequest.getResource(), null); + applyRulesAndFailIfDeny(theOperation, requestDetails, inputResource, inputResourceId, null); } @Override @@ -236,7 +256,9 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer @Override public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { - switch (determineOperationDirection(theRequestDetails.getRestOperationType())) { + switch (determineOperationDirection(theRequestDetails.getRestOperationType(), null)) { + case IN_UNCATEGORIZED: + return true; case IN: case NONE: return true; @@ -246,8 +268,6 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } FhirContext fhirContext = theRequestDetails.getServer().getFhirContext(); - List rules = buildRuleList(theRequestDetails); - List resources = Collections.emptyList(); switch (theRequestDetails.getRestOperationType()) { @@ -272,7 +292,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } for (IBaseResource nextResponse : resources) { - applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, nextResponse); + applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse); } return true; @@ -334,9 +354,10 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer private enum OperationExamineDirection { IN, + IN_UNCATEGORIZED, NONE, OUT, - BOTH + BOTH, } public static class Verdict { diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java index b874b253d75..9898ad868c4 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/BaseRule.java @@ -35,7 +35,7 @@ abstract class BaseRule implements IAuthRule { return myName; } - public void setMode(PolicyEnum theRuleMode) { + void setMode(PolicyEnum theRuleMode) { myMode = theRuleMode; } @@ -43,7 +43,7 @@ abstract class BaseRule implements IAuthRule { return new Verdict(myMode, this); } - public PolicyEnum getMode() { + PolicyEnum getMode() { return myMode; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java index 2e49d2caadb..4750f92dc53 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; */ import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; @@ -37,6 +38,7 @@ public interface IAuthRule { * The request * @param theInputResource * The resource being input by the client, or null + * @param theInputResourceId TODO * @param theOutputResource * The resource being returned by the server, or null * @param theRuleApplier @@ -44,7 +46,7 @@ public interface IAuthRule { * nested objects in the request, such as nested requests in a transaction) * @return Returns a policy decision, or null if the rule does not apply */ - Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier); + Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier); /** * Returns a name for this rule, to be used in logs and error messages diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderAppliesTo.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderAppliesTo.java new file mode 100644 index 00000000000..d4e6d013d34 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderAppliesTo.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +public interface IAuthRuleBuilderAppliesTo { + + /** + * Rule applies to resources of the given type + */ + T resourcesOfType(Class theType); + + /** + * Rule applies to all resources + */ + T allResources(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java index 3e324f60895..4fe9b4352a0 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java @@ -22,11 +22,39 @@ package ca.uhn.fhir.rest.server.interceptor.auth; public interface IAuthRuleBuilderRule { + /** + * This rule applies to create operations with a conditional + * URL as a part of the request. Note that this rule will allow the conditional + * operation to proceed, but the server is expected to determine the actual target + * of the conditional request and send a subsequent event to the {@link AuthorizationInterceptor} + * in order to authorize the actual target. + *

+ * In other words, if the server is configured correctly, this chain will allow the + * client to perform a conditional update, but a different rule is required to actually + * authorize the target that the conditional update is determined to match. + *

+ */ + IAuthRuleBuilderRuleConditional createConditional(); + /** * This rule applies to the FHIR delete operation */ IAuthRuleBuilderRuleOp delete(); + /** + * This rule applies to create operations with a conditional + * URL as a part of the request. Note that this rule will allow the conditional + * operation to proceed, but the server is expected to determine the actual target + * of the conditional request and send a subsequent event to the {@link AuthorizationInterceptor} + * in order to authorize the actual target. + *

+ * In other words, if the server is configured correctly, this chain will allow the + * client to perform a conditional update, but a different rule is required to actually + * authorize the target that the conditional update is determined to match. + *

+ */ + IAuthRuleBuilderRuleConditional deleteConditional(); + /** * This rules applies to the metadata operation (retrieve the * server's conformance statement) @@ -36,6 +64,11 @@ public interface IAuthRuleBuilderRule { */ IAuthRuleBuilderRuleOpClassifierFinished metadata(); + /** + * This rule applies to a FHIR operation (e.g. $validate) + */ + IAuthRuleBuilderOperation operation(); + /** * This rule applies to any FHIR operation involving reading, including * read, vread, search, and @@ -49,15 +82,24 @@ public interface IAuthRuleBuilderRule { */ IAuthRuleBuilderRuleTransaction transaction(); + /** + * This rule applies to update operations with a conditional + * URL as a part of the request. Note that this rule will allow the conditional + * operation to proceed, but the server is expected to determine the actual target + * of the conditional request and send a subsequent event to the {@link AuthorizationInterceptor} + * in order to authorize the actual target. + *

+ * In other words, if the server is configured correctly, this chain will allow the + * client to perform a conditional update, but a different rule is required to actually + * authorize the target that the conditional update is determined to match. + *

+ */ + IAuthRuleBuilderRuleConditional updateConditional(); + /** * This rule applies to any FHIR operation involving writing, including * create, and update */ IAuthRuleBuilderRuleOp write(); - /** - * This rule applies to a FHIR operation (e.g. $validate) - */ - IAuthRuleBuilderOperation operation(); - } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditional.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditional.java new file mode 100644 index 00000000000..b8576fc34f2 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditional.java @@ -0,0 +1,5 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +public interface IAuthRuleBuilderRuleConditional extends IAuthRuleBuilderAppliesTo { + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditionalClassifier.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditionalClassifier.java new file mode 100644 index 00000000000..0693c0e8216 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleConditionalClassifier.java @@ -0,0 +1,27 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import java.util.List; + +/* + * #%L + * HAPI FHIR - Core Library + * %% + * Copyright (C) 2014 - 2016 University Health Network + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ + +public interface IAuthRuleBuilderRuleConditionalClassifier extends IAuthRuleFinished { + // nothing +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java index 9c9f05d1dc1..1b66894ca90 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOp.java @@ -1,37 +1,5 @@ package ca.uhn.fhir.rest.server.interceptor.auth; -/* - * #%L - * HAPI FHIR - Core Library - * %% - * Copyright (C) 2014 - 2016 University Health Network - * %% - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * #L% - */ - -import org.hl7.fhir.instance.model.api.IBaseResource; - -public interface IAuthRuleBuilderRuleOp { - - /** - * Rule applies to resources of the given type - */ - IAuthRuleBuilderRuleOpClassifier resourcesOfType(Class theType); - - /** - * Rule applies to all resources - */ - IAuthRuleBuilderRuleOpClassifier allResources(); - +public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo { + // nothing } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifierFinished.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifierFinished.java index 5cc3844bc73..b4231e1325d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifierFinished.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRuleOpClassifierFinished.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.rest.server.interceptor.auth; -import java.util.List; - /* * #%L * HAPI FHIR - Core Library @@ -22,15 +20,6 @@ import java.util.List; * #L% */ -public interface IAuthRuleBuilderRuleOpClassifierFinished { - /** - * Start another rule - */ - IAuthRuleBuilder andThen(); - - /** - * Build the rule list - */ - List build(); - +public interface IAuthRuleBuilderRuleOpClassifierFinished extends IAuthRuleFinished { + // nothing } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java new file mode 100644 index 00000000000..820d38d14a4 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleFinished.java @@ -0,0 +1,17 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import java.util.List; + +public interface IAuthRuleFinished { + + /** + * Start another rule + */ + IAuthRuleBuilder andThen(); + + /** + * Build the rule list + */ + List build(); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java index fecd7326ca3..dd7898c02a8 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java @@ -21,6 +21,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; */ import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; @@ -28,6 +29,6 @@ import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict public interface IRuleApplier { - Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource); + Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource); } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java index 1e97a1b0c6d..e2cc9baa44f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/OperationRule.java @@ -54,7 +54,7 @@ class OperationRule extends BaseRule implements IAuthRule { } @Override - public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier) { + public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier) { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); boolean applies = false; @@ -67,7 +67,7 @@ class OperationRule extends BaseRule implements IAuthRule { case EXTENDED_OPERATION_TYPE: if (myAppliesToTypes != null) { for (Class next : myAppliesToTypes) { - String resName = ctx.getResourceDefinition(theRequestDetails.getResourceName()).getName(); + String resName = ctx.getResourceDefinition(next).getName(); if (resName.equals(theRequestDetails.getResourceName())) { applies = true; break; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 7ffeeaa92c1..45a65ef4af5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -1,7 +1,5 @@ package ca.uhn.fhir.rest.server.interceptor.auth; -import java.util.ArrayList; - /* * #%L * HAPI FHIR - Core Library @@ -12,7 +10,7 @@ import java.util.ArrayList; * 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 + * 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, @@ -21,17 +19,14 @@ import java.util.ArrayList; * limitations under the License. * #L% */ - -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import org.apache.commons.lang3.Validate; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; + public class RuleBuilder implements IAuthRuleBuilder { private ArrayList myRules; @@ -39,7 +34,7 @@ public class RuleBuilder implements IAuthRuleBuilder { public RuleBuilder() { myRules = new ArrayList(); } - + @Override public IAuthRuleBuilderRule allow() { return allow(null); @@ -57,7 +52,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinished allowAll(String theRuleName) { - myRules.add(new Rule(theRuleName).setOp(RuleOpEnum.ALLOW_ALL)); + myRules.add(new RuleImplOp(theRuleName).setOp(RuleOpEnum.ALLOW_ALL)); return new RuleBuilderFinished(); } @@ -83,21 +78,30 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRuleOpClassifierFinished denyAll(String theRuleName) { - myRules.add(new Rule(theRuleName).setOp(RuleOpEnum.DENY_ALL)); + myRules.add(new RuleImplOp(theRuleName).setOp(RuleOpEnum.DENY_ALL)); return new RuleBuilderFinished(); } - private final class RuleBuilderFinished implements IAuthRuleBuilderRuleOpClassifierFinished { + private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished { @Override public IAuthRuleBuilder andThen() { + doBuildRule(); return RuleBuilder.this; } @Override public List build() { + doBuildRule(); return myRules; } + + /** + * Subclasses may override + */ + protected void doBuildRule() { + // nothing + } } private class RuleBuilderRule implements IAuthRuleBuilderRule { @@ -111,39 +115,100 @@ public class RuleBuilder implements IAuthRuleBuilder { myRuleName = theRuleName; } + @Override + public IAuthRuleBuilderRuleConditional createConditional() { + return new RuleBuilderRuleConditional(RestOperationTypeEnum.CREATE); + } + @Override public IAuthRuleBuilderRuleOp delete() { myRuleOp = RuleOpEnum.DELETE; return new RuleBuilderRuleOp(); } - + + @Override + public IAuthRuleBuilderRuleConditional deleteConditional() { + return new RuleBuilderRuleConditional(RestOperationTypeEnum.DELETE); + } + @Override public RuleBuilderFinished metadata() { - Rule rule = new Rule(myRuleName); + RuleImplOp rule = new RuleImplOp(myRuleName); rule.setOp(RuleOpEnum.METADATA); rule.setMode(myRuleMode); myRules.add(rule); return new RuleBuilderFinished(); } + @Override + public IAuthRuleBuilderOperation operation() { + return new RuleBuilderRuleOperation(); + } + @Override public IAuthRuleBuilderRuleOp read() { myRuleOp = RuleOpEnum.READ; return new RuleBuilderRuleOp(); } - + @Override public IAuthRuleBuilderRuleTransaction transaction() { myRuleOp = RuleOpEnum.TRANSACTION; return new RuleBuilderRuleTransaction(); } - + + @Override + public IAuthRuleBuilderRuleConditional updateConditional() { + return new RuleBuilderRuleConditional(RestOperationTypeEnum.UPDATE); + } + @Override public IAuthRuleBuilderRuleOp write() { myRuleOp = RuleOpEnum.WRITE; return new RuleBuilderRuleOp(); } + private class RuleBuilderRuleConditional implements IAuthRuleBuilderRuleConditional { + + private AppliesTypeEnum myAppliesTo; + + private Set myAppliesToTypes; + private RestOperationTypeEnum myOperationType; + + public RuleBuilderRuleConditional(RestOperationTypeEnum theOperationType) { + myOperationType = theOperationType; + } + + @Override + public IAuthRuleBuilderRuleConditionalClassifier allResources() { + myAppliesTo = AppliesTypeEnum.ALL_RESOURCES; + return new RuleBuilderRuleConditionalClassifier(); + } + + @Override + public IAuthRuleBuilderRuleConditionalClassifier resourcesOfType(Class theType) { + Validate.notNull(theType, "theType must not be null"); + myAppliesTo = AppliesTypeEnum.TYPES; + myAppliesToTypes = Collections.singleton(theType); + return new RuleBuilderRuleConditionalClassifier(); + } + + public class RuleBuilderRuleConditionalClassifier extends RuleBuilderFinished implements IAuthRuleBuilderRuleConditionalClassifier { + + @Override + protected void doBuildRule() { + RuleImplConditional rule = new RuleImplConditional(myRuleName); + rule.setMode(myRuleMode); + rule.setOperationType(myOperationType); + rule.setAppliesTo(myAppliesTo); + rule.setAppliesToTypes(myAppliesToTypes); + myRules.add(rule); + + } + } + + } + private class RuleBuilderRuleOp implements IAuthRuleBuilderRuleOp { private AppliesTypeEnum myAppliesTo; @@ -170,8 +235,8 @@ public class RuleBuilder implements IAuthRuleBuilder { private Collection myInCompartmentOwners; private IAuthRuleBuilderRuleOpClassifierFinished finished() { - - Rule rule = new Rule(myRuleName); + + RuleImplOp rule = new RuleImplOp(myRuleName); rule.setMode(myRuleMode); rule.setOp(myRuleOp); rule.setAppliesTo(myAppliesTo); @@ -180,7 +245,7 @@ public class RuleBuilder implements IAuthRuleBuilder { rule.setClassifierCompartmentName(myInCompartmentName); rule.setClassifierCompartmentOwners(myInCompartmentOwners); myRules.add(rule); - + return new RuleBuilderFinished(); } @@ -222,29 +287,18 @@ public class RuleBuilder implements IAuthRuleBuilder { } - private class RuleBuilderRuleTransaction implements IAuthRuleBuilderRuleTransaction { + private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation { @Override - public IAuthRuleBuilderRuleTransactionOp withAnyOperation() { - return new RuleBuilderRuleTransactionOp(); + public IAuthRuleBuilderOperationNamed named(String theOperationName) { + Validate.notBlank(theOperationName, "theOperationName must not be null or empty"); + return new RuleBuilderRuleOperationNamed(theOperationName); } - private class RuleBuilderRuleTransactionOp implements IAuthRuleBuilderRuleTransactionOp { - @Override - public IAuthRuleBuilderRuleOpClassifierFinished andApplyNormalRules() { - Rule rule = new Rule(myRuleName); - rule.setMode(myRuleMode); - rule.setOp(myRuleOp); - rule.setTransactionAppliesToOp(TransactionAppliesToEnum.ANY_OPERATION); - myRules.add(rule); - return new RuleBuilderFinished(); - } - + @Override + public IAuthRuleBuilderOperationNamed withAnyName() { + return new RuleBuilderRuleOperationNamed(null); } - - } - - private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation { private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed { @@ -258,14 +312,6 @@ public class RuleBuilder implements IAuthRuleBuilder { } } - @Override - public IAuthRuleBuilderRuleOpClassifierFinished onServer() { - OperationRule rule = createRule(); - rule.appliesToServer(); - myRules.add(rule); - return new RuleBuilderFinished(); - } - private OperationRule createRule() { OperationRule rule = new OperationRule(myRuleName); rule.setOperationName(myOperationName); @@ -273,24 +319,12 @@ public class RuleBuilder implements IAuthRuleBuilder { return rule; } - @Override - public IAuthRuleBuilderRuleOpClassifierFinished onType(Class theType) { - Validate.notNull(theType, "theType must not be null"); - - OperationRule rule = createRule(); - HashSet> appliesToTypes = new HashSet>(); - appliesToTypes.add(theType); - rule.appliesToTypes(appliesToTypes); - myRules.add(rule); - return new RuleBuilderFinished(); - } - @Override public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) { Validate.notNull(theInstanceId, "theInstanceId must not be null"); Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type"); Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part"); - + OperationRule rule = createRule(); ArrayList ids = new ArrayList(); ids.add(theInstanceId); @@ -298,26 +332,54 @@ public class RuleBuilder implements IAuthRuleBuilder { myRules.add(rule); return new RuleBuilderFinished(); } - - } - - @Override - public IAuthRuleBuilderOperationNamed named(String theOperationName) { - Validate.notBlank(theOperationName, "theOperationName must not be null or empty"); - return new RuleBuilderRuleOperationNamed(theOperationName); + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished onServer() { + OperationRule rule = createRule(); + rule.appliesToServer(); + myRules.add(rule); + return new RuleBuilderFinished(); + } + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished onType(Class theType) { + Validate.notNull(theType, "theType must not be null"); + + OperationRule rule = createRule(); + HashSet> appliesToTypes = new HashSet>(); + appliesToTypes.add(theType); + rule.appliesToTypes(appliesToTypes); + myRules.add(rule); + return new RuleBuilderFinished(); + } + } - @Override - public IAuthRuleBuilderOperationNamed withAnyName() { - return new RuleBuilderRuleOperationNamed(null); - } - } - @Override - public IAuthRuleBuilderOperation operation() { - return new RuleBuilderRuleOperation(); + private class RuleBuilderRuleTransaction implements IAuthRuleBuilderRuleTransaction { + + @Override + public IAuthRuleBuilderRuleTransactionOp withAnyOperation() { + return new RuleBuilderRuleTransactionOp(); + } + + private class RuleBuilderRuleTransactionOp implements IAuthRuleBuilderRuleTransactionOp { + + @Override + public IAuthRuleBuilderRuleOpClassifierFinished andApplyNormalRules() { + RuleImplOp rule = new RuleImplOp(myRuleName); + rule.setMode(myRuleMode); + rule.setOp(myRuleOp); + rule.setTransactionAppliesToOp(TransactionAppliesToEnum.ANY_OPERATION); + myRules.add(rule); + return new RuleBuilderFinished(); + } + + } + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java new file mode 100644 index 00000000000..349d4ef75a7 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplConditional.java @@ -0,0 +1,63 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import java.util.Set; + +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; + +import ca.uhn.fhir.rest.api.RestOperationTypeEnum; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict; + +public class RuleImplConditional extends BaseRule implements IAuthRule { + + private AppliesTypeEnum myAppliesTo; + private Set myAppliesToTypes; + private RestOperationTypeEnum myOperationType; + + public RuleImplConditional(String theRuleName) { + super(theRuleName); + } + + @Override + public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, + IRuleApplier theRuleApplier) { + + if (theInputResourceId != null) { + return null; + } + + if (theOperation == RestOperationTypeEnum.UPDATE) { + switch (myAppliesTo) { + case ALL_RESOURCES: + break; + case TYPES: + if (theInputResource == null || !myAppliesToTypes.contains(theInputResource.getClass())) { + return null; + } + break; + } + + if (theRequestDetails.getConditionalUrl(myOperationType) == null) { + return null; + } + + return newVerdict(); + } + + return null; + } + + void setAppliesTo(AppliesTypeEnum theAppliesTo) { + myAppliesTo = theAppliesTo; + } + + void setAppliesToTypes(Set theAppliesToTypes) { + myAppliesToTypes = theAppliesToTypes; + } + + void setOperationType(RestOperationTypeEnum theOperationType) { + myOperationType = theOperationType; + } + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java similarity index 81% rename from hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java rename to hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java index ffd6fad82f6..05af00d3d98 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleImplOp.java @@ -10,7 +10,7 @@ package ca.uhn.fhir.rest.server.interceptor.auth; * 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 + * 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, @@ -39,7 +39,7 @@ import ca.uhn.fhir.util.BundleUtil; import ca.uhn.fhir.util.BundleUtil.BundleEntryParts; import ca.uhn.fhir.util.FhirTerser; -class Rule extends BaseRule implements IAuthRule { +class RuleImplOp extends BaseRule implements IAuthRule { private AppliesTypeEnum myAppliesTo; private Set myAppliesToTypes; @@ -49,34 +49,37 @@ class Rule extends BaseRule implements IAuthRule { private RuleOpEnum myOp; private TransactionAppliesToEnum myTransactionAppliesToOp; - public Rule(String theRuleName) { + public RuleImplOp(String theRuleName) { super(theRuleName); } @Override - public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier) { + public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, + IRuleApplier theRuleApplier) { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); - IBaseResource appliesTo; + IBaseResource appliesToResource; + IIdType appliesToResourceId = null; switch (myOp) { case READ: if (theOutputResource == null) { return null; } - appliesTo = theOutputResource; + appliesToResource = theOutputResource; break; case WRITE: - if (theInputResource == null) { + if (theInputResource == null && theInputResourceId == null) { return null; } - appliesTo = theInputResource; + appliesToResource = theInputResource; + appliesToResourceId = theInputResourceId; break; case DELETE: if (theOperation == RestOperationTypeEnum.DELETE) { if (theInputResource == null) { return newVerdict(); } else { - appliesTo = theInputResource; + appliesToResource = theInputResource; } } else { return null; @@ -115,7 +118,7 @@ class Rule extends BaseRule implements IAuthRule { throw new InvalidRequestException("Can not handle transaction with nested resource of type " + resourceDef.getName()); } - Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(operation, theRequestDetails, inputResource, null); + Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(operation, theRequestDetails, inputResource, null, null); if (newVerdict == null) { continue; } else if (verdict == null) { @@ -133,7 +136,7 @@ class Rule extends BaseRule implements IAuthRule { if (nextPart.getResource() == null) { continue; } - Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(RestOperationTypeEnum.READ, theRequestDetails, null, nextPart.getResource()); + Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(RestOperationTypeEnum.READ, theRequestDetails, null, null, nextPart.getResource()); if (newVerdict == null) { continue; } else if (verdict == null) { @@ -165,8 +168,16 @@ class Rule extends BaseRule implements IAuthRule { case ALL_RESOURCES: break; case TYPES: - if (myAppliesToTypes.contains(appliesTo.getClass()) == false) { - return null; + if (appliesToResource != null) { + if (myAppliesToTypes.contains(appliesToResource.getClass()) == false) { + return null; + } + } + if (appliesToResourceId != null) { + Class type = theRequestDetails.getServer().getFhirContext().getResourceDefinition(appliesToResourceId.getResourceType()).getImplementingClass(); + if (myAppliesToTypes.contains(type) == false) { + return null; + } } break; default: @@ -180,9 +191,17 @@ class Rule extends BaseRule implements IAuthRule { FhirTerser t = ctx.newTerser(); boolean foundMatch = false; for (IIdType next : myClassifierCompartmentOwners) { - if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, appliesTo, next)) { - foundMatch = true; - break; + 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 (!foundMatch) { @@ -196,7 +215,6 @@ class Rule extends BaseRule implements IAuthRule { return newVerdict(); } - private boolean requestAppliesToTransaction(FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) { if (!"Bundle".equals(theContext.getResourceDefinition(theInputResource).getName())) { return false; @@ -238,8 +256,7 @@ class Rule extends BaseRule implements IAuthRule { myClassifierType = theClassifierType; } - - public Rule setOp(RuleOpEnum theRuleOp) { + public RuleImplOp setOp(RuleOpEnum theRuleOp) { myOp = theRuleOp; return this; } diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderDstu3Test.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderDstu3Test.java index b58d935daaa..efb8508273c 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderDstu3Test.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderDstu3Test.java @@ -386,9 +386,9 @@ public class AbstractJaxRsResourceProviderDstu3Test { when(mock.update(idCaptor.capture(), patientCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome()); client.update().resource(createPatient(1)).conditional().where(Patient.IDENTIFIER.exactly().identifier("2")).execute(); - compareResultId(1, patientCaptor.getValue()); + assertEquals(null, patientCaptor.getValue().getIdElement().getIdPart()); + assertEquals(null, patientCaptor.getValue().getIdElement().getVersionIdPart()); assertEquals("Patient?identifier=2&_format=json", conditionalCaptor.getValue()); - compareResultId(1, patientCaptor.getValue()); } @SuppressWarnings("unchecked") diff --git a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java index b7d35635847..837f2509963 100644 --- a/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java +++ b/hapi-fhir-jaxrsserver-base/src/test/java/ca/uhn/fhir/jaxrs/server/AbstractJaxRsResourceProviderTest.java @@ -70,7 +70,6 @@ import ca.uhn.fhir.util.TestUtil; @FixMethodOrder(MethodSorters.NAME_ASCENDING) public class AbstractJaxRsResourceProviderTest { - private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AbstractJaxRsResourceProviderTest.class); private static IGenericClient client; @@ -372,9 +371,8 @@ public class AbstractJaxRsResourceProviderTest { when(mock.update(idCaptor.capture(), patientCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome()); client.update().resource(createPatient(1)).conditional().where(Patient.IDENTIFIER.exactly().identifier("2")).execute(); - assertEquals("1", patientCaptor.getValue().getId().getIdPart()); + assertEquals(null, patientCaptor.getValue().getId().getIdPart()); assertEquals("Patient?identifier=2&_format=json", conditionalCaptor.getValue()); - compareResultId(1, patientCaptor.getValue()); } @SuppressWarnings("unchecked") diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java index 6467060e0ce..e6c529ae3e1 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirResourceDao.java @@ -166,9 +166,12 @@ public abstract class BaseHapiFhirResourceDao extends B String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart()); throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing")); } - } else { + } else if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart()); throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing")); + } else { + // As of DSTU3, ID and version in the body should be ignored for a create/update + theResource.setId(""); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java index 105c3aa2ca5..bdd01237434 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3Test.java @@ -634,17 +634,13 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { } @Test - public void testCreateTextIdFails() { + public void testCreateTextIdDoesntFail() { Patient p = new Patient(); p.addIdentifier().setSystem("urn:system").setValue("testCreateTextIdFails"); p.addName().addFamily("Hello"); p.setId("Patient/ABC"); - try { - myPatientDao.create(p, mySrd); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), containsString("Can not create resource with ID[ABC], ID must not be supplied")); - } + String id = myPatientDao.create(p, mySrd).getId().getIdPart(); + assertNotEquals("ABC", id); } @Test @@ -678,20 +674,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test { assertThat(toUnqualifiedVersionlessIdValues(myCarePlanDao.search(params)), empty()); } - @Test - public void testCreateWithIdFails() { - Patient p = new Patient(); - p.addIdentifier().setSystem("urn:system").setValue("testCreateNumericIdFails"); - p.addName().addFamily("Hello"); - p.setId("Patient/abc"); - try { - myPatientDao.create(p, mySrd); - fail(); - } catch (InvalidRequestException e) { - assertThat(e.getMessage(), containsString("Can not create resource with ID[abc], ID must not be supplied")); - } - } - @Test public void testCreateBundleAllowsDocumentAndCollection() { String methodName = "testCreateBundleAllowsDocumentAndCollection"; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java index 6e64abe9e4a..1800a56defc 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java @@ -70,6 +70,7 @@ public class AuthorizationInterceptorResourceProviderDstu3Test extends BaseResou //@formatter:off return new RuleBuilder() .allow("Rule 2").write().allResources().inCompartment("Patient", new IdDt("Patient/" + output1.getId().getIdPart())).andThen() + .allow().updateConditional().allResources() .build(); //@formatter:on } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index add32791221..bfa7ae574e2 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -570,6 +570,107 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { } + @Test + public void testIdAndVersionInBodyForCreate() throws IOException { + String methodName = "testIdAndVersionInBodyForCreate"; + + Patient pt = new Patient(); + pt.setId("Patient/AAA/_history/4"); + pt.addName().addFamily(methodName); + String resource = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(pt); + + ourLog.info("Input: {}", resource); + + HttpPost post = new HttpPost(ourServerBase + "/Patient"); + post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse response = ourHttpClient.execute(post); + IdType id; + try { + String respString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", respString); + assertEquals(201, response.getStatusLine().getStatusCode()); + String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(); + assertThat(newIdString, startsWith(ourServerBase + "/Patient/")); + id = new IdType(newIdString); + } finally { + response.close(); + } + + assertEquals("1", id.getVersionIdPart()); + assertNotEquals("AAA", id.getIdPart()); + + HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart()); + response = ourHttpClient.execute(get); + try { + assertEquals(200, response.getStatusLine().getStatusCode()); + String respString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", respString); + assertThat(respString, containsString("")); + assertThat(respString, containsString("")); + } finally { + response.close(); + } + } + + @Test + public void testIdAndVersionInBodyForUpdate() throws IOException { + String methodName = "testIdAndVersionInBodyForUpdate"; + + Patient pt = new Patient(); + pt.setId("Patient/AAA/_history/4"); + pt.addName().addFamily(methodName); + String resource = myFhirCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(pt); + + ourLog.info("Input: {}", resource); + + HttpPost post = new HttpPost(ourServerBase + "/Patient"); + post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse response = ourHttpClient.execute(post); + IdType id; + try { + String respString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", respString); + assertEquals(201, response.getStatusLine().getStatusCode()); + String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(); + assertThat(newIdString, startsWith(ourServerBase + "/Patient/")); + id = new IdType(newIdString); + } finally { + response.close(); + } + + assertEquals("1", id.getVersionIdPart()); + assertNotEquals("AAA", id.getIdPart()); + + HttpPut put = new HttpPut(ourServerBase + "/Patient/" + id.getIdPart() + "/_history/1"); + put.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + response = ourHttpClient.execute(put); + try { + String respString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", respString); + assertEquals(200, response.getStatusLine().getStatusCode()); + String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(); + assertThat(newIdString, startsWith(ourServerBase + "/Patient/")); + id = new IdType(newIdString); + } finally { + response.close(); + } + + assertEquals("2", id.getVersionIdPart()); + assertNotEquals("AAA", id.getIdPart()); + + HttpGet get = new HttpGet(ourServerBase + "/Patient/" + id.getIdPart()); + response = ourHttpClient.execute(get); + try { + assertEquals(200, response.getStatusLine().getStatusCode()); + String respString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); + ourLog.info("Response: {}", respString); + assertThat(respString, containsString("")); + assertThat(respString, containsString("")); + } finally { + response.close(); + } + } + @Test public void testCreateResourceConditionalComplex() throws IOException { Patient pt = new Patient(); @@ -2493,9 +2594,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { put.addHeader("Accept", Constants.CT_FHIR_JSON); CloseableHttpResponse response = ourHttpClient.execute(put); try { - assertEquals(400, response.getStatusLine().getStatusCode()); - OperationOutcome oo = myFhirCtx.newJsonParser().parseResource(OperationOutcome.class, new InputStreamReader(response.getEntity().getContent())); - assertEquals("Can not update resource, resource body must contain an ID element which matches the request URL for update (PUT) operation - Resource body ID of \"FOO\" does not match URL ID of \""+p1id.getIdPart()+"\"", oo.getIssue().get(0).getDiagnostics()); + assertEquals(200, response.getStatusLine().getStatusCode()); } finally { response.close(); } @@ -2722,16 +2821,15 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { pt.addName().addFamily(methodName); String resource = myFhirCtx.newXmlParser().encodeResourceToString(pt); - HttpPut post = new HttpPut(ourServerBase + "/Patient/2"); + HttpPut post = new HttpPut(ourServerBase + "/Patient/A2"); post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); CloseableHttpResponse response = ourHttpClient.execute(post); try { String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info(responseString); - assertEquals(400, response.getStatusLine().getStatusCode()); - OperationOutcome oo = myFhirCtx.newXmlParser().parseResource(OperationOutcome.class, responseString); - assertThat(oo.getIssue().get(0).getDiagnostics(), containsString( - "Can not update resource, resource body must contain an ID element which matches the request URL for update (PUT) operation - Resource body ID of \"333\" does not match URL ID of \"2\"")); + assertEquals(201, response.getStatusLine().getStatusCode()); + assertThat(responseString, containsString("/A2/")); + assertThat(responseString, not(containsString("333"))); } finally { response.close(); } diff --git a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java index 9c257ddc131..0aaab012721 100644 --- a/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java +++ b/hapi-fhir-structures-dstu2/src/main/java/ca/uhn/fhir/model/dstu2/resource/BaseResource.java @@ -307,6 +307,8 @@ public abstract class BaseResource extends BaseElement implements IResource { myId = (IdDt) theId; } else if (theId != null) { myId = new IdDt(theId.getValue()); + } else { + myId = null; } return this; } diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java index 36534b0623f..04d1b5557d2 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java @@ -70,6 +70,7 @@ import ca.uhn.fhir.rest.server.IResourceProvider; import ca.uhn.fhir.rest.server.RestfulServer; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor.ActionRequestDetails; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; import ca.uhn.fhir.util.PortUtil; import ca.uhn.fhir.util.TestUtil; @@ -77,6 +78,7 @@ public class AuthorizationInterceptorDstu2Test { private static final String ERR403 = "{\"resourceType\":\"OperationOutcome\",\"issue\":[{\"severity\":\"error\",\"code\":\"processing\",\"diagnostics\":\"Access denied by default policy (no applicable rules)\"}]}"; private static CloseableHttpClient ourClient; + private static String ourConditionalCreateId; private static FhirContext ourCtx = FhirContext.forDstu2(); private static boolean ourHitMethod; private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AuthorizationInterceptorDstu2Test.class); @@ -85,6 +87,7 @@ public class AuthorizationInterceptorDstu2Test { private static Server ourServer; private static RestfulServer ourServlet; + @Before public void before() { ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.NEVER); @@ -93,9 +96,9 @@ public class AuthorizationInterceptorDstu2Test { } ourReturn = null; ourHitMethod = false; + ourConditionalCreateId = "1123"; } - private HttpEntity createFhirResourceEntity(IBaseResource theResource) { String out = ourCtx.newJsonParser().encodeResourceToString(theResource); return new StringEntity(out, ContentType.create(Constants.CT_FHIR_JSON, "UTF-8")); @@ -175,286 +178,6 @@ public class AuthorizationInterceptorDstu2Test { } - @Test - public void testOperationServerLevel() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - //@formatter:off - return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onServer().andThen() - .build(); - //@formatter:on - } - }); - - HttpGet httpGet; - HttpResponse status; - String response; - - // Server - ourHitMethod = false; - ourReturn = Arrays.asList(createObservation(10, "Patient/2")); - httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); - status = ourClient.execute(httpGet); - extractResponseAndClose(status); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - // Type - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - // Instance - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - } - - @Test - public void testOperationInstanceLevel() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - //@formatter:off - return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andThen() - .build(); - //@formatter:on - } - }); - - HttpGet httpGet; - HttpResponse status; - String response; - - // Server - ourHitMethod = false; - ourReturn = Arrays.asList(createObservation(10, "Patient/2")); - httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - // Type - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - // Instance - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - // Wrong instance - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - } - - @Test - public void testOperationAnyName() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - //@formatter:off - return new RuleBuilder() - .allow("RULE 1").operation().withAnyName().onServer().andThen() - .build(); - //@formatter:on - } - }); - - HttpGet httpGet; - HttpResponse status; - String response; - - // Server - ourHitMethod = false; - ourReturn = Arrays.asList(createObservation(10, "Patient/2")); - httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - } - - @Test - public void testOperationTypeLevel() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - //@formatter:off - return new RuleBuilder() - .allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() - .build(); - //@formatter:on - } - }); - - HttpGet httpGet; - HttpResponse status; - String response; - - // Server - ourHitMethod = false; - ourReturn = Arrays.asList(createObservation(10, "Patient/2")); - httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - // Type - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - // Wrong type - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/1/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - // Wrong name - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName2"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - - // Instance - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by default policy")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - } - - @Test - public void testDenyAll() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - //@formatter:off - return new RuleBuilder() - .allow().read().resourcesOfType(Patient.class).withAnyId().andThen() - .denyAll("Default Rule") - .build(); - //@formatter:on - } - }); - - HttpGet httpGet; - HttpResponse status; - String response; - - ourHitMethod = false; - ourReturn = Arrays.asList(createPatient(2)); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); - status = ourClient.execute(httpGet); - extractResponseAndClose(status); - assertEquals(200, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - ourHitMethod = false; - ourReturn = Arrays.asList(createObservation(10, "Patient/2")); - httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by rule: Default Rule")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); - - ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$validate"); - status = ourClient.execute(httpGet); - response = extractResponseAndClose(status); - ourLog.info(response); - assertThat(response, containsString("Access denied by rule: Default Rule")); - assertEquals(403, status.getStatusLine().getStatusCode()); - assertFalse(ourHitMethod); - } - - @Test - public void testMetadataAllow() throws Exception { - ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { - @Override - public List buildRuleList(RequestDetails theRequestDetails) { - //@formatter:off - return new RuleBuilder() - .allow("Rule 1").metadata() - .build(); - //@formatter:on - } - }); - - HttpGet httpGet; - HttpResponse status; - String response; - - ourReturn = Arrays.asList(createPatient(2)); - ourHitMethod = false; - httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata"); - status = ourClient.execute(httpGet); - extractResponseAndClose(status); - assertEquals(200, status.getStatusLine().getStatusCode()); - } - @Test public void testBatchWhenOnlyTransactionAllowed() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -564,6 +287,77 @@ public class AuthorizationInterceptorDstu2Test { assertEquals(ERR403, response); } + @Test + public void testDenyAll() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow().read().resourcesOfType(Patient.class).withAnyId().andThen() + .denyAll("Default Rule") + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/10"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by rule: Default Rule")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$validate"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by rule: Default Rule")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + + @Test + public void testMetadataAllow() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").metadata() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + ourReturn = Arrays.asList(createPatient(2)); + ourHitMethod = false; + httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + } + @Test public void testMetadataDeny() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.ALLOW) { @@ -588,7 +382,216 @@ public class AuthorizationInterceptorDstu2Test { extractResponseAndClose(status); assertEquals(403, status.getStatusLine().getStatusCode()); } + + @Test + public void testOperationAnyName() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().withAnyName().onServer().andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + @Test + public void testOperationInstanceLevel() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onInstance(new IdDt("http://example.com/Patient/1/_history/2")).andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Wrong instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/2/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + } + + @Test + public void testOperationServerLevel() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onServer().andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + @Test + public void testOperationTypeLevel() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("RULE 1").operation().named("opName").onType(Patient.class).andThen() + .build(); + //@formatter:on + } + }); + + HttpGet httpGet; + HttpResponse status; + String response; + + // Server + ourHitMethod = false; + ourReturn = Arrays.asList(createObservation(10, "Patient/2")); + httpGet = new HttpGet("http://localhost:" + ourPort + "/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + // Wrong type + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Wrong name + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName2"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + + // Instance + ourHitMethod = false; + ourReturn = Arrays.asList(createPatient(2)); + httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/1/$opName"); + status = ourClient.execute(httpGet); + response = extractResponseAndClose(status); + ourLog.info(response); + assertThat(response, containsString("Access denied by default policy")); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); + } + @Test public void testReadByAnyId() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -689,6 +692,7 @@ public class AuthorizationInterceptorDstu2Test { } + @Test public void testReadByCompartmentWrong() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -749,7 +753,6 @@ public class AuthorizationInterceptorDstu2Test { } - @Test public void testTransactionWriteGood() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @@ -803,14 +806,15 @@ public class AuthorizationInterceptorDstu2Test { HttpEntityEnclosingRequestBase httpPost; HttpResponse status; + String response; ourHitMethod = false; httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); httpPost.setEntity(createFhirResourceEntity(createPatient(null))); status = ourClient.execute(httpPost); - String response = extractResponseAndClose(status); - assertEquals(ERR403, response); + response = extractResponseAndClose(status); assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); assertFalse(ourHitMethod); // Conditional @@ -820,8 +824,8 @@ public class AuthorizationInterceptorDstu2Test { httpPost.setEntity(createFhirResourceEntity(createPatient(null))); status = ourClient.execute(httpPost); response = extractResponseAndClose(status); - assertEquals(ERR403, response); assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); assertFalse(ourHitMethod); ourHitMethod = false; @@ -829,8 +833,8 @@ public class AuthorizationInterceptorDstu2Test { httpPost.setEntity(createFhirResourceEntity(createObservation(null, "Patient/2"))); status = ourClient.execute(httpPost); response = extractResponseAndClose(status); - assertEquals(ERR403, response); assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); assertFalse(ourHitMethod); ourHitMethod = false; @@ -891,13 +895,14 @@ public class AuthorizationInterceptorDstu2Test { }); HttpEntityEnclosingRequestBase httpPost; + String response; HttpResponse status; ourHitMethod = false; httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/2"); httpPost.setEntity(createFhirResourceEntity(createPatient(2))); status = ourClient.execute(httpPost); - String response = extractResponseAndClose(status); + response = extractResponseAndClose(status); assertEquals(ERR403, response); assertEquals(403, status.getStatusLine().getStatusCode()); assertFalse(ourHitMethod); @@ -907,7 +912,7 @@ public class AuthorizationInterceptorDstu2Test { httpPost.setEntity(createFhirResourceEntity(createPatient(1))); status = ourClient.execute(httpPost); extractResponseAndClose(status); - assertEquals(201, status.getStatusLine().getStatusCode()); + assertEquals(200, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); // Conditional @@ -918,7 +923,16 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); assertEquals(ERR403, response); assertEquals(403, status.getStatusLine().getStatusCode()); - assertTrue(ourHitMethod); + assertFalse(ourHitMethod); + + ourHitMethod = false; + httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createPatient(99))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(ERR403, response); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertFalse(ourHitMethod); ourHitMethod = false; httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation/10"); @@ -937,10 +951,182 @@ public class AuthorizationInterceptorDstu2Test { assertEquals(403, status.getStatusLine().getStatusCode()); assertFalse(ourHitMethod); } - + @Test + public void testWriteByCompartmentUpdateConditionalResolvesToInvalid() throws Exception { + ourConditionalCreateId = "1123"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").write().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").write().resourcesOfType(Observation.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 3").updateConditional().resourcesOfType(Patient.class) + .build(); + //@formatter:on + } + }); + + HttpEntityEnclosingRequestBase httpPost; + HttpResponse status; + String response; + + ourHitMethod = false; + httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createPatient(null))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertTrue(ourHitMethod); + + } + + + @Test + public void testWriteByCompartmentUpdateConditionalResolvesToValid() throws Exception { + ourConditionalCreateId = "1"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").write().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").write().resourcesOfType(Observation.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 3").updateConditional().resourcesOfType(Patient.class) + .build(); + //@formatter:on + } + }); + + HttpEntityEnclosingRequestBase httpPost; + HttpResponse status; + String response; + + ourHitMethod = false; + httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createPatient(null))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation?foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createObservation(null, "Patient/12"))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertFalse(ourHitMethod); + + } + + @Test + public void testWriteByCompartmentCreateConditionalResolvesToValid() throws Exception { + ourConditionalCreateId = "1"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").write().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").createConditional().resourcesOfType(Patient.class) + .build(); + //@formatter:on + } + }); + + HttpEntityEnclosingRequestBase httpPost; + HttpResponse status; + String response; + + ourHitMethod = false; + httpPost = new HttpPost("http://localhost:" + ourPort + "/Patient"); + httpPost.addHeader(Constants.HEADER_IF_NONE_EXIST, "foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createPatient(null))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + @Test + public void testWriteByCompartmentDeleteConditionalResolvesToValid() throws Exception { + ourConditionalCreateId = "1"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").write().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").deleteConditional().resourcesOfType(Patient.class) + .build(); + //@formatter:on + } + }); + + HttpDelete httpDelete; + HttpResponse status; + String response; + + ourHitMethod = false; + httpDelete = new HttpDelete("http://localhost:" + ourPort + "/Patient?foo=bar"); + status = ourClient.execute(httpDelete); + response = extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + } + + @Test + public void testWriteByCompartmentUpdateConditionalResolvesToValidAllTypes() throws Exception { + ourConditionalCreateId = "1"; + + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").write().resourcesOfType(Patient.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").write().resourcesOfType(Observation.class).inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 3").updateConditional().allResources() + .build(); + //@formatter:on + } + }); + + HttpEntityEnclosingRequestBase httpPost; + HttpResponse status; + String response; + + ourHitMethod = false; + httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createPatient(null))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(200, status.getStatusLine().getStatusCode()); + assertTrue(ourHitMethod); + + ourHitMethod = false; + httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation?foo=bar"); + httpPost.setEntity(createFhirResourceEntity(createObservation(null, "Patient/12"))); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertEquals(ERR403, response); + assertTrue(ourHitMethod); + + } + @AfterClass public static void afterClassClearContext() throws Exception { ourServer.stop(); @@ -977,7 +1163,7 @@ public class AuthorizationInterceptorDstu2Test { public static class DummyObservationResourceProvider implements IResourceProvider { @Create() - public MethodOutcome create(@ResourceParam Observation theResource) { + public MethodOutcome create(@ResourceParam Observation theResource, @ConditionalUrlParam String theConditionalUrl) { ourHitMethod = true; theResource.setId("Observation/1/_history/1"); MethodOutcome retVal = new MethodOutcome(); @@ -998,50 +1184,65 @@ public class AuthorizationInterceptorDstu2Test { return Observation.class; } - @Read(version = true) - public Observation read(@IdParam IdDt theId) { - ourHitMethod = true; - return (Observation) ourReturn.get(0); - } - - @Search() - public List search() { - ourHitMethod = true; - return ourReturn; - } - - @Update() - public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Observation theResource) { - ourHitMethod = true; - theResource.setId(theId.withVersion("2")); - MethodOutcome retVal = new MethodOutcome(); - retVal.setCreated(true); - retVal.setResource(theResource); - return retVal; - } @Operation(name="opName", idempotent=true) public Parameters operation() { ourHitMethod = true; return (Parameters) new Parameters().setId("1"); } + @Operation(name="opName", idempotent=true) public Parameters operation(@IdParam IdDt theId) { ourHitMethod = true; return (Parameters) new Parameters().setId("1"); } + @Read(version = true) + public Observation read(@IdParam IdDt theId) { + ourHitMethod = true; + return (Observation) ourReturn.get(0); + } + @Search() + public List search() { + ourHitMethod = true; + return ourReturn; + } + @Update() + public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Observation theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { + ourHitMethod = true; + + if (isNotBlank(theConditionalUrl)) { + IdDt actual = new IdDt("Observation", ourConditionalCreateId); + ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, actual); + subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE); + theResource.setId(actual); + } else { + ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, theResource); + subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE); + theResource.setId(theId.withVersion("2")); + } + + MethodOutcome retVal = new MethodOutcome(); + retVal.setCreated(true); + retVal.setResource(theResource); + return retVal; + } + } - + public static class DummyPatientResourceProvider implements IResourceProvider { + @Create() public MethodOutcome create(@ResourceParam Patient theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { if (isNotBlank(theConditionalUrl)) { - IdDt actual = new IdDt("Patient", "1123"); + IdDt actual = new IdDt("Patient", ourConditionalCreateId); ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, actual); subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.CREATE); + } else { + ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, theResource); + subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.CREATE); } ourHitMethod = true; @@ -1054,7 +1255,7 @@ public class AuthorizationInterceptorDstu2Test { @Delete() - public MethodOutcome delete(IRequestOperationCallback theRequestOperationCallback, @IdParam IdDt theId) { + public MethodOutcome delete(IRequestOperationCallback theRequestOperationCallback, @IdParam IdDt theId, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { ourHitMethod = true; for (IBaseResource next : ourReturn) { theRequestOperationCallback.resourceDeleted(next); @@ -1063,58 +1264,11 @@ public class AuthorizationInterceptorDstu2Test { return retVal; } - @Validate - public MethodOutcome validate(@ResourceParam Patient theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - ourHitMethod = true; - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setDiagnostics("OK"); - return new MethodOutcome(oo); - } - - @Validate - public MethodOutcome validate(@ResourceParam Patient theResource, @IdParam IdDt theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, - @Validate.Profile String theProfile, RequestDetails theRequestDetails) { - ourHitMethod = true; - OperationOutcome oo = new OperationOutcome(); - oo.addIssue().setDiagnostics("OK"); - return new MethodOutcome(oo); - } - @Override public Class getResourceType() { return Patient.class; } - - @Read(version = true) - public Patient read(@IdParam IdDt theId) { - ourHitMethod = true; - return (Patient) ourReturn.get(0); - } - - @Search() - public List search() { - ourHitMethod = true; - return ourReturn; - } - - @Update() - public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Patient theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { - ourHitMethod = true; - - if (isNotBlank(theConditionalUrl)) { - IdDt actual = new IdDt("Patient", "1123"); - ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, actual); - subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE); - } - theResource.setId(theId.withVersion("2")); - MethodOutcome retVal = new MethodOutcome(); - retVal.setCreated(true); - retVal.setResource(theResource); - return retVal; - } - @Operation(name="opName", idempotent=true) public Parameters operation() { ourHitMethod = true; @@ -1133,6 +1287,56 @@ public class AuthorizationInterceptorDstu2Test { return (Parameters) new Parameters().setId("1"); } + @Read(version = true) + public Patient read(@IdParam IdDt theId) { + ourHitMethod = true; + return (Patient) ourReturn.get(0); + } + + @Search() + public List search() { + ourHitMethod = true; + return ourReturn; + } + + @Update() + public MethodOutcome update(@IdParam IdDt theId, @ResourceParam Patient theResource, @ConditionalUrlParam String theConditionalUrl, RequestDetails theRequestDetails) { + ourHitMethod = true; + + if (isNotBlank(theConditionalUrl)) { + IdDt actual = new IdDt("Patient", ourConditionalCreateId); + ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, actual); + subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE); + theResource.setId(actual); + } else { + ActionRequestDetails subRequest = new ActionRequestDetails(theRequestDetails, theResource); + subRequest.notifyIncomingRequestPreHandled(RestOperationTypeEnum.UPDATE); + theResource.setId(theId.withVersion("2")); + } + + MethodOutcome retVal = new MethodOutcome(); + retVal.setResource(theResource); + return retVal; + } + + @Validate + public MethodOutcome validate(@ResourceParam Patient theResource, @IdParam IdDt theId, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, + @Validate.Profile String theProfile, RequestDetails theRequestDetails) { + ourHitMethod = true; + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDiagnostics("OK"); + return new MethodOutcome(oo); + } + + @Validate + public MethodOutcome validate(@ResourceParam Patient theResource, @ResourceParam String theRawResource, @ResourceParam EncodingEnum theEncoding, @Validate.Mode ValidationModeEnum theMode, + @Validate.Profile String theProfile, RequestDetails theRequestDetails) { + ourHitMethod = true; + OperationOutcome oo = new OperationOutcome(); + oo.addIssue().setDiagnostics("OK"); + return new MethodOutcome(oo); + } + } public static class PlainProvider diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu3Test.java index c3b49c5028d..a571adf3add 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/rest/server/UpdateDstu3Test.java @@ -1,7 +1,9 @@ package ca.uhn.fhir.rest.server; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import java.nio.charset.StandardCharsets; import java.util.concurrent.TimeUnit; import org.apache.commons.io.IOUtils; @@ -25,7 +27,6 @@ import org.junit.BeforeClass; import org.junit.Test; import ca.uhn.fhir.context.FhirContext; -import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.ConditionalUrlParam; import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.ResourceParam; @@ -52,7 +53,7 @@ public class UpdateDstu3Test { HttpResponse status = ourClient.execute(httpPost); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info("Response was:\n{}", responseContent); @@ -78,7 +79,7 @@ public class UpdateDstu3Test { CloseableHttpResponse status = ourClient.execute(httpPost); try { - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info("Response was:\n{}", responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -101,7 +102,7 @@ public class UpdateDstu3Test { CloseableHttpResponse status = ourClient.execute(httpPost); try { - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); ourLog.info("Response was:\n{}", responseContent); assertEquals(200, status.getStatusLine().getStatusCode()); @@ -117,28 +118,22 @@ public class UpdateDstu3Test { public void testUpdateWrongUrlInBody() throws Exception { Patient patient = new Patient(); - patient.setId("3"); + patient.setId("Patient/3/_history/4"); patient.addIdentifier().setValue("002"); - HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/001"); + HttpPut httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient/1/_history/2"); httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); HttpResponse status = ourClient.execute(httpPost); - String responseContent = IOUtils.toString(status.getEntity().getContent()); + String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8); IOUtils.closeQuietly(status.getEntity().getContent()); ourLog.info("Response was:\n{}", responseContent); - OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent); - assertEquals( - "Can not update resource, resource body must contain an ID element which matches the request URL for update (PUT) operation - Resource body ID of \"3\" does not match URL ID of \"001\"", - oo.getIssue().get(0).getDiagnostics()); - - assertEquals(400, status.getStatusLine().getStatusCode()); - assertNull(status.getFirstHeader("location")); - assertNull(status.getFirstHeader("content-location")); - + assertEquals(200, status.getStatusLine().getStatusCode()); + assertEquals("http://localhost:" + ourPort + "/Patient/1/_history/002", status.getFirstHeader("location").getValue()); + assertEquals("Patient/1/_history/2", ourId.getValue()); } @AfterClass diff --git a/src/changes/changes.xml b/src/changes/changes.xml index eb1aeb24464..68c03905713 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -161,6 +161,21 @@ if it contained custom fields that also used custom types. Thanks to GitHub user @sjanic for reporting! + + Servers in STU3 mode will now ignore any ID or VersionID found in the + resource body provided by the client when processing FHIR + update]]> operations. This change has been made + because the FHIR specification now requires servers to ignore + these values. Note that as a result of this change, resources passed + to @Update]]> methods will always have + null]]> ID + + + Add new methods to + AuthorizationInterceptor]]> + which allow user code to declare support for conditional + create, update, and delete. + diff --git a/src/site/xdoc/doc_rest_server_security.xml b/src/site/xdoc/doc_rest_server_security.xml index a36ad6d3c2f..87e88e8a6ee 100644 --- a/src/site/xdoc/doc_rest_server_security.xml +++ b/src/site/xdoc/doc_rest_server_security.xml @@ -91,8 +91,10 @@

This interceptor can help with the complicated task of determining whether a user has the appropriate permission to perform a given task on a FHIR server. This is - done by declaring + done by declaring a set of rules that can selectively allow (whitelist) and/or selectively + block (blacklist) requests.

+

AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet been heavily tested. Use with caution, and do lots of testing! We welcome From 379abb8e47cc1c33a40217c9f2b6abdc546a366d Mon Sep 17 00:00:00 2001 From: James Agnew Date: Tue, 16 Aug 2016 11:23:07 -0700 Subject: [PATCH 2/3] Don't strip urn: prefixes from references when encoding --- .../java/ca/uhn/fhir/i18n/HapiLocalizer.java | 52 ++++++++++----- .../java/ca/uhn/fhir/parser/BaseParser.java | 45 +++++++------ .../uhn/fhir/parser/XmlParserDstu3Test.java | 65 ++++++------------- src/changes/changes.xml | 5 ++ 4 files changed, 86 insertions(+), 81 deletions(-) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/HapiLocalizer.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/HapiLocalizer.java index 4d4024310f9..9c3143fe04d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/HapiLocalizer.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/i18n/HapiLocalizer.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.i18n; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /* * #%L * HAPI FHIR - Core Library @@ -10,7 +12,7 @@ package ca.uhn.fhir.i18n; * 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 + * 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, @@ -21,6 +23,8 @@ package ca.uhn.fhir.i18n; */ import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.ResourceBundle; import java.util.concurrent.ConcurrentHashMap; @@ -30,40 +34,56 @@ import java.util.concurrent.ConcurrentHashMap; */ public class HapiLocalizer { - private ResourceBundle myBundle; + private List myBundle = new ArrayList(); private final Map myKeyToMessageFormat = new ConcurrentHashMap(); public HapiLocalizer() { - myBundle = ResourceBundle.getBundle(HapiLocalizer.class.getPackage().getName() + ".hapi-messages"); + this(HapiLocalizer.class.getPackage().getName() + ".hapi-messages"); + } + + public HapiLocalizer(String... theBundleNames) { + for (String nextName : theBundleNames) { + myBundle.add(ResourceBundle.getBundle(nextName)); + } } public String getMessage(Class theType, String theKey, Object... theParameters) { return getMessage(theType.getName() + '.' + theKey, theParameters); } - + public String getMessage(String theQualifiedKey, Object... theParameters) { if (theParameters != null && theParameters.length > 0) { MessageFormat format = myKeyToMessageFormat.get(theQualifiedKey); if (format != null) { return format.format(theParameters).toString(); } - - String formatString = myBundle.getString(theQualifiedKey); - if (formatString== null) { - formatString = "!MESSAGE!"; - } - + + String formatString = findFormatString(theQualifiedKey); + format = new MessageFormat(formatString.trim()); myKeyToMessageFormat.put(theQualifiedKey, format); return format.format(theParameters).toString(); } else { - String retVal = myBundle.getString(theQualifiedKey); - if (retVal == null) { - retVal = "!MESSAGE!"; - } + String retVal = findFormatString(theQualifiedKey); return retVal; } } - - + + private String findFormatString(String theQualifiedKey) { + String formatString = null; + for (ResourceBundle nextBundle : myBundle) { + if (nextBundle.containsKey(theQualifiedKey)) { + formatString = nextBundle.getString(theQualifiedKey); + } + if (isNotBlank(formatString)) { + break; + } + } + + if (formatString == null) { + formatString = "!MESSAGE!"; + } + return formatString; + } + } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java index 0df72d48812..17c00535a52 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/parser/BaseParser.java @@ -10,7 +10,7 @@ package ca.uhn.fhir.parser; * 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 + * 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, @@ -285,13 +285,17 @@ public abstract class BaseParser implements IParser { IIdType refId = theRef.getResource().getIdElement(); if (refId != null) { if (refId.hasIdPart()) { - if (!refId.hasResourceType()) { - refId = refId.withResourceType(myContext.getResourceDefinition(theRef.getResource()).getName()); - } - if (isStripVersionsFromReferences(theCompositeChildElement)) { - reference = refId.toVersionless().getValue(); - } else { + if (refId.getValue().startsWith("urn:")) { reference = refId.getValue(); + } else { + if (!refId.hasResourceType()) { + refId = refId.withResourceType(myContext.getResourceDefinition(theRef.getResource()).getName()); + } + if (isStripVersionsFromReferences(theCompositeChildElement)) { + reference = refId.toVersionless().getValue(); + } else { + reference = refId.getValue(); + } } } } @@ -388,7 +392,8 @@ public abstract class BaseParser implements IParser { Validate.notNull(theWriter, "theWriter can not be null"); if (theResource.getStructureFhirVersionEnum() != myContext.getVersion().getVersion()) { - throw new IllegalArgumentException("This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum()); + throw new IllegalArgumentException( + "This parser is for FHIR version " + myContext.getVersion().getVersion() + " - Can not encode a structure for version " + theResource.getStructureFhirVersionEnum()); } doEncodeResourceToWriter(theResource, theWriter); @@ -430,9 +435,9 @@ public abstract class BaseParser implements IParser { String childName = theChild.getChildNameByDatatype(type); BaseRuntimeElementDefinition childDef = theChild.getChildElementDefinitionByDatatype(type); if (childDef == null) { - // if (theValue instanceof IBaseExtension) { - // return null; - // } + // if (theValue instanceof IBaseExtension) { + // return null; + // } /* * For RI structures Enumeration class, this replaces the child def @@ -578,7 +583,8 @@ public abstract class BaseParser implements IParser { } protected boolean isChildContained(BaseRuntimeElementDefinition childDef, boolean theIncludedResource) { - return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) && getContainedResources().isEmpty() == false && theIncludedResource == false; + return (childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCES || childDef.getChildType() == ChildTypeEnum.CONTAINED_RESOURCE_LIST) && getContainedResources().isEmpty() == false + && theIncludedResource == false; } @Override @@ -623,7 +629,7 @@ public abstract class BaseParser implements IParser { @Override public T parseResource(Class theResourceType, Reader theReader) throws DataFormatException { - /* + /* * We do this so that the context can verify that the structure is for * the correct FHIR version */ @@ -698,7 +704,8 @@ public abstract class BaseParser implements IParser { return parseTagList(new StringReader(theString)); } - protected List preProcessValues(BaseRuntimeChildDefinition theMetaChildUncast, IBaseResource theResource, List theValues, CompositeChildElement theCompositeChildElement) { + protected List preProcessValues(BaseRuntimeChildDefinition theMetaChildUncast, IBaseResource theResource, List theValues, + CompositeChildElement theCompositeChildElement) { if (myContext.getVersion().getVersion().isRi()) { /* @@ -1156,11 +1163,11 @@ public abstract class BaseParser implements IParser { retVal = !checkIfParentShouldNotBeEncodedAndBuildPath(new StringBuilder(), true); } } - // if (retVal == false && myEncodeElements.contains("*.(mandatory)")) { - // if (myDef.getMin() > 0) { - // retVal = true; - // } - // } + // if (retVal == false && myEncodeElements.contains("*.(mandatory)")) { + // if (myDef.getMin() > 0) { + // retVal = true; + // } + // } return retVal; } diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java index 3b186fa8719..b88e3678ec2 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java @@ -4,6 +4,7 @@ import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.Matchers.stringContainsInOrder; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; @@ -34,66 +35,21 @@ import org.custommonkey.xmlunit.XMLUnit; import org.hamcrest.collection.IsEmptyCollection; import org.hamcrest.core.StringContains; import org.hamcrest.text.StringContainsInOrder; +import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.Address.AddressUse; import org.hl7.fhir.dstu3.model.Address.AddressUseEnumFactory; -import org.hl7.fhir.dstu3.model.AllergyIntolerance; -import org.hl7.fhir.dstu3.model.Annotation; -import org.hl7.fhir.dstu3.model.Appointment; -import org.hl7.fhir.dstu3.model.AuditEvent; -import org.hl7.fhir.dstu3.model.Binary; -import org.hl7.fhir.dstu3.model.Bundle; import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleLinkComponent; import org.hl7.fhir.dstu3.model.Bundle.BundleType; -import org.hl7.fhir.dstu3.model.CodeType; -import org.hl7.fhir.dstu3.model.CodeableConcept; -import org.hl7.fhir.dstu3.model.Coding; -import org.hl7.fhir.dstu3.model.Communication; -import org.hl7.fhir.dstu3.model.Composition; -import org.hl7.fhir.dstu3.model.ConceptMap; -import org.hl7.fhir.dstu3.model.Condition; import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem; -import org.hl7.fhir.dstu3.model.DataElement; -import org.hl7.fhir.dstu3.model.DateTimeType; -import org.hl7.fhir.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.DiagnosticReport; import org.hl7.fhir.dstu3.model.DiagnosticReport.DiagnosticReportStatus; -import org.hl7.fhir.dstu3.model.DocumentManifest; -import org.hl7.fhir.dstu3.model.Duration; -import org.hl7.fhir.dstu3.model.ElementDefinition; import org.hl7.fhir.dstu3.model.ElementDefinition.ElementDefinitionBindingComponent; -import org.hl7.fhir.dstu3.model.Encounter; -import org.hl7.fhir.dstu3.model.EnumFactory; -import org.hl7.fhir.dstu3.model.Enumeration; import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; import org.hl7.fhir.dstu3.model.Enumerations.DocumentReferenceStatus; -import org.hl7.fhir.dstu3.model.Extension; -import org.hl7.fhir.dstu3.model.GuidanceResponse; -import org.hl7.fhir.dstu3.model.HumanName; import org.hl7.fhir.dstu3.model.HumanName.NameUse; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.Identifier; import org.hl7.fhir.dstu3.model.Identifier.IdentifierUse; -import org.hl7.fhir.dstu3.model.InstantType; -import org.hl7.fhir.dstu3.model.Location; -import org.hl7.fhir.dstu3.model.Medication; -import org.hl7.fhir.dstu3.model.MedicationOrder; -import org.hl7.fhir.dstu3.model.MedicationStatement; -import org.hl7.fhir.dstu3.model.Observation; import org.hl7.fhir.dstu3.model.Observation.ObservationRelationshipType; import org.hl7.fhir.dstu3.model.Observation.ObservationStatus; -import org.hl7.fhir.dstu3.model.Organization; -import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.dstu3.model.PrimitiveType; -import org.hl7.fhir.dstu3.model.ProcedureRequest; -import org.hl7.fhir.dstu3.model.Quantity; -import org.hl7.fhir.dstu3.model.Reference; -import org.hl7.fhir.dstu3.model.Resource; -import org.hl7.fhir.dstu3.model.SampledData; -import org.hl7.fhir.dstu3.model.SimpleQuantity; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.UriType; -import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IBaseResource; import org.junit.After; import org.junit.AfterClass; @@ -126,6 +82,23 @@ public class XmlParserDstu3Test { ourCtx.setNarrativeGenerator(null); } + @Test + public void testEncodeReferenceWithUuid() { + + Practitioner pract = new Practitioner(); + pract.setId(IdType.newRandomUuid()); + pract.addName().addFamily("PRACT FAMILY"); + + Patient patient = new Patient(); + patient.addGeneralPractitioner().setResource(pract); + + String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); + ourLog.info(encoded); + + assertThat(pract.getId(), startsWith("urn:uuid:")); + assertThat(encoded, containsString("")); + } + @Test public void testEncodeAndParseContainedCustomTypes() { ourCtx = FhirContext.forDstu3(); diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 68c03905713..3d75fcc329d 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -176,6 +176,11 @@ which allow user code to declare support for conditional create, update, and delete. + + When encoding a resource with a reference to another resource + that has a placeholder ID (e.g. urn:uuid:foo), the urn prefix + was incorrectly stripped from the reference. + From da8abca1ffdaa4dcae3d931b2e4732e437951cac Mon Sep 17 00:00:00 2001 From: James Agnew Date: Sun, 21 Aug 2016 10:24:42 -0700 Subject: [PATCH 3/3] Add custom extension example --- .../customtype/CustomCompositeExtension.java | 84 ++++ .../search/DatabaseBackedPagingProvider.java | 2 +- .../FhirResourceDaoDstu3SearchNoFtTest.java | 60 +-- .../dstu3/ResourceProviderDstu3Test.java | 44 ++ .../ca/uhn/fhirtest/TestRestfulServer.java | 8 +- .../PatientWithCustomCompositeExtension.java | 82 ++++ .../uhn/fhir/parser/XmlParserDstu3Test.java | 378 +++++++++--------- src/site/xdoc/doc_extensions.xml | 30 ++ 8 files changed, 447 insertions(+), 241 deletions(-) create mode 100644 examples/src/main/java/example/customtype/CustomCompositeExtension.java create mode 100644 hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/PatientWithCustomCompositeExtension.java diff --git a/examples/src/main/java/example/customtype/CustomCompositeExtension.java b/examples/src/main/java/example/customtype/CustomCompositeExtension.java new file mode 100644 index 00000000000..49a1ce5f5ce --- /dev/null +++ b/examples/src/main/java/example/customtype/CustomCompositeExtension.java @@ -0,0 +1,84 @@ +package example.customtype; + +import org.hl7.fhir.dstu3.model.BackboneElement; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.util.ElementUtil; + +//START SNIPPET: resource +@ResourceDef(name = "Patient") +public class CustomCompositeExtension extends Patient { + + private static final long serialVersionUID = 1L; + + /** + * A custom extension + */ + @Child(name = "foo") + @Extension(url="http://acme.org/fooParent", definedLocally = false, isModifier = false) + protected FooParentExtension fooParentExtension; + + public FooParentExtension getFooParentExtension() { + return fooParentExtension; + } + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(fooParentExtension); + } + + public void setFooParentExtension(FooParentExtension theFooParentExtension) { + fooParentExtension = theFooParentExtension; + } + + @Block + public static class FooParentExtension extends BackboneElement { + + private static final long serialVersionUID = 4522090347756045145L; + + @Child(name = "childA") + @Extension(url = "http://acme.org/fooChildA", definedLocally = false, isModifier = false) + private StringType myChildA; + + @Child(name = "childB") + @Extension(url = "http://acme.org/fooChildB", definedLocally = false, isModifier = false) + private StringType myChildB; + + @Override + public FooParentExtension copy() { + FooParentExtension copy = new FooParentExtension(); + copy.myChildA = myChildA; + copy.myChildB = myChildB; + return copy; + } + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(myChildA, myChildB); + } + + public StringType getChildA() { + return myChildA; + } + + public StringType getChildB() { + return myChildB; + } + + public void setChildA(StringType theChildA) { + myChildA = theChildA; + } + + public void setChildB(StringType theChildB) { + myChildB = theChildB; + } + + } + +} +//END SNIPPET: resource diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java index 3184454e192..6fc1ddba100 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/search/DatabaseBackedPagingProvider.java @@ -58,7 +58,7 @@ public class DatabaseBackedPagingProvider extends FifoMemoryPagingProvider { if (!provider.ensureSearchEntityLoaded()) { return null; } - return provider; + retVal = provider; } return retVal; } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java index 0ed2703f7ae..c28c06470a8 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/dao/dstu3/FhirResourceDaoDstu3SearchNoFtTest.java @@ -14,46 +14,16 @@ import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import java.math.BigDecimal; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import javax.servlet.http.HttpServletRequest; import org.apache.commons.lang3.StringUtils; -import org.hl7.fhir.dstu3.model.Appointment; -import org.hl7.fhir.dstu3.model.CodeType; -import org.hl7.fhir.dstu3.model.CodeableConcept; -import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.*; import org.hl7.fhir.dstu3.model.ContactPoint.ContactPointSystem; -import org.hl7.fhir.dstu3.model.DateTimeType; -import org.hl7.fhir.dstu3.model.DateType; -import org.hl7.fhir.dstu3.model.Device; -import org.hl7.fhir.dstu3.model.DiagnosticRequest; -import org.hl7.fhir.dstu3.model.DiagnosticReport; -import org.hl7.fhir.dstu3.model.Encounter; import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; -import org.hl7.fhir.dstu3.model.IdType; -import org.hl7.fhir.dstu3.model.Immunization; -import org.hl7.fhir.dstu3.model.Location; -import org.hl7.fhir.dstu3.model.Medication; -import org.hl7.fhir.dstu3.model.MedicationOrder; -import org.hl7.fhir.dstu3.model.Observation; -import org.hl7.fhir.dstu3.model.Organization; -import org.hl7.fhir.dstu3.model.Patient; -import org.hl7.fhir.dstu3.model.Period; -import org.hl7.fhir.dstu3.model.Practitioner; -import org.hl7.fhir.dstu3.model.Quantity; -import org.hl7.fhir.dstu3.model.Reference; -import org.hl7.fhir.dstu3.model.StringType; -import org.hl7.fhir.dstu3.model.Subscription; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType; import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; -import org.hl7.fhir.dstu3.model.Substance; -import org.hl7.fhir.dstu3.model.TemporalPrecisionEnum; -import org.hl7.fhir.dstu3.model.ValueSet; import org.hl7.fhir.instance.model.api.IAnyResource; import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IIdType; @@ -63,35 +33,13 @@ import org.junit.Test; import ca.uhn.fhir.jpa.dao.BaseHapiFhirDao; import ca.uhn.fhir.jpa.dao.SearchParameterMap; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamDate; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamNumber; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamQuantity; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamString; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamToken; -import ca.uhn.fhir.jpa.entity.ResourceIndexedSearchParamUri; -import ca.uhn.fhir.jpa.entity.ResourceLink; +import ca.uhn.fhir.jpa.entity.*; import ca.uhn.fhir.model.api.IQueryParameterType; import ca.uhn.fhir.model.api.Include; import ca.uhn.fhir.model.dstu.valueset.QuantityCompararatorEnum; import ca.uhn.fhir.rest.api.SortOrderEnum; import ca.uhn.fhir.rest.api.SortSpec; -import ca.uhn.fhir.rest.param.CompositeParam; -import ca.uhn.fhir.rest.param.DateParam; -import ca.uhn.fhir.rest.param.DateRangeParam; -import ca.uhn.fhir.rest.param.HasParam; -import ca.uhn.fhir.rest.param.NumberParam; -import ca.uhn.fhir.rest.param.ParamPrefixEnum; -import ca.uhn.fhir.rest.param.QuantityParam; -import ca.uhn.fhir.rest.param.ReferenceParam; -import ca.uhn.fhir.rest.param.StringAndListParam; -import ca.uhn.fhir.rest.param.StringOrListParam; -import ca.uhn.fhir.rest.param.StringParam; -import ca.uhn.fhir.rest.param.TokenAndListParam; -import ca.uhn.fhir.rest.param.TokenOrListParam; -import ca.uhn.fhir.rest.param.TokenParam; -import ca.uhn.fhir.rest.param.TokenParamModifier; -import ca.uhn.fhir.rest.param.UriParam; -import ca.uhn.fhir.rest.param.UriParamQualifierEnum; +import ca.uhn.fhir.rest.param.*; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.rest.server.IBundleProvider; import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java index bfa7ae574e2..99986be7610 100644 --- a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/ResourceProviderDstu3Test.java @@ -1,5 +1,6 @@ package ca.uhn.fhir.jpa.provider.dstu3; +import static org.apache.commons.lang3.StringUtils.isNotBlank; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInRelativeOrder; @@ -95,9 +96,11 @@ import org.hl7.fhir.instance.model.api.IIdType; import org.junit.AfterClass; import org.junit.Ignore; import org.junit.Test; +import org.springframework.core.NestedExceptionUtils; import com.google.common.collect.Lists; +import ca.uhn.fhir.jpa.dao.SearchParameterMap; import ca.uhn.fhir.model.primitive.InstantDt; import ca.uhn.fhir.model.primitive.UriDt; import ca.uhn.fhir.parser.IParser; @@ -128,6 +131,47 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { } + @Test + public void testSearchPagingKeepsOldSearches() throws Exception { + String methodName = "testSearchPagingKeepsOldSearches"; + IIdType pid1; + { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue("0"); + patient.addName().addFamily(methodName).addGiven("Joe"); + pid1 = myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + for (int i = 1; i <= 20; i++) { + Patient patient = new Patient(); + patient.addIdentifier().setSystem("urn:system").setValue(Integer.toString(i)); + patient.addName().addFamily(methodName).addGiven("Joe"); + myPatientDao.create(patient, mySrd).getId().toUnqualifiedVersionless(); + } + + List linkNext = Lists.newArrayList(); + for (int i = 0 ; i < 100; i++) { + Bundle bundle = ourClient + .search() + .forResource(Patient.class) + .where(Patient.NAME.matches().value("testSearchPagingKeepsOldSearches")) + .count(5) + .returnBundle(Bundle.class) + .execute(); + assertTrue(isNotBlank(bundle.getLink("next").getUrl())); + assertEquals(5, bundle.getEntry().size()); + linkNext.add(bundle.getLink("next").getUrl()); + } + + int index = 0; + for (String nextLink : linkNext) { + ourLog.info("Fetching index {}", index++); + Bundle b = ourClient.fetchResourceFromUrl(Bundle.class, nextLink); + assertEquals(5, b.getEntry().size()); + } + } + + @Test public void testHasParameter() throws Exception { IIdType pid0; diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java index c78f90f983a..8b076a2d19b 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/TestRestfulServer.java @@ -25,6 +25,7 @@ import ca.uhn.fhir.jpa.provider.JpaSystemProviderDstu2; import ca.uhn.fhir.jpa.provider.dstu3.JpaConformanceProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.JpaSystemProviderDstu3; import ca.uhn.fhir.jpa.provider.dstu3.TerminologyUploaderProviderDstu3; +import ca.uhn.fhir.jpa.search.DatabaseBackedPagingProvider; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.rest.server.ETagSupportEnum; import ca.uhn.fhir.rest.server.EncodingEnum; @@ -200,11 +201,10 @@ public class TestRestfulServer extends RestfulServer { setServerAddressStrategy(new MyHardcodedServerAddressStrategy(baseUrl)); /* - * This is a simple paging strategy that keeps the last 10 - * searches in memory + * Spool results to the database */ - setPagingProvider(new FifoMemoryPagingProvider(10).setMaximumPageSize(500)); - + setPagingProvider(myAppCtx.getBean(DatabaseBackedPagingProvider.class)); + /* * Load interceptors for the server from Spring */ diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/PatientWithCustomCompositeExtension.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/PatientWithCustomCompositeExtension.java new file mode 100644 index 00000000000..82ea6448407 --- /dev/null +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/PatientWithCustomCompositeExtension.java @@ -0,0 +1,82 @@ +package ca.uhn.fhir.parser; + +import org.hl7.fhir.dstu3.model.BackboneElement; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.StringType; + +import ca.uhn.fhir.model.api.annotation.Block; +import ca.uhn.fhir.model.api.annotation.Child; +import ca.uhn.fhir.model.api.annotation.Extension; +import ca.uhn.fhir.model.api.annotation.ResourceDef; +import ca.uhn.fhir.util.ElementUtil; + +@ResourceDef(name = "Patient") +public class PatientWithCustomCompositeExtension extends Patient { + + private static final long serialVersionUID = 1L; + + /** + * A custom extension + */ + @Child(name = "foo") + @Extension(url="http://acme.org/fooParent", definedLocally = false, isModifier = false) + protected FooParentExtension fooParentExtension; + + public FooParentExtension getFooParentExtension() { + return fooParentExtension; + } + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(fooParentExtension); + } + + public void setFooParentExtension(FooParentExtension theFooParentExtension) { + fooParentExtension = theFooParentExtension; + } + + @Block + public static class FooParentExtension extends BackboneElement { + + private static final long serialVersionUID = 4522090347756045145L; + + @Child(name = "childA") + @Extension(url = "http://acme.org/fooChildA", definedLocally = false, isModifier = false) + private StringType myChildA; + + @Child(name = "childB") + @Extension(url = "http://acme.org/fooChildB", definedLocally = false, isModifier = false) + private StringType myChildB; + + @Override + public FooParentExtension copy() { + FooParentExtension copy = new FooParentExtension(); + copy.myChildA = myChildA; + copy.myChildB = myChildB; + return copy; + } + + @Override + public boolean isEmpty() { + return super.isEmpty() && ElementUtil.isEmpty(myChildA, myChildB); + } + + public StringType getChildA() { + return myChildA; + } + + public StringType getChildB() { + return myChildB; + } + + public void setChildA(StringType theChildA) { + myChildA = theChildA; + } + + public void setChildB(StringType theChildB) { + myChildB = theChildB; + } + + } + +} \ No newline at end of file diff --git a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java index b88e3678ec2..b6535883644 100644 --- a/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java +++ b/hapi-fhir-structures-dstu3/src/test/java/ca/uhn/fhir/parser/XmlParserDstu3Test.java @@ -66,6 +66,7 @@ import ca.uhn.fhir.model.api.annotation.ResourceDef; import ca.uhn.fhir.narrative.DefaultThymeleafNarrativeGenerator; import ca.uhn.fhir.parser.FooMessageHeaderWithExplicitField.FooMessageSourceComponent; import ca.uhn.fhir.parser.IParserErrorHandler.IParseLocation; +import ca.uhn.fhir.parser.PatientWithCustomCompositeExtension.FooParentExtension; import ca.uhn.fhir.rest.client.IGenericClient; import ca.uhn.fhir.rest.server.Constants; import ca.uhn.fhir.util.TestUtil; @@ -82,159 +83,6 @@ public class XmlParserDstu3Test { ourCtx.setNarrativeGenerator(null); } - @Test - public void testEncodeReferenceWithUuid() { - - Practitioner pract = new Practitioner(); - pract.setId(IdType.newRandomUuid()); - pract.addName().addFamily("PRACT FAMILY"); - - Patient patient = new Patient(); - patient.addGeneralPractitioner().setResource(pract); - - String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); - ourLog.info(encoded); - - assertThat(pract.getId(), startsWith("urn:uuid:")); - assertThat(encoded, containsString("")); - } - - @Test - public void testEncodeAndParseContainedCustomTypes() { - ourCtx = FhirContext.forDstu3(); - ourCtx.setDefaultTypeForProfile(CustomObservation.PROFILE, CustomObservation.class); - ourCtx.setDefaultTypeForProfile(CustomDiagnosticReport.PROFILE, CustomDiagnosticReport.class); - - CustomObservation obs = new CustomObservation(); - obs.setStatus(ObservationStatus.FINAL); - - CustomDiagnosticReport dr = new CustomDiagnosticReport(); - dr.setStatus(DiagnosticReportStatus.FINAL); - dr.addResult().setResource(obs); - - IParser parser = ourCtx.newXmlParser(); - parser.setPrettyPrint(true); - - String output = parser.encodeResourceToString(dr); - ourLog.info(output); - - //@formatter:off - assertThat(output,stringContainsInOrder( - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "")); - //@formatter:on - - /* - * Now PARSE! - */ - - dr = (CustomDiagnosticReport) parser.parseResource(output); - assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); - - assertEquals("#1", dr.getResult().get(0).getReference()); - obs = (CustomObservation) dr.getResult().get(0).getResource(); - assertEquals(ObservationStatus.FINAL, obs.getStatus()); - - ourCtx = null; - } - - @Test - public void testEncodeAndParseContainedNonCustomTypes() { - ourCtx = FhirContext.forDstu3(); - - Observation obs = new Observation(); - obs.setStatus(ObservationStatus.FINAL); - - DiagnosticReport dr = new DiagnosticReport(); - dr.setStatus(DiagnosticReportStatus.FINAL); - dr.addResult().setResource(obs); - - IParser parser = ourCtx.newXmlParser(); - parser.setPrettyPrint(true); - - String output = parser.encodeResourceToString(dr); - ourLog.info(output); - - //@formatter:off - assertThat(output,stringContainsInOrder( - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "", - "")); - //@formatter:on - - /* - * Now PARSE! - */ - - dr = (DiagnosticReport) parser.parseResource(output); - assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); - - assertEquals("#1", dr.getResult().get(0).getReference()); - obs = (Observation) dr.getResult().get(0).getResource(); - assertEquals(ObservationStatus.FINAL, obs.getStatus()); - - ourCtx = null; - } - - @Test - public void testEncodeHistoryEncodeVersionsAtPath3() { - ourCtx = FhirContext.forDstu3(); - - assertNull(ourCtx.newXmlParser().getStripVersionsFromReferences()); - - AuditEvent auditEvent = new AuditEvent(); - auditEvent.addEntity().setReference(new Reference("http://foo.com/Organization/2/_history/1")); - - IParser parser = ourCtx.newXmlParser(); - - parser.setDontStripVersionsFromReferencesAtPaths("AuditEvent.entity.reference"); - String enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); - ourLog.info(enc); - assertThat(enc, containsString("")); - - parser.setDontStripVersionsFromReferencesAtPaths(new ArrayList()); - enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); - ourLog.info(enc); - assertThat(enc, containsString("")); - - parser.setDontStripVersionsFromReferencesAtPaths((String[])null); - enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); - ourLog.info(enc); - assertThat(enc, containsString("")); - - parser.setDontStripVersionsFromReferencesAtPaths((List)null); - enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); - ourLog.info(enc); - assertThat(enc, containsString("")); - - } - - @Test public void testBundleWithBinary() { //@formatter:off @@ -265,7 +113,7 @@ public class XmlParserDstu3Test { assertArrayEquals(new byte[] { 1, 2, 3, 4 }, bin.getContent()); } - + @Test public void testContainedResourceInExtensionUndeclared() { Patient p = new Patient(); @@ -287,7 +135,7 @@ public class XmlParserDstu3Test { o = (Organization) rr.getResource(); assertEquals("ORG", o.getName()); } - + @Test public void testDuration() { Encounter enc = new Encounter(); @@ -301,7 +149,7 @@ public class XmlParserDstu3Test { assertThat(str, not(containsString("meta"))); assertThat(str, containsString("")); } - + @Test public void testEncodeAndParseBundleWithResourceRefs() { @@ -339,6 +187,24 @@ public class XmlParserDstu3Test { assertSame(org, pt.getManagingOrganization().getResource()); } + @Test + public void testEncodeAndParseCompositeExtension() { + PatientWithCustomCompositeExtension pat = new PatientWithCustomCompositeExtension(); + pat.setId("123"); + pat.setFooParentExtension(new FooParentExtension()); + pat.getFooParentExtension().setChildA(new StringType("ValueA")); + pat.getFooParentExtension().setChildB(new StringType("ValueB")); + + String enc = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(pat); + ourLog.info(enc); + + pat = ourCtx.newXmlParser().parseResource(PatientWithCustomCompositeExtension.class, enc); + + assertEquals("ValueA", pat.getFooParentExtension().getChildA().getValue()); + assertEquals("ValueB", pat.getFooParentExtension().getChildB().getValue()); + } + + @Test public void testEncodeAndParseContained() { IParser xmlParser = ourCtx.newXmlParser().setPrettyPrint(true); @@ -408,6 +274,108 @@ public class XmlParserDstu3Test { } + @Test + public void testEncodeAndParseContainedCustomTypes() { + ourCtx = FhirContext.forDstu3(); + ourCtx.setDefaultTypeForProfile(CustomObservation.PROFILE, CustomObservation.class); + ourCtx.setDefaultTypeForProfile(CustomDiagnosticReport.PROFILE, CustomDiagnosticReport.class); + + CustomObservation obs = new CustomObservation(); + obs.setStatus(ObservationStatus.FINAL); + + CustomDiagnosticReport dr = new CustomDiagnosticReport(); + dr.setStatus(DiagnosticReportStatus.FINAL); + dr.addResult().setResource(obs); + + IParser parser = ourCtx.newXmlParser(); + parser.setPrettyPrint(true); + + String output = parser.encodeResourceToString(dr); + ourLog.info(output); + + //@formatter:off + assertThat(output,stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "")); + //@formatter:on + + /* + * Now PARSE! + */ + + dr = (CustomDiagnosticReport) parser.parseResource(output); + assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); + + assertEquals("#1", dr.getResult().get(0).getReference()); + obs = (CustomObservation) dr.getResult().get(0).getResource(); + assertEquals(ObservationStatus.FINAL, obs.getStatus()); + + ourCtx = null; + } + + @Test + public void testEncodeAndParseContainedNonCustomTypes() { + ourCtx = FhirContext.forDstu3(); + + Observation obs = new Observation(); + obs.setStatus(ObservationStatus.FINAL); + + DiagnosticReport dr = new DiagnosticReport(); + dr.setStatus(DiagnosticReportStatus.FINAL); + dr.addResult().setResource(obs); + + IParser parser = ourCtx.newXmlParser(); + parser.setPrettyPrint(true); + + String output = parser.encodeResourceToString(dr); + ourLog.info(output); + + //@formatter:off + assertThat(output,stringContainsInOrder( + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "")); + //@formatter:on + + /* + * Now PARSE! + */ + + dr = (DiagnosticReport) parser.parseResource(output); + assertEquals(DiagnosticReportStatus.FINAL, dr.getStatus()); + + assertEquals("#1", dr.getResult().get(0).getReference()); + obs = (Observation) dr.getResult().get(0).getResource(); + assertEquals(ObservationStatus.FINAL, obs.getStatus()); + + ourCtx = null; + } + @Test public void testEncodeAndParseExtensionOnCode() { Organization o = new Organization(); @@ -1113,6 +1081,30 @@ public class XmlParserDstu3Test { ourLog.info(parser.encodeResourceToString(gr)); } + @Test + public void testEncodeDeclaredBlock() throws Exception { + FooMessageSourceComponent source = new FooMessageHeaderWithExplicitField.FooMessageSourceComponent(); + source.getMessageHeaderApplicationId().setValue("APPID"); + source.setName("NAME"); + + FooMessageHeaderWithExplicitField header = new FooMessageHeaderWithExplicitField(); + header.setSourceNew(source); + + header.addDestination().setName("DEST"); + + Bundle bundle = new Bundle(); + bundle.addEntry().setResource(header); + + IParser p = ourCtx.newXmlParser(); + p.setPrettyPrint(true); + + String encode = p.encodeResourceToString(bundle); + ourLog.info(encode); + + assertThat(encode, containsString("")); + assertThat(encode, stringContainsInOrder("")); + + parser.setDontStripVersionsFromReferencesAtPaths(new ArrayList()); + enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); + ourLog.info(enc); + assertThat(enc, containsString("")); + + parser.setDontStripVersionsFromReferencesAtPaths((String[])null); + enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); + ourLog.info(enc); + assertThat(enc, containsString("")); + + parser.setDontStripVersionsFromReferencesAtPaths((List)null); + enc = parser.setPrettyPrint(true).encodeResourceToString(auditEvent); + ourLog.info(enc); + assertThat(enc, containsString("")); + + } + @Test public void testEncodeNarrativeSuppressed() { Patient patient = new Patient(); @@ -1471,6 +1496,23 @@ public class XmlParserDstu3Test { assertThat(str, containsString("")); } + @Test + public void testEncodeReferenceWithUuid() { + + Practitioner pract = new Practitioner(); + pract.setId(IdType.newRandomUuid()); + pract.addName().addFamily("PRACT FAMILY"); + + Patient patient = new Patient(); + patient.addGeneralPractitioner().setResource(pract); + + String encoded = ourCtx.newXmlParser().setPrettyPrint(true).encodeResourceToString(patient); + ourLog.info(encoded); + + assertThat(pract.getId(), startsWith("urn:uuid:")); + assertThat(encoded, containsString("")); + } + @Test public void testEncodeSummary() { Patient patient = new Patient(); @@ -1534,30 +1576,6 @@ public class XmlParserDstu3Test { assertThat(encode, stringContainsInOrder("")); - assertThat(encode, stringContainsInOrder(" + + + +

+ The following example shows a resource containing a composite + extension. +

+ + + + + + +

+ This could be used to create a resource such as the + following: +

+ + + + + + + + + + +]]> + +