Add support for conditional operations to AuthorizationInterceptor

This commit is contained in:
James Agnew 2016-08-15 13:35:50 -04:00
parent f64337b651
commit eee168ced6
32 changed files with 1209 additions and 644 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<String, String[]> 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<String, List<String>> myUnqualifiedToQualifiedNames;
private Map<Object, Object> myUserData;
protected abstract byte[] getByteStreamRequestContents();
@ -76,6 +79,40 @@ public abstract class RequestDetails {
return myCompleteUrl;
}
/**
* Returns the <b>conditional URL</b> if this request has one, or <code>null</code> otherwise. For an
* update or delete method, this is the part of the URL after the <code>?</code>. For a create, this
* is the value of the <code>If-None-Exist</code> header.
*
* @param theOperationType The operation type to find the conditional URL for
* @return Returns the <b>conditional URL</b> if this request has one, or <code>null</code> 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.
* <p>
@ -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<String, List<String>> 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<IServerInterceptor> 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<IServerInterceptor> 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<? extends IBaseResource> 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);
}
}
}
}
}

View File

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

View File

@ -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<IAuthRule> 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<IAuthRule> rules = buildRuleList(theRequestDetails);
List<IBaseResource> 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 {

View File

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

View File

@ -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 <code>null</code>
* @param theInputResourceId TODO
* @param theOutputResource
* The resource being returned by the server, or <code>null</code>
* @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 <code>null</code> 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

View File

@ -0,0 +1,17 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
import org.hl7.fhir.instance.model.api.IBaseResource;
public interface IAuthRuleBuilderAppliesTo<T> {
/**
* Rule applies to resources of the given type
*/
T resourcesOfType(Class<? extends IBaseResource> theType);
/**
* Rule applies to all resources
*/
T allResources();
}

View File

@ -22,11 +22,39 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
public interface IAuthRuleBuilderRule {
/**
* This rule applies to <code>create</code> operations with a <code>conditional</code>
* 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.
* <p>
* 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.
* </p>
*/
IAuthRuleBuilderRuleConditional createConditional();
/**
* This rule applies to the FHIR delete operation
*/
IAuthRuleBuilderRuleOp delete();
/**
* This rule applies to <code>create</code> operations with a <code>conditional</code>
* 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.
* <p>
* 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.
* </p>
*/
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. <code>$validate</code>)
*/
IAuthRuleBuilderOperation operation();
/**
* This rule applies to any FHIR operation involving reading, including
* <code>read</code>, <code>vread</code>, <code>search</code>, and
@ -49,15 +82,24 @@ public interface IAuthRuleBuilderRule {
*/
IAuthRuleBuilderRuleTransaction transaction();
/**
* This rule applies to <code>update</code> operations with a <code>conditional</code>
* 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.
* <p>
* 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.
* </p>
*/
IAuthRuleBuilderRuleConditional updateConditional();
/**
* This rule applies to any FHIR operation involving writing, including
* <code>create</code>, and <code>update</code>
*/
IAuthRuleBuilderRuleOp write();
/**
* This rule applies to a FHIR operation (e.g. <code>$validate</code>)
*/
IAuthRuleBuilderOperation operation();
}

View File

@ -0,0 +1,5 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
public interface IAuthRuleBuilderRuleConditional extends IAuthRuleBuilderAppliesTo<IAuthRuleBuilderRuleConditionalClassifier> {
}

View File

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

View File

@ -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<? extends IBaseResource> theType);
/**
* Rule applies to all resources
*/
IAuthRuleBuilderRuleOpClassifier allResources();
public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthRuleBuilderRuleOpClassifier> {
// nothing
}

View File

@ -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<IAuthRule> build();
public interface IAuthRuleBuilderRuleOpClassifierFinished extends IAuthRuleFinished {
// nothing
}

View File

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

View File

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

View File

@ -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<? extends IBaseResource> next : myAppliesToTypes) {
String resName = ctx.getResourceDefinition(theRequestDetails.getResourceName()).getName();
String resName = ctx.getResourceDefinition(next).getName();
if (resName.equals(theRequestDetails.getResourceName())) {
applies = true;
break;

View File

@ -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<IAuthRule> myRules;
@ -39,7 +34,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
public RuleBuilder() {
myRules = new ArrayList<IAuthRule>();
}
@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<IAuthRule> 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<? extends IBaseResource> 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<? extends IIdType> 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<? extends IBaseResource> theType) {
Validate.notNull(theType, "theType must not be null");
OperationRule rule = createRule();
HashSet<Class<? extends IBaseResource>> appliesToTypes = new HashSet<Class<? extends IBaseResource>>();
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<IIdType> ids = new ArrayList<IIdType>();
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<? extends IBaseResource> theType) {
Validate.notNull(theType, "theType must not be null");
OperationRule rule = createRule();
HashSet<Class<? extends IBaseResource>> appliesToTypes = new HashSet<Class<? extends IBaseResource>>();
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();
}
}
}
}
}

View File

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

View File

@ -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<? extends IBaseResource> 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;
}

View File

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

View File

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

View File

@ -166,9 +166,12 @@ public abstract class BaseHapiFhirResourceDao<T extends IBaseResource> 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("");
}
}

View File

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

View File

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

View File

@ -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("<id value=\"" + id.getIdPart() + "\"/>"));
assertThat(respString, containsString("<versionId value=\"1\"/>"));
} 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("<id value=\"" + id.getIdPart() + "\"/>"));
assertThat(respString, containsString("<versionId value=\"2\"/>"));
} 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();
}

View File

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

View File

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

View File

@ -161,6 +161,21 @@
if it contained custom fields that also used custom
types. Thanks to GitHub user @sjanic for reporting!
</action>
<action type="add">
Servers in STU3 mode will now ignore any ID or VersionID found in the
resource body provided by the client when processing FHIR
<![CDATA[<code>update</code>]]> 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 <![CDATA[<code>@Update</code>]]> methods will always have
<![CDATA[<code>null</code>]]> ID
</action>
<action type="add">
Add new methods to
<![CDATA[<code>AuthorizationInterceptor</code>]]>
which allow user code to declare support for conditional
create, update, and delete.
</action>
</release>
<release version="1.6" date="2016-07-07">
<action type="fix">

View File

@ -91,8 +91,10 @@
<p>
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.
</p>
<p class="doc_info_bubble">
AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet
been heavily tested. Use with caution, and do lots of testing! We welcome