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(); return ordinal() > theVersion.ordinal();
} }
public boolean isOlderThan(FhirVersionEnum theVersion) {
return ordinal() < theVersion.ordinal();
}
/** /**
* Returns true if the given version is present on the classpath * 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) { if (getContext().getVersion().getVersion() == FhirVersionEnum.DSTU1) {
resource.setId(urlId); resource.setId(urlId);
} else { } else {
if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3) == false) {
resource.setId(theRequest.getId());
}
String matchUrl = null; String matchUrl = null;
if (myConditionalUrlIndex != -1) { if (myConditionalUrlIndex != -1) {
matchUrl = (String) theParams[myConditionalUrlIndex]; matchUrl = (String) theParams[myConditionalUrlIndex];

View File

@ -67,29 +67,7 @@ class ConditionalParamBinder implements IParameter {
@Override @Override
public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException { public Object translateQueryParametersIntoServerArgument(RequestDetails theRequest, BaseMethodBinding<?> theMethodBinding) throws InternalErrorException, InvalidRequestException {
return theRequest.getConditionalUrl(myOperationType);
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);
} }
} }

View File

@ -27,8 +27,10 @@ import java.util.Collections;
import java.util.Set; import java.util.Set;
import org.hl7.fhir.instance.model.api.IBaseResource; 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.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Create;
import ca.uhn.fhir.rest.api.RequestTypeEnum; 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); String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "idInUrlForCreate", theUrlId);
throw new InvalidRequestException(msg); throw new InvalidRequestException(msg);
} }
if (isNotBlank(theResourceId)) { if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "idInBodyForCreate", theResourceId); if (isNotBlank(theResourceId)) {
throw new InvalidRequestException(msg); 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; package ca.uhn.fhir.rest.method;
import static org.apache.commons.lang3.StringUtils.isBlank;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.Reader; 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.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; 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.IRestfulResponse;
import ca.uhn.fhir.rest.server.IRestfulServerDefaults; import ca.uhn.fhir.rest.server.IRestfulServerDefaults;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
@ -52,6 +55,7 @@ public abstract class RequestDetails {
private String myOperation; private String myOperation;
private Map<String, String[]> myParameters; private Map<String, String[]> myParameters;
private byte[] myRequestContents; private byte[] myRequestContents;
private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback();
private String myRequestPath; private String myRequestPath;
private RequestTypeEnum myRequestType; private RequestTypeEnum myRequestType;
private String myResourceName; private String myResourceName;
@ -59,7 +63,6 @@ public abstract class RequestDetails {
private IRestfulResponse myResponse; private IRestfulResponse myResponse;
private RestOperationTypeEnum myRestOperationType; private RestOperationTypeEnum myRestOperationType;
private String mySecondaryOperation; private String mySecondaryOperation;
private IRequestOperationCallback myRequestOperationCallback = new RequestOperationCallback();
private Map<String, List<String>> myUnqualifiedToQualifiedNames; private Map<String, List<String>> myUnqualifiedToQualifiedNames;
private Map<Object, Object> myUserData; private Map<Object, Object> myUserData;
protected abstract byte[] getByteStreamRequestContents(); protected abstract byte[] getByteStreamRequestContents();
@ -76,6 +79,40 @@ public abstract class RequestDetails {
return myCompleteUrl; 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 * 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; 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. * The part of the request URL that comes after the server base.
* <p> * <p>
@ -175,15 +221,6 @@ public abstract class RequestDetails {
*/ */
public abstract String getServerBaseForRequest(); 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() { public Map<String, List<String>> getUnqualifiedToQualifiedNames() {
return myUnqualifiedToQualifiedNames; return myUnqualifiedToQualifiedNames;
} }
@ -290,13 +327,20 @@ public abstract class RequestDetails {
public void setRestOperationType(RestOperationTypeEnum theRestOperationType) { public void setRestOperationType(RestOperationTypeEnum theRestOperationType) {
myRestOperationType = theRestOperationType; myRestOperationType = theRestOperationType;
} }
public void setSecondaryOperation(String theSecondaryOperation) { public void setSecondaryOperation(String theSecondaryOperation) {
mySecondaryOperation = theSecondaryOperation; mySecondaryOperation = theSecondaryOperation;
} }
private class RequestOperationCallback implements IRequestOperationCallback { private class RequestOperationCallback implements IRequestOperationCallback {
private List<IServerInterceptor> getInterceptors() {
if (getServer() == null) {
return Collections.emptyList();
}
return getServer().getInterceptors();
}
@Override @Override
public void resourceCreated(IBaseResource theResource) { public void resourceCreated(IBaseResource theResource) {
for (IServerInterceptor next : getInterceptors()) { 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 @Override
public void resourceDeleted(IBaseResource theResource) { public void resourceDeleted(IBaseResource theResource) {
for (IServerInterceptor next : getInterceptors()) { 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 @Override
public void resourcesCreated(Collection<? extends IBaseResource> theResource) { public void resourcesCreated(Collection<? extends IBaseResource> theResource) {
for (IBaseResource next : 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 not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -30,6 +30,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.model.api.IResource; import ca.uhn.fhir.model.api.IResource;
import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.annotation.Update; import ca.uhn.fhir.rest.annotation.Update;
@ -62,7 +63,8 @@ public class UpdateMethodBinding extends BaseOutcomeReturningMethodBindingWithRe
id.setValue(locationHeader); id.setValue(locationHeader);
if (isNotBlank(id.getResourceType())) { if (isNotBlank(id.getResourceType())) {
if (!getResourceName().equals(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"); String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "noIdInUrlForUpdate");
throw new InvalidRequestException(msg); throw new InvalidRequestException(msg);
} }
if (isBlank(theResourceId)) { if (getContext().getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
// String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "noIdInBodyForUpdate"); if (isBlank(theResourceId)) {
ourLog.warn("No resource ID found in resource body for update"); ourLog.warn("No resource ID found in resource body for update");
theResource.setId(theUrlId); theResource.setId(theUrlId);
} else { } else {
if (!theResourceId.equals(theUrlId)) { if (!theResourceId.equals(theUrlId)) {
String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "incorrectIdForUpdate", theResourceId, theUrlId); String msg = getContext().getLocalizer().getMessage(BaseOutcomeReturningMethodBindingWithResourceParam.class, "incorrectIdForUpdate", theResourceId, theUrlId);
throw new InvalidRequestException(msg); 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.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseBundle; import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource; 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.FhirContext;
import ca.uhn.fhir.model.api.Bundle; import ca.uhn.fhir.model.api.Bundle;
@ -78,8 +79,8 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
setDefaultPolicy(theDefaultPolicy); setDefaultPolicy(theDefaultPolicy);
} }
private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource) {
Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theOutputResource); Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource);
if (decision.getDecision() == PolicyEnum.ALLOW) { if (decision.getDecision() == PolicyEnum.ALLOW) {
return; return;
@ -89,13 +90,13 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
} }
@Override @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); List<IAuthRule> rules = buildRuleList(theRequestDetails);
ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation); ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation);
Verdict verdict = null; Verdict verdict = null;
for (IAuthRule nextRule : rules) { for (IAuthRule nextRule : rules) {
verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theOutputResource, this); verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theInputResourceId, theOutputResource, this);
if (verdict != null) { if (verdict != null) {
ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision()); ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision());
break; 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) { switch (theOperation) {
case ADD_TAGS: case ADD_TAGS:
case DELETE_TAGS: case DELETE_TAGS:
@ -147,6 +148,13 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
case CREATE: case CREATE:
case UPDATE: case UPDATE:
// if (theRequestResource != null) {
// if (theRequestResource.getIdElement() != null) {
// if (theRequestResource.getIdElement().hasIdPart() == false) {
// return OperationExamineDirection.IN_UNCATEGORIZED;
// }
// }
// }
return OperationExamineDirection.IN; return OperationExamineDirection.IN;
case META: case META:
@ -204,14 +212,26 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
} }
private void handleUserOperation(RequestDetails theRequest, IBaseResource theResource, RestOperationTypeEnum operation) { private void handleUserOperation(RequestDetails theRequest, IBaseResource theResource, RestOperationTypeEnum operation) {
applyRulesAndFailIfDeny(operation, theRequest, theResource, null); applyRulesAndFailIfDeny(operation, theRequest, theResource, theResource.getIdElement(), null);
} }
@Override @Override
public void incomingRequestPreHandled(RestOperationTypeEnum theOperation, ActionRequestDetails theProcessedRequest) { 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 IN:
case BOTH: case BOTH:
inputResource = theProcessedRequest.getResource();
inputResourceId = theProcessedRequest.getId();
break; break;
case NONE: case NONE:
case OUT: case OUT:
@ -219,7 +239,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
} }
RequestDetails requestDetails = theProcessedRequest.getRequestDetails(); RequestDetails requestDetails = theProcessedRequest.getRequestDetails();
applyRulesAndFailIfDeny(theOperation, requestDetails, theProcessedRequest.getResource(), null); applyRulesAndFailIfDeny(theOperation, requestDetails, inputResource, inputResourceId, null);
} }
@Override @Override
@ -236,7 +256,9 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
@Override @Override
public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) { public boolean outgoingResponse(RequestDetails theRequestDetails, IBaseResource theResponseObject) {
switch (determineOperationDirection(theRequestDetails.getRestOperationType())) { switch (determineOperationDirection(theRequestDetails.getRestOperationType(), null)) {
case IN_UNCATEGORIZED:
return true;
case IN: case IN:
case NONE: case NONE:
return true; return true;
@ -246,8 +268,6 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
} }
FhirContext fhirContext = theRequestDetails.getServer().getFhirContext(); FhirContext fhirContext = theRequestDetails.getServer().getFhirContext();
List<IAuthRule> rules = buildRuleList(theRequestDetails);
List<IBaseResource> resources = Collections.emptyList(); List<IBaseResource> resources = Collections.emptyList();
switch (theRequestDetails.getRestOperationType()) { switch (theRequestDetails.getRestOperationType()) {
@ -272,7 +292,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
} }
for (IBaseResource nextResponse : resources) { for (IBaseResource nextResponse : resources) {
applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, nextResponse); applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, null, nextResponse);
} }
return true; return true;
@ -334,9 +354,10 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer
private enum OperationExamineDirection { private enum OperationExamineDirection {
IN, IN,
IN_UNCATEGORIZED,
NONE, NONE,
OUT, OUT,
BOTH BOTH,
} }
public static class Verdict { public static class Verdict {

View File

@ -35,7 +35,7 @@ abstract class BaseRule implements IAuthRule {
return myName; return myName;
} }
public void setMode(PolicyEnum theRuleMode) { void setMode(PolicyEnum theRuleMode) {
myMode = theRuleMode; myMode = theRuleMode;
} }
@ -43,7 +43,7 @@ abstract class BaseRule implements IAuthRule {
return new Verdict(myMode, this); return new Verdict(myMode, this);
} }
public PolicyEnum getMode() { PolicyEnum getMode() {
return myMode; 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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.method.RequestDetails;
@ -37,6 +38,7 @@ public interface IAuthRule {
* The request * The request
* @param theInputResource * @param theInputResource
* The resource being input by the client, or <code>null</code> * The resource being input by the client, or <code>null</code>
* @param theInputResourceId TODO
* @param theOutputResource * @param theOutputResource
* The resource being returned by the server, or <code>null</code> * The resource being returned by the server, or <code>null</code>
* @param theRuleApplier * @param theRuleApplier
@ -44,7 +46,7 @@ public interface IAuthRule {
* nested objects in the request, such as nested requests in a transaction) * 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 * @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 * 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 { 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 * This rule applies to the FHIR delete operation
*/ */
IAuthRuleBuilderRuleOp delete(); 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 * This rules applies to the metadata operation (retrieve the
* server's conformance statement) * server's conformance statement)
@ -36,6 +64,11 @@ public interface IAuthRuleBuilderRule {
*/ */
IAuthRuleBuilderRuleOpClassifierFinished metadata(); 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 * This rule applies to any FHIR operation involving reading, including
* <code>read</code>, <code>vread</code>, <code>search</code>, and * <code>read</code>, <code>vread</code>, <code>search</code>, and
@ -49,15 +82,24 @@ public interface IAuthRuleBuilderRule {
*/ */
IAuthRuleBuilderRuleTransaction transaction(); 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 * This rule applies to any FHIR operation involving writing, including
* <code>create</code>, and <code>update</code> * <code>create</code>, and <code>update</code>
*/ */
IAuthRuleBuilderRuleOp write(); 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; package ca.uhn.fhir.rest.server.interceptor.auth;
/* public interface IAuthRuleBuilderRuleOp extends IAuthRuleBuilderAppliesTo<IAuthRuleBuilderRuleOpClassifier> {
* #%L // nothing
* 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();
} }

View File

@ -1,7 +1,5 @@
package ca.uhn.fhir.rest.server.interceptor.auth; package ca.uhn.fhir.rest.server.interceptor.auth;
import java.util.List;
/* /*
* #%L * #%L
* HAPI FHIR - Core Library * HAPI FHIR - Core Library
@ -22,15 +20,6 @@ import java.util.List;
* #L% * #L%
*/ */
public interface IAuthRuleBuilderRuleOpClassifierFinished { public interface IAuthRuleBuilderRuleOpClassifierFinished extends IAuthRuleFinished {
/** // nothing
* Start another rule
*/
IAuthRuleBuilder andThen();
/**
* Build the rule list
*/
List<IAuthRule> build();
} }

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.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.method.RequestDetails;
@ -28,6 +29,6 @@ import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict
public interface IRuleApplier { 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 @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(); FhirContext ctx = theRequestDetails.getServer().getFhirContext();
boolean applies = false; boolean applies = false;
@ -67,7 +67,7 @@ class OperationRule extends BaseRule implements IAuthRule {
case EXTENDED_OPERATION_TYPE: case EXTENDED_OPERATION_TYPE:
if (myAppliesToTypes != null) { if (myAppliesToTypes != null) {
for (Class<? extends IBaseResource> next : myAppliesToTypes) { for (Class<? extends IBaseResource> next : myAppliesToTypes) {
String resName = ctx.getResourceDefinition(theRequestDetails.getResourceName()).getName(); String resName = ctx.getResourceDefinition(next).getName();
if (resName.equals(theRequestDetails.getResourceName())) { if (resName.equals(theRequestDetails.getResourceName())) {
applies = true; applies = true;
break; break;

View File

@ -1,7 +1,5 @@
package ca.uhn.fhir.rest.server.interceptor.auth; package ca.uhn.fhir.rest.server.interceptor.auth;
import java.util.ArrayList;
/* /*
* #%L * #%L
* HAPI FHIR - Core Library * 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 not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -21,17 +19,14 @@ import java.util.ArrayList;
* limitations under the License. * limitations under the License.
* #L% * #L%
*/ */
import java.util.*;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource; import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType; import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
public class RuleBuilder implements IAuthRuleBuilder { public class RuleBuilder implements IAuthRuleBuilder {
private ArrayList<IAuthRule> myRules; private ArrayList<IAuthRule> myRules;
@ -39,7 +34,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
public RuleBuilder() { public RuleBuilder() {
myRules = new ArrayList<IAuthRule>(); myRules = new ArrayList<IAuthRule>();
} }
@Override @Override
public IAuthRuleBuilderRule allow() { public IAuthRuleBuilderRule allow() {
return allow(null); return allow(null);
@ -57,7 +52,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished allowAll(String theRuleName) { 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(); return new RuleBuilderFinished();
} }
@ -83,21 +78,30 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished denyAll(String theRuleName) { 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(); return new RuleBuilderFinished();
} }
private final class RuleBuilderFinished implements IAuthRuleBuilderRuleOpClassifierFinished { private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished {
@Override @Override
public IAuthRuleBuilder andThen() { public IAuthRuleBuilder andThen() {
doBuildRule();
return RuleBuilder.this; return RuleBuilder.this;
} }
@Override @Override
public List<IAuthRule> build() { public List<IAuthRule> build() {
doBuildRule();
return myRules; return myRules;
} }
/**
* Subclasses may override
*/
protected void doBuildRule() {
// nothing
}
} }
private class RuleBuilderRule implements IAuthRuleBuilderRule { private class RuleBuilderRule implements IAuthRuleBuilderRule {
@ -111,39 +115,100 @@ public class RuleBuilder implements IAuthRuleBuilder {
myRuleName = theRuleName; myRuleName = theRuleName;
} }
@Override
public IAuthRuleBuilderRuleConditional createConditional() {
return new RuleBuilderRuleConditional(RestOperationTypeEnum.CREATE);
}
@Override @Override
public IAuthRuleBuilderRuleOp delete() { public IAuthRuleBuilderRuleOp delete() {
myRuleOp = RuleOpEnum.DELETE; myRuleOp = RuleOpEnum.DELETE;
return new RuleBuilderRuleOp(); return new RuleBuilderRuleOp();
} }
@Override
public IAuthRuleBuilderRuleConditional deleteConditional() {
return new RuleBuilderRuleConditional(RestOperationTypeEnum.DELETE);
}
@Override @Override
public RuleBuilderFinished metadata() { public RuleBuilderFinished metadata() {
Rule rule = new Rule(myRuleName); RuleImplOp rule = new RuleImplOp(myRuleName);
rule.setOp(RuleOpEnum.METADATA); rule.setOp(RuleOpEnum.METADATA);
rule.setMode(myRuleMode); rule.setMode(myRuleMode);
myRules.add(rule); myRules.add(rule);
return new RuleBuilderFinished(); return new RuleBuilderFinished();
} }
@Override
public IAuthRuleBuilderOperation operation() {
return new RuleBuilderRuleOperation();
}
@Override @Override
public IAuthRuleBuilderRuleOp read() { public IAuthRuleBuilderRuleOp read() {
myRuleOp = RuleOpEnum.READ; myRuleOp = RuleOpEnum.READ;
return new RuleBuilderRuleOp(); return new RuleBuilderRuleOp();
} }
@Override @Override
public IAuthRuleBuilderRuleTransaction transaction() { public IAuthRuleBuilderRuleTransaction transaction() {
myRuleOp = RuleOpEnum.TRANSACTION; myRuleOp = RuleOpEnum.TRANSACTION;
return new RuleBuilderRuleTransaction(); return new RuleBuilderRuleTransaction();
} }
@Override
public IAuthRuleBuilderRuleConditional updateConditional() {
return new RuleBuilderRuleConditional(RestOperationTypeEnum.UPDATE);
}
@Override @Override
public IAuthRuleBuilderRuleOp write() { public IAuthRuleBuilderRuleOp write() {
myRuleOp = RuleOpEnum.WRITE; myRuleOp = RuleOpEnum.WRITE;
return new RuleBuilderRuleOp(); 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 class RuleBuilderRuleOp implements IAuthRuleBuilderRuleOp {
private AppliesTypeEnum myAppliesTo; private AppliesTypeEnum myAppliesTo;
@ -170,8 +235,8 @@ public class RuleBuilder implements IAuthRuleBuilder {
private Collection<? extends IIdType> myInCompartmentOwners; private Collection<? extends IIdType> myInCompartmentOwners;
private IAuthRuleBuilderRuleOpClassifierFinished finished() { private IAuthRuleBuilderRuleOpClassifierFinished finished() {
Rule rule = new Rule(myRuleName); RuleImplOp rule = new RuleImplOp(myRuleName);
rule.setMode(myRuleMode); rule.setMode(myRuleMode);
rule.setOp(myRuleOp); rule.setOp(myRuleOp);
rule.setAppliesTo(myAppliesTo); rule.setAppliesTo(myAppliesTo);
@ -180,7 +245,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
rule.setClassifierCompartmentName(myInCompartmentName); rule.setClassifierCompartmentName(myInCompartmentName);
rule.setClassifierCompartmentOwners(myInCompartmentOwners); rule.setClassifierCompartmentOwners(myInCompartmentOwners);
myRules.add(rule); myRules.add(rule);
return new RuleBuilderFinished(); return new RuleBuilderFinished();
} }
@ -222,29 +287,18 @@ public class RuleBuilder implements IAuthRuleBuilder {
} }
private class RuleBuilderRuleTransaction implements IAuthRuleBuilderRuleTransaction { private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation {
@Override @Override
public IAuthRuleBuilderRuleTransactionOp withAnyOperation() { public IAuthRuleBuilderOperationNamed named(String theOperationName) {
return new RuleBuilderRuleTransactionOp(); Validate.notBlank(theOperationName, "theOperationName must not be null or empty");
return new RuleBuilderRuleOperationNamed(theOperationName);
} }
private class RuleBuilderRuleTransactionOp implements IAuthRuleBuilderRuleTransactionOp {
@Override @Override
public IAuthRuleBuilderRuleOpClassifierFinished andApplyNormalRules() { public IAuthRuleBuilderOperationNamed withAnyName() {
Rule rule = new Rule(myRuleName); return new RuleBuilderRuleOperationNamed(null);
rule.setMode(myRuleMode);
rule.setOp(myRuleOp);
rule.setTransactionAppliesToOp(TransactionAppliesToEnum.ANY_OPERATION);
myRules.add(rule);
return new RuleBuilderFinished();
}
} }
}
private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation {
private class RuleBuilderRuleOperationNamed implements IAuthRuleBuilderOperationNamed { 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() { private OperationRule createRule() {
OperationRule rule = new OperationRule(myRuleName); OperationRule rule = new OperationRule(myRuleName);
rule.setOperationName(myOperationName); rule.setOperationName(myOperationName);
@ -273,24 +319,12 @@ public class RuleBuilder implements IAuthRuleBuilder {
return rule; 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 @Override
public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) { public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) {
Validate.notNull(theInstanceId, "theInstanceId must not be null"); Validate.notNull(theInstanceId, "theInstanceId must not be null");
Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type"); Validate.notBlank(theInstanceId.getResourceType(), "theInstanceId does not have a resource type");
Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part"); Validate.notBlank(theInstanceId.getIdPart(), "theInstanceId does not have an ID part");
OperationRule rule = createRule(); OperationRule rule = createRule();
ArrayList<IIdType> ids = new ArrayList<IIdType>(); ArrayList<IIdType> ids = new ArrayList<IIdType>();
ids.add(theInstanceId); ids.add(theInstanceId);
@ -298,26 +332,54 @@ public class RuleBuilder implements IAuthRuleBuilder {
myRules.add(rule); myRules.add(rule);
return new RuleBuilderFinished(); return new RuleBuilderFinished();
} }
} @Override
public IAuthRuleBuilderRuleOpClassifierFinished onServer() {
@Override OperationRule rule = createRule();
public IAuthRuleBuilderOperationNamed named(String theOperationName) { rule.appliesToServer();
Validate.notBlank(theOperationName, "theOperationName must not be null or empty"); myRules.add(rule);
return new RuleBuilderRuleOperationNamed(theOperationName); 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 private class RuleBuilderRuleTransaction implements IAuthRuleBuilderRuleTransaction {
public IAuthRuleBuilderOperation operation() {
return new RuleBuilderRuleOperation(); @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 not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -39,7 +39,7 @@ import ca.uhn.fhir.util.BundleUtil;
import ca.uhn.fhir.util.BundleUtil.BundleEntryParts; import ca.uhn.fhir.util.BundleUtil.BundleEntryParts;
import ca.uhn.fhir.util.FhirTerser; import ca.uhn.fhir.util.FhirTerser;
class Rule extends BaseRule implements IAuthRule { class RuleImplOp extends BaseRule implements IAuthRule {
private AppliesTypeEnum myAppliesTo; private AppliesTypeEnum myAppliesTo;
private Set<?> myAppliesToTypes; private Set<?> myAppliesToTypes;
@ -49,34 +49,37 @@ class Rule extends BaseRule implements IAuthRule {
private RuleOpEnum myOp; private RuleOpEnum myOp;
private TransactionAppliesToEnum myTransactionAppliesToOp; private TransactionAppliesToEnum myTransactionAppliesToOp;
public Rule(String theRuleName) { public RuleImplOp(String theRuleName) {
super(theRuleName); super(theRuleName);
} }
@Override @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(); FhirContext ctx = theRequestDetails.getServer().getFhirContext();
IBaseResource appliesTo; IBaseResource appliesToResource;
IIdType appliesToResourceId = null;
switch (myOp) { switch (myOp) {
case READ: case READ:
if (theOutputResource == null) { if (theOutputResource == null) {
return null; return null;
} }
appliesTo = theOutputResource; appliesToResource = theOutputResource;
break; break;
case WRITE: case WRITE:
if (theInputResource == null) { if (theInputResource == null && theInputResourceId == null) {
return null; return null;
} }
appliesTo = theInputResource; appliesToResource = theInputResource;
appliesToResourceId = theInputResourceId;
break; break;
case DELETE: case DELETE:
if (theOperation == RestOperationTypeEnum.DELETE) { if (theOperation == RestOperationTypeEnum.DELETE) {
if (theInputResource == null) { if (theInputResource == null) {
return newVerdict(); return newVerdict();
} else { } else {
appliesTo = theInputResource; appliesToResource = theInputResource;
} }
} else { } else {
return null; 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()); 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) { if (newVerdict == null) {
continue; continue;
} else if (verdict == null) { } else if (verdict == null) {
@ -133,7 +136,7 @@ class Rule extends BaseRule implements IAuthRule {
if (nextPart.getResource() == null) { if (nextPart.getResource() == null) {
continue; 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) { if (newVerdict == null) {
continue; continue;
} else if (verdict == null) { } else if (verdict == null) {
@ -165,8 +168,16 @@ class Rule extends BaseRule implements IAuthRule {
case ALL_RESOURCES: case ALL_RESOURCES:
break; break;
case TYPES: case TYPES:
if (myAppliesToTypes.contains(appliesTo.getClass()) == false) { if (appliesToResource != null) {
return 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; break;
default: default:
@ -180,9 +191,17 @@ class Rule extends BaseRule implements IAuthRule {
FhirTerser t = ctx.newTerser(); FhirTerser t = ctx.newTerser();
boolean foundMatch = false; boolean foundMatch = false;
for (IIdType next : myClassifierCompartmentOwners) { for (IIdType next : myClassifierCompartmentOwners) {
if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, appliesTo, next)) { if (appliesToResource != null) {
foundMatch = true; if (t.isSourceInCompartmentForTarget(myClassifierCompartmentName, appliesToResource, next)) {
break; foundMatch = true;
break;
}
}
if (appliesToResourceId != null && appliesToResourceId.hasResourceType() && appliesToResourceId.hasIdPart()) {
if (appliesToResourceId.toUnqualifiedVersionless().getValue().equals(next.toUnqualifiedVersionless().getValue())) {
foundMatch = true;
break;
}
} }
} }
if (!foundMatch) { if (!foundMatch) {
@ -196,7 +215,6 @@ class Rule extends BaseRule implements IAuthRule {
return newVerdict(); return newVerdict();
} }
private boolean requestAppliesToTransaction(FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) { private boolean requestAppliesToTransaction(FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) {
if (!"Bundle".equals(theContext.getResourceDefinition(theInputResource).getName())) { if (!"Bundle".equals(theContext.getResourceDefinition(theInputResource).getName())) {
return false; return false;
@ -238,8 +256,7 @@ class Rule extends BaseRule implements IAuthRule {
myClassifierType = theClassifierType; myClassifierType = theClassifierType;
} }
public RuleImplOp setOp(RuleOpEnum theRuleOp) {
public Rule setOp(RuleOpEnum theRuleOp) {
myOp = theRuleOp; myOp = theRuleOp;
return this; return this;
} }

View File

@ -386,9 +386,9 @@ public class AbstractJaxRsResourceProviderDstu3Test {
when(mock.update(idCaptor.capture(), patientCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome()); 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(); 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()); assertEquals("Patient?identifier=2&_format=json", conditionalCaptor.getValue());
compareResultId(1, patientCaptor.getValue());
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")

View File

@ -70,7 +70,6 @@ import ca.uhn.fhir.util.TestUtil;
@FixMethodOrder(MethodSorters.NAME_ASCENDING) @FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class AbstractJaxRsResourceProviderTest { public class AbstractJaxRsResourceProviderTest {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AbstractJaxRsResourceProviderTest.class);
private static IGenericClient client; private static IGenericClient client;
@ -372,9 +371,8 @@ public class AbstractJaxRsResourceProviderTest {
when(mock.update(idCaptor.capture(), patientCaptor.capture(), conditionalCaptor.capture())).thenReturn(new MethodOutcome()); 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(); 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()); assertEquals("Patient?identifier=2&_format=json", conditionalCaptor.getValue());
compareResultId(1, patientCaptor.getValue());
} }
@SuppressWarnings("unchecked") @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()); String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedNumericId", theResource.getIdElement().getIdPart());
throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing")); 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()); String message = getContext().getLocalizer().getMessage(BaseHapiFhirResourceDao.class, "failedToCreateWithClientAssignedId", theResource.getIdElement().getIdPart());
throw new InvalidRequestException(message, createErrorOperationOutcome(message, "processing")); 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 @Test
public void testCreateTextIdFails() { public void testCreateTextIdDoesntFail() {
Patient p = new Patient(); Patient p = new Patient();
p.addIdentifier().setSystem("urn:system").setValue("testCreateTextIdFails"); p.addIdentifier().setSystem("urn:system").setValue("testCreateTextIdFails");
p.addName().addFamily("Hello"); p.addName().addFamily("Hello");
p.setId("Patient/ABC"); p.setId("Patient/ABC");
try { String id = myPatientDao.create(p, mySrd).getId().getIdPart();
myPatientDao.create(p, mySrd); assertNotEquals("ABC", id);
fail();
} catch (InvalidRequestException e) {
assertThat(e.getMessage(), containsString("Can not create resource with ID[ABC], ID must not be supplied"));
}
} }
@Test @Test
@ -678,20 +674,6 @@ public class FhirResourceDaoDstu3Test extends BaseJpaDstu3Test {
assertThat(toUnqualifiedVersionlessIdValues(myCarePlanDao.search(params)), empty()); 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 @Test
public void testCreateBundleAllowsDocumentAndCollection() { public void testCreateBundleAllowsDocumentAndCollection() {
String methodName = "testCreateBundleAllowsDocumentAndCollection"; String methodName = "testCreateBundleAllowsDocumentAndCollection";

View File

@ -70,6 +70,7 @@ public class AuthorizationInterceptorResourceProviderDstu3Test extends BaseResou
//@formatter:off //@formatter:off
return new RuleBuilder() return new RuleBuilder()
.allow("Rule 2").write().allResources().inCompartment("Patient", new IdDt("Patient/" + output1.getId().getIdPart())).andThen() .allow("Rule 2").write().allResources().inCompartment("Patient", new IdDt("Patient/" + output1.getId().getIdPart())).andThen()
.allow().updateConditional().allResources()
.build(); .build();
//@formatter:on //@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 @Test
public void testCreateResourceConditionalComplex() throws IOException { public void testCreateResourceConditionalComplex() throws IOException {
Patient pt = new Patient(); Patient pt = new Patient();
@ -2493,9 +2594,7 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
put.addHeader("Accept", Constants.CT_FHIR_JSON); put.addHeader("Accept", Constants.CT_FHIR_JSON);
CloseableHttpResponse response = ourHttpClient.execute(put); CloseableHttpResponse response = ourHttpClient.execute(put);
try { try {
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(200, 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());
} finally { } finally {
response.close(); response.close();
} }
@ -2722,16 +2821,15 @@ public class ResourceProviderDstu3Test extends BaseResourceProviderDstu3Test {
pt.addName().addFamily(methodName); pt.addName().addFamily(methodName);
String resource = myFhirCtx.newXmlParser().encodeResourceToString(pt); 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"))); post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
CloseableHttpResponse response = ourHttpClient.execute(post); CloseableHttpResponse response = ourHttpClient.execute(post);
try { try {
String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8); String responseString = IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info(responseString); ourLog.info(responseString);
assertEquals(400, response.getStatusLine().getStatusCode()); assertEquals(201, response.getStatusLine().getStatusCode());
OperationOutcome oo = myFhirCtx.newXmlParser().parseResource(OperationOutcome.class, responseString); assertThat(responseString, containsString("/A2/"));
assertThat(oo.getIssue().get(0).getDiagnostics(), containsString( assertThat(responseString, not(containsString("333")));
"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\""));
} finally { } finally {
response.close(); response.close();
} }

View File

@ -307,6 +307,8 @@ public abstract class BaseResource extends BaseElement implements IResource {
myId = (IdDt) theId; myId = (IdDt) theId;
} else if (theId != null) { } else if (theId != null) {
myId = new IdDt(theId.getValue()); myId = new IdDt(theId.getValue());
} else {
myId = null;
} }
return this; return this;
} }

View File

@ -1,7 +1,9 @@
package ca.uhn.fhir.rest.server; 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 java.util.concurrent.TimeUnit;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
@ -25,7 +27,6 @@ import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import ca.uhn.fhir.context.FhirContext; 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.ConditionalUrlParam;
import ca.uhn.fhir.rest.annotation.IdParam; import ca.uhn.fhir.rest.annotation.IdParam;
import ca.uhn.fhir.rest.annotation.ResourceParam; import ca.uhn.fhir.rest.annotation.ResourceParam;
@ -52,7 +53,7 @@ public class UpdateDstu3Test {
HttpResponse status = ourClient.execute(httpPost); 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()); IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent); ourLog.info("Response was:\n{}", responseContent);
@ -78,7 +79,7 @@ public class UpdateDstu3Test {
CloseableHttpResponse status = ourClient.execute(httpPost); CloseableHttpResponse status = ourClient.execute(httpPost);
try { try {
String responseContent = IOUtils.toString(status.getEntity().getContent()); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response was:\n{}", responseContent); ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
@ -101,7 +102,7 @@ public class UpdateDstu3Test {
CloseableHttpResponse status = ourClient.execute(httpPost); CloseableHttpResponse status = ourClient.execute(httpPost);
try { try {
String responseContent = IOUtils.toString(status.getEntity().getContent()); String responseContent = IOUtils.toString(status.getEntity().getContent(), StandardCharsets.UTF_8);
ourLog.info("Response was:\n{}", responseContent); ourLog.info("Response was:\n{}", responseContent);
assertEquals(200, status.getStatusLine().getStatusCode()); assertEquals(200, status.getStatusLine().getStatusCode());
@ -117,28 +118,22 @@ public class UpdateDstu3Test {
public void testUpdateWrongUrlInBody() throws Exception { public void testUpdateWrongUrlInBody() throws Exception {
Patient patient = new Patient(); Patient patient = new Patient();
patient.setId("3"); patient.setId("Patient/3/_history/4");
patient.addIdentifier().setValue("002"); 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"))); httpPost.setEntity(new StringEntity(ourCtx.newXmlParser().encodeResourceToString(patient), ContentType.create(Constants.CT_FHIR_XML, "UTF-8")));
HttpResponse status = ourClient.execute(httpPost); 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()); IOUtils.closeQuietly(status.getEntity().getContent());
ourLog.info("Response was:\n{}", responseContent); ourLog.info("Response was:\n{}", responseContent);
OperationOutcome oo = ourCtx.newXmlParser().parseResource(OperationOutcome.class, responseContent); assertEquals(200, status.getStatusLine().getStatusCode());
assertEquals( assertEquals("http://localhost:" + ourPort + "/Patient/1/_history/002", status.getFirstHeader("location").getValue());
"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\"", assertEquals("Patient/1/_history/2", ourId.getValue());
oo.getIssue().get(0).getDiagnostics());
assertEquals(400, status.getStatusLine().getStatusCode());
assertNull(status.getFirstHeader("location"));
assertNull(status.getFirstHeader("content-location"));
} }
@AfterClass @AfterClass

View File

@ -161,6 +161,21 @@
if it contained custom fields that also used custom if it contained custom fields that also used custom
types. Thanks to GitHub user @sjanic for reporting! types. Thanks to GitHub user @sjanic for reporting!
</action> </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>
<release version="1.6" date="2016-07-07"> <release version="1.6" date="2016-07-07">
<action type="fix"> <action type="fix">

View File

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