From 3f2b5fdeb754e6ed2f459a563eea76f4fe29b89b Mon Sep 17 00:00:00 2001 From: jamesagnew Date: Sat, 16 Apr 2016 10:22:43 -0400 Subject: [PATCH] Get security interceptor working --- .../interceptor/IServerInterceptor.java | 10 + .../auth/AuthorizationInterceptor.java | 62 ++-- .../server/interceptor/auth/IAuthRule.java | 8 +- .../auth/IAuthRuleBuilderRule.java | 5 + .../server/interceptor/auth/IRuleApplier.java | 13 + .../rest/server/interceptor/auth/Rule.java | 111 +++++++- .../server/interceptor/auth/RuleBuilder.java | 22 +- .../server/interceptor/auth/RuleOpEnum.java | 3 +- .../interceptor/auth/RuleVerdictEnum.java | 27 -- .../java/ca/uhn/fhir/util/BundleUtil.java | 118 ++++++-- .../ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java | 7 +- ...nInterceptorResourceProviderDstu3Test.java | 267 ++++++++++++++++++ .../AuthorizationInterceptorDstu2Test.java | 115 ++++++-- 13 files changed, 635 insertions(+), 133 deletions(-) create mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java delete mode 100644 hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleVerdictEnum.java create mode 100644 hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java index 112bf609aa7..68cfa96b7e7 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/IServerInterceptor.java @@ -441,6 +441,16 @@ public interface IServerInterceptor { myResource = theObject; } + /** + * This method may be invoked by user code to notify interceptors that a nested + * operation is being invoked which is denoted by this request details. + */ + public void notifyIncomingRequestPreHandled(RestOperationTypeEnum theOperationType) { + for (IServerInterceptor next : getRequestDetails().getServer().getInterceptors()) { + next.incomingRequestPreHandled(theOperationType, this); + } + } + } } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java index 826b63d41a5..922adb5b1e1 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.java @@ -39,6 +39,7 @@ import ca.uhn.fhir.model.api.TagList; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; import ca.uhn.fhir.rest.server.interceptor.IServerOperationInterceptor; import ca.uhn.fhir.rest.server.interceptor.InterceptorAdapter; import ca.uhn.fhir.util.BundleUtil; @@ -49,7 +50,7 @@ import ca.uhn.fhir.util.CoverageIgnore; * inspect requests and responses to determine whether the calling user * has permission to perform the given action. */ -public class AuthorizationInterceptor extends InterceptorAdapter implements IServerOperationInterceptor { +public class AuthorizationInterceptor extends InterceptorAdapter implements IServerOperationInterceptor, IRuleApplier { private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AuthorizationInterceptor.class); @@ -72,9 +73,8 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer setDefaultPolicy(theDefaultPolicy); } - private void applyRulesAndFailIfDeny(List theRules, RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { - ourLog.trace("Applying {} rules to render an auth decision for operation {}", theRules.size(), theOperation); - Verdict decision = applyRulesAndReturnDecision(theRules, theOperation, theRequestDetails, theInputResource, theOutputResource); + private void applyRulesAndFailIfDeny(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { + Verdict decision = applyRulesAndReturnDecision(theOperation, theRequestDetails, theInputResource, theOutputResource); if (decision.getDecision() == PolicyEnum.ALLOW) { return; @@ -83,38 +83,26 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer handleDeny(decision); } - private Verdict applyRulesAndReturnDecision(List theRules, RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { - PolicyEnum result = null; -// PolicyEnum preference = null; - IAuthRule decidingRule = null; - - for (IAuthRule nextRule : theRules) { - RuleVerdictEnum decision = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theOutputResource); - - switch (decision) { - case NO_DECISION: - continue; - case ALLOW: - result = PolicyEnum.ALLOW; - decidingRule = nextRule; - break; - case DENY: - result = PolicyEnum.DENY; - decidingRule = nextRule; - break; - } - - if (result != null) { - ourLog.trace("Rule {} returned decision {}", nextRule, result); + @Override + public Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { + List rules = buildRuleList(theRequestDetails); + ourLog.trace("Applying {} rules to render an auth decision for operation {}", rules.size(), theOperation); + + Verdict verdict = null; + for (IAuthRule nextRule : rules) { + verdict = nextRule.applyRule(theOperation, theRequestDetails, theInputResource, theOutputResource, this); + if (verdict != null) { + ourLog.trace("Rule {} returned decision {}", nextRule, verdict.getDecision()); break; } } - if (result == null) { + if (verdict == null) { ourLog.trace("No rules returned a decision, applying default {}", myDefaultPolicy); - result = myDefaultPolicy; + return new Verdict(myDefaultPolicy, null); } - return new Verdict(result, decidingRule); + + return verdict; } /** @@ -197,22 +185,21 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer * Handle an access control verdict of {@link PolicyEnum#DENY}. *

* Subclasses may override to implement specific behaviour, but default is to - * throw {@link AuthenticationException} (HTTP 401) with error message citing the + * throw {@link ForbiddenOperationException} (HTTP 403) with error message citing the * rule name which trigered failure *

*/ protected void handleDeny(Verdict decision) { if (decision.getDecidingRule() != null) { String ruleName = defaultString(decision.getDecidingRule().getName(), "(unnamed rule)"); - throw new AuthenticationException("Access denied by rule: " + ruleName); + throw new ForbiddenOperationException("Access denied by rule: " + ruleName); } else { - throw new AuthenticationException("Access denied by default policy (no applicable rules)"); + throw new ForbiddenOperationException("Access denied by default policy (no applicable rules)"); } } private void handleUserOperation(RequestDetails theRequest, IBaseResource theResource, RestOperationTypeEnum operation) { - List rules = buildRuleList(theRequest); - applyRulesAndFailIfDeny(rules, operation, theRequest, theResource, null); + applyRulesAndFailIfDeny(operation, theRequest, theResource, null); } @Override @@ -227,8 +214,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } RequestDetails requestDetails = theProcessedRequest.getRequestDetails(); - List rules = buildRuleList(requestDetails); - applyRulesAndFailIfDeny(rules, theOperation, requestDetails, theProcessedRequest.getResource(), null); + applyRulesAndFailIfDeny(theOperation, requestDetails, theProcessedRequest.getResource(), null); } @Override @@ -281,7 +267,7 @@ public class AuthorizationInterceptor extends InterceptorAdapter implements ISer } for (IBaseResource nextResponse : resources) { - applyRulesAndFailIfDeny(rules, theRequestDetails.getRestOperationType(), theRequestDetails, null, nextResponse); + applyRulesAndFailIfDeny(theRequestDetails.getRestOperationType(), theRequestDetails, null, nextResponse); } return true; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java index 6dfddda0fbb..2e49d2caadb 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRule.java @@ -24,6 +24,7 @@ import org.hl7.fhir.instance.model.api.IBaseResource; 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 interface IAuthRule { @@ -38,9 +39,12 @@ public interface IAuthRule { * The resource being input by the client, or null * @param theOutputResource * The resource being returned by the server, or null - * @return Returns a policy decision, or {@link RuleVerdictEnum#NO_DECISION} if the rule does not apply + * @param theRuleApplier + * The rule applying module (this can be used by rules to apply the rule set to + * nested objects in the request, such as nested requests in a transaction) + * @return Returns a policy decision, or null if the rule does not apply */ - RuleVerdictEnum applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource); + Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier); /** * Returns a name for this rule, to be used in logs and error messages diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java index d4925bb6dcc..725aff15ea2 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IAuthRuleBuilderRule.java @@ -22,6 +22,11 @@ package ca.uhn.fhir.rest.server.interceptor.auth; public interface IAuthRuleBuilderRule { + /** + * This rule applies to the FHIR delete operation + */ + IAuthRuleBuilderRuleOp delete(); + /** * This rules applies to the metadata operation (retrieve the * server's conformance statement) diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java new file mode 100644 index 00000000000..cb4dcadcfd3 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/IRuleApplier.java @@ -0,0 +1,13 @@ +package ca.uhn.fhir.rest.server.interceptor.auth; + +import org.hl7.fhir.instance.model.api.IBaseResource; + +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 interface IRuleApplier { + + Verdict applyRulesAndReturnDecision(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource); + +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java index ed2ec222634..26e29efce77 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/Rule.java @@ -21,16 +21,23 @@ package ca.uhn.fhir.rest.server.interceptor.auth; */ import java.util.Collection; +import java.util.List; import java.util.Set; +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.context.RuntimeResourceDefinition; +import ca.uhn.fhir.rest.api.RequestTypeEnum; import ca.uhn.fhir.rest.api.RestOperationTypeEnum; import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict; import ca.uhn.fhir.util.BundleUtil; +import ca.uhn.fhir.util.BundleUtil.BundleEntryParts; import ca.uhn.fhir.util.FhirTerser; class Rule implements IAuthRule { @@ -40,7 +47,7 @@ class Rule implements IAuthRule { private String myClassifierCompartmentName; private Collection myClassifierCompartmentOwners; private ClassifierTypeEnum myClassifierType; - private RuleVerdictEnum myMode; + private PolicyEnum myMode; private String myName; private RuleOpEnum myOp; private TransactionAppliesToEnum myTransactionAppliesToOp; @@ -50,33 +57,107 @@ class Rule implements IAuthRule { } @Override - public RuleVerdictEnum applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource) { + public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IBaseResource theOutputResource, IRuleApplier theRuleApplier) { FhirContext ctx = theRequestDetails.getServer().getFhirContext(); IBaseResource appliesTo; switch (myOp) { case READ: + if (theOutputResource == null) { + return null; + } appliesTo = theOutputResource; break; case WRITE: + if (theInputResource == null) { + return null; + } appliesTo = theInputResource; break; + case DELETE: + if (theOperation == RestOperationTypeEnum.DELETE) { + if (theInputResource == null) { + return new Verdict(myMode, this); + } else { + appliesTo = theInputResource; + } + } else { + return null; + } + break; case BATCH: case TRANSACTION: - if (requestAppliesToTransaction(ctx, myOp, theInputResource)) { - return myMode; + if (theInputResource != null && requestAppliesToTransaction(ctx, myOp, theInputResource)) { + if (myMode == PolicyEnum.DENY) { + return new Verdict(PolicyEnum.DENY, this); + } else { + List inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource); + Verdict verdict = null; + for (BundleEntryParts nextPart : inputResources) { + + IBaseResource inputResource = nextPart.getResource(); + RestOperationTypeEnum operation = null; + if (nextPart.getRequestType() == RequestTypeEnum.GET) { + continue; + } + if (nextPart.getRequestType() == RequestTypeEnum.POST) { + operation = RestOperationTypeEnum.CREATE; + } else if (nextPart.getRequestType() == RequestTypeEnum.PUT) { + operation = RestOperationTypeEnum.UPDATE; + } else { + throw new InvalidRequestException("Can not handle transaction with operation of type " + nextPart.getRequestType()); + } + + /* + * This is basically just being conservative - Be careful of transactions containing + * nested operations and nested transactions. We block the by default. At some point + * it would be nice to be more nuanced here. + */ + RuntimeResourceDefinition resourceDef = ctx.getResourceDefinition(nextPart.getResource()); + if ("Parameters".equals(resourceDef.getName()) || "Bundle".equals(resourceDef.getName())) { + throw new InvalidRequestException("Can not handle transaction with nested resource of type " + resourceDef.getName()); + } + + Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(operation, theRequestDetails, inputResource, null); + if (newVerdict == null) { + continue; + } else if (verdict == null) { + verdict = newVerdict; + } else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) { + verdict = newVerdict; + } + } + return verdict; + } + } else if (theOutputResource != null) { + List inputResources = BundleUtil.toListOfEntries(ctx, (IBaseBundle) theInputResource); + Verdict verdict = null; + for (BundleEntryParts nextPart : inputResources) { + if (nextPart.getResource() == null) { + continue; + } + Verdict newVerdict = theRuleApplier.applyRulesAndReturnDecision(RestOperationTypeEnum.READ, theRequestDetails, null, nextPart.getResource()); + if (newVerdict == null) { + continue; + } else if (verdict == null) { + verdict = newVerdict; + } else if (verdict.getDecision() == PolicyEnum.ALLOW && newVerdict.getDecision() == PolicyEnum.DENY) { + verdict = newVerdict; + } + } + return verdict; } else { - return RuleVerdictEnum.NO_DECISION; + return null; } case ALLOW_ALL: - return RuleVerdictEnum.ALLOW; + return new Verdict(PolicyEnum.ALLOW, this); case DENY_ALL: - return RuleVerdictEnum.DENY; + return new Verdict(PolicyEnum.DENY, this); case METADATA: if (theOperation == RestOperationTypeEnum.METADATA) { - return myMode; + return new Verdict(myMode, this); } else { - return RuleVerdictEnum.NO_DECISION; + return null; } default: // Should not happen @@ -88,7 +169,7 @@ class Rule implements IAuthRule { break; case TYPES: if (myAppliesToTypes.contains(appliesTo.getClass()) == false) { - return RuleVerdictEnum.NO_DECISION; + return null; } break; default: @@ -108,17 +189,21 @@ class Rule implements IAuthRule { } } if (!foundMatch) { - return RuleVerdictEnum.NO_DECISION; + return null; } break; default: throw new IllegalStateException("Unable to apply security to event of applies to type " + myAppliesTo); } - return myMode; + return new Verdict(myMode, this); } private boolean requestAppliesToTransaction(FhirContext theContext, RuleOpEnum theOp, IBaseResource theInputResource) { + if (!"Bundle".equals(theContext.getResourceDefinition(theInputResource).getName())) { + return false; + } + IBaseBundle request = (IBaseBundle) theInputResource; String bundleType = BundleUtil.getBundleType(theContext, request); switch (theOp) { @@ -160,7 +245,7 @@ class Rule implements IAuthRule { myClassifierType = theClassifierType; } - public void setMode(RuleVerdictEnum theRuleMode) { + public void setMode(PolicyEnum theRuleMode) { myMode = theRuleMode; } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java index 08a2a27bcc6..3f1027dd15f 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleBuilder.java @@ -46,7 +46,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRule allow(String theRuleName) { - return new RuleBuilderRule(RuleVerdictEnum.ALLOW, theRuleName); + return new RuleBuilderRule(PolicyEnum.ALLOW, theRuleName); } @Override @@ -72,7 +72,7 @@ public class RuleBuilder implements IAuthRuleBuilder { @Override public IAuthRuleBuilderRule deny(String theRuleName) { - return new RuleBuilderRule(RuleVerdictEnum.DENY, theRuleName); + return new RuleBuilderRule(PolicyEnum.DENY, theRuleName); } @Override @@ -101,15 +101,21 @@ public class RuleBuilder implements IAuthRuleBuilder { private class RuleBuilderRule implements IAuthRuleBuilderRule { - private RuleOpEnum myRuleOp; - private RuleVerdictEnum myRuleMode; + private PolicyEnum myRuleMode; private String myRuleName; + private RuleOpEnum myRuleOp; - public RuleBuilderRule(RuleVerdictEnum theRuleMode, String theRuleName) { + public RuleBuilderRule(PolicyEnum theRuleMode, String theRuleName) { myRuleMode = theRuleMode; myRuleName = theRuleName; } + @Override + public IAuthRuleBuilderRuleOp delete() { + myRuleOp = RuleOpEnum.DELETE; + return new RuleBuilderRuleOp(); + } + @Override public RuleBuilderFinished metadata() { Rule rule = new Rule(myRuleName); @@ -118,13 +124,13 @@ public class RuleBuilder implements IAuthRuleBuilder { myRules.add(rule); return new RuleBuilderFinished(); } - + @Override public IAuthRuleBuilderRuleOp read() { myRuleOp = RuleOpEnum.READ; return new RuleBuilderRuleOp(); } - + @Override public IAuthRuleBuilderRuleTransaction transaction() { myRuleOp = RuleOpEnum.TRANSACTION; @@ -136,7 +142,7 @@ public class RuleBuilder implements IAuthRuleBuilder { myRuleOp = RuleOpEnum.WRITE; return new RuleBuilderRuleOp(); } - + private class RuleBuilderRuleOp implements IAuthRuleBuilderRuleOp { private AppliesTypeEnum myAppliesTo; diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java index 52c1671b546..3d6296424c5 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleOpEnum.java @@ -27,5 +27,6 @@ enum RuleOpEnum { DENY_ALL, TRANSACTION, METADATA, - BATCH + BATCH, + DELETE } diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleVerdictEnum.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleVerdictEnum.java deleted file mode 100644 index b4d79325ac1..00000000000 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/server/interceptor/auth/RuleVerdictEnum.java +++ /dev/null @@ -1,27 +0,0 @@ -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% - */ - -enum RuleVerdictEnum { - ALLOW, - DENY, - NO_DECISION -} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java index 4cde0019218..23c61e72c6d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/BundleUtil.java @@ -1,5 +1,7 @@ package ca.uhn.fhir.util; +import static org.apache.commons.lang3.StringUtils.isNotBlank; + /* * #%L * HAPI FHIR - Core Library @@ -33,33 +35,13 @@ import ca.uhn.fhir.context.BaseRuntimeChildDefinition; import ca.uhn.fhir.context.BaseRuntimeElementCompositeDefinition; import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.RuntimeResourceDefinition; +import ca.uhn.fhir.rest.api.RequestTypeEnum; /** * Fetch resources from a bundle */ public class BundleUtil { - /** - * Extract all of the resources from a given bundle - */ - public static List toListOfResources(FhirContext theContext, IBaseBundle theBundle) { - List retVal = new ArrayList(); - - RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); - BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); - List entries = entryChild.getAccessor().getValues(theBundle); - - BaseRuntimeElementCompositeDefinition entryChildElem = (BaseRuntimeElementCompositeDefinition) entryChild.getChildByName("entry"); - BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); - for (IBase nextEntry : entries) { - for (IBase next : resourceChild.getAccessor().getValues(nextEntry)) { - retVal.add((IBaseResource) next); - } - } - - return retVal; - } - @SuppressWarnings("unchecked") public static List> getBundleEntryUrlsAndResources(FhirContext theContext, IBaseBundle theBundle) { RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); @@ -96,7 +78,7 @@ public class BundleUtil { return retVal; } - + public static String getBundleType(FhirContext theContext, IBaseBundle theBundle) { RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); BaseRuntimeChildDefinition entryChild = def.getChildByName("type"); @@ -108,4 +90,96 @@ public class BundleUtil { return null; } + /** + * Extract all of the resources from a given bundle + */ + public static List toListOfEntries(FhirContext theContext, IBaseBundle theBundle) { + List retVal = new ArrayList(); + + RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); + BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); + List entries = entryChild.getAccessor().getValues(theBundle); + + BaseRuntimeElementCompositeDefinition entryChildElem = (BaseRuntimeElementCompositeDefinition) entryChild.getChildByName("entry"); + + BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); + BaseRuntimeChildDefinition requestChild = entryChildElem.getChildByName("request"); + BaseRuntimeElementCompositeDefinition requestElem = (BaseRuntimeElementCompositeDefinition) requestChild.getChildByName("request"); + BaseRuntimeChildDefinition urlChild = requestElem.getChildByName("url"); + BaseRuntimeChildDefinition methodChild = requestElem.getChildByName("method"); + + IBaseResource resource = null; + String url = null; + RequestTypeEnum requestType = null; + + for (IBase nextEntry : entries) { + for (IBase next : resourceChild.getAccessor().getValues(nextEntry)) { + resource = (IBaseResource) next; + } + for (IBase nextRequest : requestChild.getAccessor().getValues(nextEntry)) { + for (IBase nextUrl : urlChild.getAccessor().getValues(nextRequest)) { + url = ((IPrimitiveType)nextUrl).getValueAsString(); + } + for (IBase nextUrl : methodChild.getAccessor().getValues(nextRequest)) { + String methodString = ((IPrimitiveType)nextUrl).getValueAsString(); + if (isNotBlank(methodString)) { + requestType = RequestTypeEnum.valueOf(methodString); + } + } + } + + /* + * All 3 might be null - That's ok because we still want to know the + * order in the original bundle. + */ + retVal.add(new BundleEntryParts(requestType, url, resource)); + } + + + return retVal; + } + + /** + * Extract all of the resources from a given bundle + */ + public static List toListOfResources(FhirContext theContext, IBaseBundle theBundle) { + List retVal = new ArrayList(); + + RuntimeResourceDefinition def = theContext.getResourceDefinition(theBundle); + BaseRuntimeChildDefinition entryChild = def.getChildByName("entry"); + List entries = entryChild.getAccessor().getValues(theBundle); + + BaseRuntimeElementCompositeDefinition entryChildElem = (BaseRuntimeElementCompositeDefinition) entryChild.getChildByName("entry"); + BaseRuntimeChildDefinition resourceChild = entryChildElem.getChildByName("resource"); + for (IBase nextEntry : entries) { + for (IBase next : resourceChild.getAccessor().getValues(nextEntry)) { + retVal.add((IBaseResource) next); + } + } + + return retVal; + } + + public static class BundleEntryParts + { + private final RequestTypeEnum myRequestType; + private final IBaseResource myResource; + private final String myUrl; + public BundleEntryParts(RequestTypeEnum theRequestType, String theUrl, IBaseResource theResource) { + super(); + myRequestType = theRequestType; + myUrl = theUrl; + myResource = theResource; + } + public RequestTypeEnum getRequestType() { + return myRequestType; + } + public IBaseResource getResource() { + return myResource; + } + public String getUrl() { + return myUrl; + } + } + } diff --git a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java index 5302d9a705c..915d4636c6c 100644 --- a/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java +++ b/hapi-fhir-jpaserver-base/src/main/java/ca/uhn/fhir/jpa/dao/BaseHapiFhirDao.java @@ -665,18 +665,21 @@ public abstract class BaseHapiFhirDao implements IDao { theProvider.setSearchResultDao(mySearchResultDao); } - protected void notifyInterceptors(RestOperationTypeEnum operationType, ActionRequestDetails requestDetails) { + protected void notifyInterceptors(RestOperationTypeEnum theOperationType, ActionRequestDetails requestDetails) { if (requestDetails.getId() != null && requestDetails.getId().hasResourceType() && isNotBlank(requestDetails.getResourceType())) { if (requestDetails.getId().getResourceType().equals(requestDetails.getResourceType()) == false) { throw new InternalErrorException("Inconsistent server state - Resource types don't match: " + requestDetails.getId().getResourceType() + " / " + requestDetails.getResourceType()); } } + + requestDetails.notifyIncomingRequestPreHandled(theOperationType); + List interceptors = getConfig().getInterceptors(); if (interceptors == null) { return; } for (IServerInterceptor next : interceptors) { - next.incomingRequestPreHandled(operationType, requestDetails); + next.incomingRequestPreHandled(theOperationType, requestDetails); } } diff --git a/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java new file mode 100644 index 00000000000..2a565cade14 --- /dev/null +++ b/hapi-fhir-jpaserver-base/src/test/java/ca/uhn/fhir/jpa/provider/dstu3/AuthorizationInterceptorResourceProviderDstu3Test.java @@ -0,0 +1,267 @@ +package ca.uhn.fhir.jpa.provider.dstu3; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.containsInRelativeOrder; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.hasItems; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.stringContainsInOrder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.hl7.fhir.dstu3.model.BaseResource; +import org.hl7.fhir.dstu3.model.Bundle; +import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent; +import org.hl7.fhir.dstu3.model.Bundle.BundleType; +import org.hl7.fhir.dstu3.model.Bundle.HTTPVerb; +import org.hl7.fhir.dstu3.model.Bundle.SearchEntryMode; +import org.hl7.fhir.dstu3.model.CodeSystem; +import org.hl7.fhir.dstu3.model.Coding; +import org.hl7.fhir.dstu3.model.Condition; +import org.hl7.fhir.dstu3.model.DateTimeType; +import org.hl7.fhir.dstu3.model.DateType; +import org.hl7.fhir.dstu3.model.Device; +import org.hl7.fhir.dstu3.model.DiagnosticOrder; +import org.hl7.fhir.dstu3.model.DocumentManifest; +import org.hl7.fhir.dstu3.model.DocumentReference; +import org.hl7.fhir.dstu3.model.Encounter; +import org.hl7.fhir.dstu3.model.Encounter.EncounterClass; +import org.hl7.fhir.dstu3.model.Encounter.EncounterLocationComponent; +import org.hl7.fhir.dstu3.model.Encounter.EncounterState; +import org.hl7.fhir.dstu3.model.Enumerations.AdministrativeGender; +import org.hl7.fhir.dstu3.model.IdType; +import org.hl7.fhir.dstu3.model.ImagingStudy; +import org.hl7.fhir.dstu3.model.InstantType; +import org.hl7.fhir.dstu3.model.Location; +import org.hl7.fhir.dstu3.model.Medication; +import org.hl7.fhir.dstu3.model.MedicationOrder; +import org.hl7.fhir.dstu3.model.Meta; +import org.hl7.fhir.dstu3.model.Narrative.NarrativeStatus; +import org.hl7.fhir.dstu3.model.Observation; +import org.hl7.fhir.dstu3.model.OperationOutcome; +import org.hl7.fhir.dstu3.model.Organization; +import org.hl7.fhir.dstu3.model.Parameters; +import org.hl7.fhir.dstu3.model.Patient; +import org.hl7.fhir.dstu3.model.Period; +import org.hl7.fhir.dstu3.model.Practitioner; +import org.hl7.fhir.dstu3.model.Questionnaire; +import org.hl7.fhir.dstu3.model.Questionnaire.QuestionnaireItemType; +import org.hl7.fhir.dstu3.model.QuestionnaireResponse; +import org.hl7.fhir.dstu3.model.Reference; +import org.hl7.fhir.dstu3.model.StringType; +import org.hl7.fhir.dstu3.model.Subscription; +import org.hl7.fhir.dstu3.model.Subscription.SubscriptionChannelType; +import org.hl7.fhir.dstu3.model.Subscription.SubscriptionStatus; +import org.hl7.fhir.dstu3.model.TemporalPrecisionEnum; +import org.hl7.fhir.dstu3.model.UnsignedIntType; +import org.hl7.fhir.dstu3.model.ValueSet; +import org.hl7.fhir.instance.model.api.IBaseResource; +import org.hl7.fhir.instance.model.api.IIdType; +import org.junit.AfterClass; +import org.junit.Ignore; +import org.junit.Test; + +import ca.uhn.fhir.jpa.dao.SearchParameterMap; +import ca.uhn.fhir.model.primitive.IdDt; +import ca.uhn.fhir.model.primitive.UriDt; +import ca.uhn.fhir.rest.api.MethodOutcome; +import ca.uhn.fhir.rest.api.SummaryEnum; +import ca.uhn.fhir.rest.client.IGenericClient; +import ca.uhn.fhir.rest.method.RequestDetails; +import ca.uhn.fhir.rest.param.DateRangeParam; +import ca.uhn.fhir.rest.param.ParamPrefixEnum; +import ca.uhn.fhir.rest.param.StringAndListParam; +import ca.uhn.fhir.rest.param.StringOrListParam; +import ca.uhn.fhir.rest.param.StringParam; +import ca.uhn.fhir.rest.param.TokenParam; +import ca.uhn.fhir.rest.server.Constants; +import ca.uhn.fhir.rest.server.IBundleProvider; +import ca.uhn.fhir.rest.server.exceptions.AuthenticationException; +import ca.uhn.fhir.rest.server.exceptions.ForbiddenOperationException; +import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException; +import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException; +import ca.uhn.fhir.rest.server.exceptions.ResourceGoneException; +import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException; +import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor; +import ca.uhn.fhir.rest.server.interceptor.auth.IAuthRule; +import ca.uhn.fhir.rest.server.interceptor.auth.PolicyEnum; +import ca.uhn.fhir.rest.server.interceptor.auth.RuleBuilder; +import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails; +import ca.uhn.fhir.util.TestUtil; +import ca.uhn.fhir.util.UrlUtil; + +public class AuthorizationInterceptorResourceProviderDstu3Test extends BaseResourceProviderDstu3Test { + + private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(AuthorizationInterceptorResourceProviderDstu3Test.class); + + @AfterClass + public static void afterClassClearContext() { + TestUtil.clearAllStaticFieldsForUnitTest(); + } + + + @Override + public void before() throws Exception { + super.before(); + myDaoConfig.setAllowMultipleDelete(true); + unregisterInterceptors(); + } + + + private void unregisterInterceptors() { + for (IServerInterceptor next : new ArrayList(ourRestServer.getInterceptors())) { + if (next instanceof AuthorizationInterceptor) { + ourRestServer.unregisterInterceptor(next); + } + } + } + + + @Test + public void testCreateConditional() { + + Patient patient = new Patient(); + patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); + patient.addName().addFamily("Tester").addGiven("Raghad"); + final MethodOutcome output1 = ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute(); + + ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 2").write().allResources().inCompartment("Patient", new IdDt("Patient/" + output1.getId().getIdPart())).andThen() + .build(); + //@formatter:on + } + }); + + patient = new Patient(); + patient.setId(output1.getId().toUnqualifiedVersionless()); + patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); + patient.addName().addFamily("Tester").addGiven("Raghad"); + MethodOutcome output2 = ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|100").execute(); + + assertEquals(output1.getId().getIdPart(), output2.getId().getIdPart()); + + patient = new Patient(); + patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); + patient.addName().addFamily("Tester").addGiven("Raghad"); + try { + ourClient.update().resource(patient).conditionalByUrl("Patient?identifier=http://uhn.ca/mrns|101").execute(); + fail(); + } catch (ForbiddenOperationException e) { + assertEquals("HTTP 403 Forbidden: Access denied by default policy (no applicable rules)", e.getMessage()); + } + + patient = new Patient(); + patient.setId("999"); + patient.addIdentifier().setSystem("http://uhn.ca/mrns").setValue("100"); + patient.addName().addFamily("Tester").addGiven("Raghad"); + try { + ourClient.update().resource(patient).execute(); + fail(); + } catch (ForbiddenOperationException e) { + assertEquals("HTTP 403 Forbidden: Access denied by default policy (no applicable rules)", e.getMessage()); + } + + } + + @Test + public void testDeleteResourceConditional() throws IOException { + String methodName = "testDeleteResourceConditional"; + + Patient pt = new Patient(); + pt.addName().addFamily(methodName); + String resource = myFhirCtx.newXmlParser().encodeResourceToString(pt); + + HttpPost post = new HttpPost(ourServerBase + "/Patient"); + post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + CloseableHttpResponse response = ourHttpClient.execute(post); + final IdType id; + try { + 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(); + } + + pt = new Patient(); + pt.addName().addFamily("FOOFOOFOO"); + resource = myFhirCtx.newXmlParser().encodeResourceToString(pt); + + post = new HttpPost(ourServerBase + "/Patient"); + post.setEntity(new StringEntity(resource, ContentType.create(Constants.CT_FHIR_XML, "UTF-8"))); + response = ourHttpClient.execute(post); + final IdType id2; + try { + assertEquals(201, response.getStatusLine().getStatusCode()); + String newIdString = response.getFirstHeader(Constants.HEADER_LOCATION_LC).getValue(); + assertThat(newIdString, startsWith(ourServerBase + "/Patient/")); + id2 = new IdType(newIdString); + } finally { + response.close(); + } + + ourRestServer.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 2").delete().allResources().inCompartment("Patient", new IdDt("Patient/" + id.getIdPart())).andThen() + .build(); + //@formatter:on + } + }); + + HttpDelete delete = new HttpDelete(ourServerBase + "/Patient?name=" + methodName); + response = ourHttpClient.execute(delete); + try { + assertEquals(204, response.getStatusLine().getStatusCode()); + } finally { + response.close(); + } + + delete = new HttpDelete(ourServerBase + "/Patient?name=FOOFOOFOO"); + response = ourHttpClient.execute(delete); + try { + assertEquals(403, response.getStatusLine().getStatusCode()); + } finally { + response.close(); + } + + } + +} diff --git a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java index fd026afbd72..6293b970142 100644 --- a/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java +++ b/hapi-fhir-structures-dstu2/src/test/java/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptorDstu2Test.java @@ -42,6 +42,7 @@ import ca.uhn.fhir.model.dstu2.resource.Bundle; import ca.uhn.fhir.model.dstu2.resource.Observation; import ca.uhn.fhir.model.dstu2.resource.Patient; import ca.uhn.fhir.model.dstu2.valueset.BundleTypeEnum; +import ca.uhn.fhir.model.dstu2.valueset.HTTPVerbEnum; import ca.uhn.fhir.model.primitive.IdDt; import ca.uhn.fhir.rest.annotation.Create; import ca.uhn.fhir.rest.annotation.Delete; @@ -152,7 +153,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by rule: Rule 1")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); } @@ -189,7 +190,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by rule: Default Rule")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); } @@ -219,8 +220,7 @@ public class AuthorizationInterceptorDstu2Test { } @Test - @Ignore // not done yet - public void testBatchWhenTransactionAllowed() throws Exception { + public void testBatchWhenOnlyTransactionAllowed() throws Exception { ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { @Override public List buildRuleList(RequestDetails theRequestDetails) { @@ -236,7 +236,7 @@ public class AuthorizationInterceptorDstu2Test { Bundle input = new Bundle(); input.setType(BundleTypeEnum.BATCH); - input.addEntry().setResource(createPatient(1)).getRequest().setUrl("/Patient"); + input.addEntry().setResource(createPatient(1)).getRequest().setUrl("/Patient").setMethod(HTTPVerbEnum.POST); Bundle output = new Bundle(); output.setType(BundleTypeEnum.TRANSACTION_RESPONSE); @@ -252,7 +252,80 @@ public class AuthorizationInterceptorDstu2Test { httpPost.setEntity(createFhirResourceEntity(input)); status = ourClient.execute(httpPost); extractResponseAndClose(status); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); + } + + @Test + public void testBatchWhenTransactionReadDenied() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").transaction().withAnyOperation().andApplyNormalRules().andThen() + .allow("Rule 2").write().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").read().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen() + .build(); + //@formatter:on + } + }); + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.BATCH); + input.addEntry().setResource(createPatient(1)).getRequest().setUrl("/Patient").setMethod(HTTPVerbEnum.POST); + + Bundle output = new Bundle(); + output.setType(BundleTypeEnum.TRANSACTION_RESPONSE); + output.addEntry().setResource(createPatient(2)); + + HttpPost httpPost; + HttpResponse status; + String response; + + ourReturn = Arrays.asList((IResource)output); + ourHitMethod = false; + httpPost = new HttpPost("http://localhost:" + ourPort + "/"); + httpPost.setEntity(createFhirResourceEntity(input)); + status = ourClient.execute(httpPost); + extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + } + + @Test + public void testBatchWhenTransactionWrongBundleType() throws Exception { + ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) { + @Override + public List buildRuleList(RequestDetails theRequestDetails) { + //@formatter:off + return new RuleBuilder() + .allow("Rule 1").transaction().withAnyOperation().andApplyNormalRules().andThen() + .allow("Rule 2").write().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen() + .allow("Rule 2").read().allResources().inCompartment("Patient", new IdDt("Patient/1")).andThen() + .build(); + //@formatter:on + } + }); + + Bundle input = new Bundle(); + input.setType(BundleTypeEnum.COLLECTION); + input.addEntry().setResource(createPatient(1)).getRequest().setUrl("/Patient").setMethod(HTTPVerbEnum.POST); + + Bundle output = new Bundle(); + output.setType(BundleTypeEnum.TRANSACTION_RESPONSE); + output.addEntry().setResource(createPatient(1)); + + HttpPost httpPost; + HttpResponse status; + String response; + + ourReturn = Arrays.asList((IResource)output); + ourHitMethod = false; + httpPost = new HttpPost("http://localhost:" + ourPort + "/"); + httpPost.setEntity(createFhirResourceEntity(input)); + status = ourClient.execute(httpPost); + response = extractResponseAndClose(status); + assertEquals(403, status.getStatusLine().getStatusCode()); + assertThat(response, containsString("Invalid transaction bundle type: collection")); } @Test @@ -277,7 +350,7 @@ public class AuthorizationInterceptorDstu2Test { httpGet = new HttpGet("http://localhost:" + ourPort + "/metadata"); status = ourClient.execute(httpGet); extractResponseAndClose(status); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); } @Test @@ -312,7 +385,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourReturn = Arrays.asList(createPatient(1), createObservation(10, "Patient/2")); @@ -322,7 +395,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourReturn = Arrays.asList(createPatient(2), createObservation(10, "Patient/1")); @@ -332,7 +405,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); } @@ -405,7 +478,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourReturn = Arrays.asList(createObservation(10, "Patient/2")); @@ -415,7 +488,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourReturn = Arrays.asList(createPatient(1), createObservation(10, "Patient/2")); @@ -425,7 +498,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourReturn = Arrays.asList(createPatient(2), createObservation(10, "Patient/1")); @@ -435,7 +508,7 @@ public class AuthorizationInterceptorDstu2Test { response = extractResponseAndClose(status); ourLog.info(response); assertThat(response, containsString("Access denied by default policy (no applicable rules)")); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); } @@ -458,7 +531,7 @@ public class AuthorizationInterceptorDstu2Test { Bundle input = new Bundle(); input.setType(BundleTypeEnum.TRANSACTION); - input.addEntry().setResource(createPatient(1)).getRequest().setUrl("/Patient"); + input.addEntry().setResource(createPatient(1)).getRequest().setUrl("/Patient").setMethod(HTTPVerbEnum.PUT); Bundle output = new Bundle(); output.setType(BundleTypeEnum.TRANSACTION_RESPONSE); @@ -500,7 +573,7 @@ public class AuthorizationInterceptorDstu2Test { status = ourClient.execute(httpPost); String response = extractResponseAndClose(status); assertEquals("Access denied by default policy (no applicable rules)", response); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertFalse(ourHitMethod); ourHitMethod = false; @@ -509,7 +582,7 @@ public class AuthorizationInterceptorDstu2Test { status = ourClient.execute(httpPost); response = extractResponseAndClose(status); assertEquals("Access denied by default policy (no applicable rules)", response); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertFalse(ourHitMethod); ourHitMethod = false; @@ -543,7 +616,7 @@ public class AuthorizationInterceptorDstu2Test { httpDelete = new HttpDelete("http://localhost:" + ourPort + "/Patient/2"); status = ourClient.execute(httpDelete); extractResponseAndClose(status); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertTrue(ourHitMethod); ourHitMethod = false; @@ -578,7 +651,7 @@ public class AuthorizationInterceptorDstu2Test { status = ourClient.execute(httpPost); String response = extractResponseAndClose(status); assertEquals("Access denied by default policy (no applicable rules)", response); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertFalse(ourHitMethod); ourHitMethod = false; @@ -603,10 +676,12 @@ public class AuthorizationInterceptorDstu2Test { status = ourClient.execute(httpPost); response = extractResponseAndClose(status); assertEquals("Access denied by default policy (no applicable rules)", response); - assertEquals(401, status.getStatusLine().getStatusCode()); + assertEquals(403, status.getStatusLine().getStatusCode()); assertFalse(ourHitMethod); } + + @AfterClass public static void afterClassClearContext() throws Exception {