Version bump to 3.3.0-SNAPSHOT

This commit is contained in:
James Agnew 2018-01-23 10:30:43 -05:00
parent 4fd3e20d06
commit 16038ece26
58 changed files with 873 additions and 465 deletions

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-standalone-overlay-example</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -17,6 +17,7 @@ import ca.uhn.fhir.rest.server.exceptions.AuthenticationException;
import ca.uhn.fhir.rest.server.interceptor.IServerInterceptor;
import ca.uhn.fhir.rest.server.interceptor.auth.*;
@SuppressWarnings("unused")
public class AuthorizationInterceptors {
public class PatientResourceProvider implements IResourceProvider
@ -35,7 +36,8 @@ public class AuthorizationInterceptors {
}
//START SNIPPET: patientAndAdmin
public class PatientAndAdminAuthorizationInterceptor extends AuthorizationInterceptor {
@SuppressWarnings("ConstantConditions")
public class PatientAndAdminAuthorizationInterceptor extends AuthorizationInterceptor {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
@ -127,5 +129,16 @@ public class AuthorizationInterceptors {
}
//END SNIPPET: conditionalUpdate
public void authorizeTenantAction() {
//START SNIPPET: authorizeTenantAction
new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow().read().resourcesOfType(Patient.class).withAnyId().forTenantIds("TENANTA").andThen()
.build();
}
};
//END SNIPPET: authorizeTenantAction
}
}

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-cli</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -26,42 +26,42 @@
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-hl7org-dstu2</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu2.1</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-dstu3</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-structures-r4</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu2</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-validation-resources-dstu3</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<optional>true</optional>
</dependency>
</dependencies>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<artifactId>hapi-deployable-pom</artifactId>
<groupId>ca.uhn.hapi.fhir</groupId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@ -11,7 +11,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
@ -494,6 +494,17 @@
<groupId>de.juplo</groupId>
<artifactId>hibernate-maven-plugin</artifactId>
<executions>
<execution>
<id>derby107</id>
<phase>process-classes</phase>
<goals>
<goal>create</goal>
</goals>
<configuration>
<dialect>org.hibernate.dialect.DerbyTenSevenDialect</dialect>
<outputFile>${project.build.directory}/classes/ca/uhn/hapi/fhir/jpa/docs/database/persistence_create_derby107.sql</outputFile>
</configuration>
</execution>
<execution>
<id>postgres94</id>
<phase>process-classes</phase>

View File

@ -10,7 +10,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,6 +20,15 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* #L%
*/
import java.util.Collection;
public interface IAuthRuleBuilderRuleOpClassifierFinished extends IAuthRuleFinished {
// nothing
IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(String... theTenantId);
IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(Collection<String> theTenantId);
IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(String... theTenantIds);
IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(Collection<String> theTenantIds);
}

View File

@ -0,0 +1,25 @@
package ca.uhn.fhir.rest.server.interceptor.auth;
/*
* #%L
* HAPI FHIR - Server Framework
* %%
* Copyright (C) 2014 - 2018 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 IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId extends IAuthRuleFinished {
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,23 +20,19 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* #L%
*/
import java.util.HashSet;
import java.util.List;
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.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.HashSet;
import java.util.List;
class OperationRule extends BaseRule implements IAuthRule {
public OperationRule(String theRuleName) {
super(theRuleName);
}
private RuleBuilder.ITenantApplicabilityChecker myTenentApplicabilityChecker;
private String myOperationName;
private boolean myAppliesToServer;
private HashSet<Class<? extends IBaseResource>> myAppliesToTypes;
@ -44,70 +40,92 @@ class OperationRule extends BaseRule implements IAuthRule {
private HashSet<Class<? extends IBaseResource>> myAppliesToInstancesOfType;
private boolean myAppliesToAnyType;
private boolean myAppliesToAnyInstance;
/**
* Must include the leading $
*/
public void setOperationName(String theOperationName) {
myOperationName = theOperationName;
public OperationRule(String theRuleName) {
super(theRuleName);
}
public String getOperationName() {
return myOperationName;
public void appliesToAnyInstance() {
myAppliesToAnyInstance = true;
}
public void appliesToAnyType() {
myAppliesToAnyType = true;
}
public void appliesToInstances(List<IIdType> theAppliesToIds) {
myAppliesToIds = theAppliesToIds;
}
public void appliesToInstancesOfType(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
myAppliesToInstancesOfType = theAppliesToTypes;
}
public void appliesToServer() {
myAppliesToServer = true;
}
public void appliesToTypes(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
myAppliesToTypes = theAppliesToTypes;
}
@Override
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource, IRuleApplier theRuleApplier) {
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
if (myTenentApplicabilityChecker != null) {
if (!myTenentApplicabilityChecker.applies(theRequestDetails)) {
return null;
}
}
boolean applies = false;
switch (theOperation) {
case EXTENDED_OPERATION_SERVER:
if (myAppliesToServer) {
applies = true;
}
break;
case EXTENDED_OPERATION_TYPE:
if (myAppliesToAnyType) {
applies = true;
} else if (myAppliesToTypes != null) {
// TODO: Convert to a map of strings and keep the result
for (Class<? extends IBaseResource> next : myAppliesToTypes) {
String resName = ctx.getResourceDefinition(next).getName();
if (resName.equals(theRequestDetails.getResourceName())) {
applies = true;
break;
}
case EXTENDED_OPERATION_SERVER:
if (myAppliesToServer) {
applies = true;
}
}
break;
case EXTENDED_OPERATION_INSTANCE:
if (myAppliesToAnyInstance) {
applies = true;
} else if (theInputResourceId != null) {
if (myAppliesToIds != null) {
String instanceId = theInputResourceId.toUnqualifiedVersionless().getValue();
for (IIdType next : myAppliesToIds) {
if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) {
applies = true;
break;
}
}
}
if (myAppliesToInstancesOfType != null) {
break;
case EXTENDED_OPERATION_TYPE:
if (myAppliesToAnyType) {
applies = true;
} else if (myAppliesToTypes != null) {
// TODO: Convert to a map of strings and keep the result
for (Class<? extends IBaseResource> next : myAppliesToInstancesOfType) {
for (Class<? extends IBaseResource> next : myAppliesToTypes) {
String resName = ctx.getResourceDefinition(next).getName();
if (resName.equals(theInputResourceId.getResourceType())) {
if (resName.equals(theRequestDetails.getResourceName())) {
applies = true;
break;
}
}
}
}
break;
default:
return null;
break;
case EXTENDED_OPERATION_INSTANCE:
if (myAppliesToAnyInstance) {
applies = true;
} else if (theInputResourceId != null) {
if (myAppliesToIds != null) {
String instanceId = theInputResourceId.toUnqualifiedVersionless().getValue();
for (IIdType next : myAppliesToIds) {
if (next.toUnqualifiedVersionless().getValue().equals(instanceId)) {
applies = true;
break;
}
}
}
if (myAppliesToInstancesOfType != null) {
// TODO: Convert to a map of strings and keep the result
for (Class<? extends IBaseResource> next : myAppliesToInstancesOfType) {
String resName = ctx.getResourceDefinition(next).getName();
if (resName.equals(theInputResourceId.getResourceType())) {
applies = true;
break;
}
}
}
}
break;
default:
return null;
}
if (!applies) {
@ -121,28 +139,19 @@ class OperationRule extends BaseRule implements IAuthRule {
return newVerdict();
}
public void appliesToServer() {
myAppliesToServer = true;
public String getOperationName() {
return myOperationName;
}
public void appliesToTypes(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
myAppliesToTypes = theAppliesToTypes;
/**
* Must include the leading $
*/
public void setOperationName(String theOperationName) {
myOperationName = theOperationName;
}
public void appliesToInstances(List<IIdType> theAppliesToIds) {
myAppliesToIds = theAppliesToIds;
}
public void appliesToInstancesOfType(HashSet<Class<? extends IBaseResource>> theAppliesToTypes) {
myAppliesToInstancesOfType = theAppliesToTypes;
}
public void appliesToAnyInstance() {
myAppliesToAnyInstance = true;
}
public void appliesToAnyType() {
myAppliesToAnyType = true;
public void setTenentApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenentApplicabilityChecker) {
myTenentApplicabilityChecker = theTenentApplicabilityChecker;
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -19,21 +19,25 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* limitations under the License.
* #L%
*/
import java.util.*;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import org.apache.commons.lang3.Validate;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import ca.uhn.fhir.model.primitive.IdDt;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import java.util.*;
import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
public class RuleBuilder implements IAuthRuleBuilder {
public static final String[] EMPTY_STRING_ARRAY = new String[0];
private ArrayList<IAuthRule> myRules;
public RuleBuilder() {
myRules = new ArrayList<IAuthRule>();
myRules = new ArrayList<>();
}
@Override
@ -53,8 +57,9 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override
public IAuthRuleBuilderRuleOpClassifierFinished allowAll(String theRuleName) {
myRules.add(new RuleImplOp(theRuleName).setOp(RuleOpEnum.ALLOW_ALL));
return new RuleBuilderFinished();
RuleImplOp rule = new RuleImplOp(theRuleName);
myRules.add(rule.setOp(RuleOpEnum.ALLOW_ALL));
return new RuleBuilderFinished(rule);
}
@Override
@ -79,11 +84,26 @@ public class RuleBuilder implements IAuthRuleBuilder {
@Override
public IAuthRuleBuilderRuleOpClassifierFinished denyAll(String theRuleName) {
myRules.add(new RuleImplOp(theRuleName).setOp(RuleOpEnum.DENY_ALL));
return new RuleBuilderFinished();
RuleImplOp rule = new RuleImplOp(theRuleName);
myRules.add(rule.setOp(RuleOpEnum.DENY_ALL));
return new RuleBuilderFinished(rule);
}
private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished {
private class RuleBuilderFinished implements IAuthRuleFinished, IAuthRuleBuilderRuleOpClassifierFinished, IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId {
private final RuleImplOp myOpRule;
private final OperationRule myOperationRule;
protected ITenantApplicabilityChecker myTenantApplicabilityChecker;
RuleBuilderFinished(RuleImplOp theRule) {
myOpRule = theRule;
myOperationRule = null;
}
public RuleBuilderFinished(OperationRule theRule) {
myOpRule = null;
myOperationRule = theRule;
}
@Override
public IAuthRuleBuilder andThen() {
@ -103,6 +123,43 @@ public class RuleBuilder implements IAuthRuleBuilder {
protected void doBuildRule() {
// nothing
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(String... theTenantIds) {
return forTenantIds(Arrays.asList(defaultIfNull(theTenantIds, EMPTY_STRING_ARRAY)));
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId forTenantIds(final Collection<String> theTenantIds) {
myTenantApplicabilityChecker = new ITenantApplicabilityChecker(){
@Override
public boolean applies(RequestDetails theRequest) {
return theTenantIds.contains(theRequest.getTenantId());
}
};
if (myOpRule != null) {
myOpRule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
}
if (myOperationRule != null) {
myOperationRule.setTenentApplicabilityChecker(myTenantApplicabilityChecker);
}
return this;
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(String... theTenantIds) {
return notForTenantIds(Arrays.asList(defaultIfNull(theTenantIds, EMPTY_STRING_ARRAY)));
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinishedWithTenantId notForTenantIds(Collection<String> theTenantIds) {
return null;// TODO: implement method body
}
}
public interface ITenantApplicabilityChecker
{
boolean applies(RequestDetails theRequest);
}
private class RuleBuilderRule implements IAuthRuleBuilderRule {
@ -111,7 +168,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
private String myRuleName;
private RuleOpEnum myRuleOp;
public RuleBuilderRule(PolicyEnum theRuleMode, String theRuleName) {
RuleBuilderRule(PolicyEnum theRuleMode, String theRuleName) {
myRuleMode = theRuleMode;
myRuleName = theRuleName;
}
@ -138,7 +195,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
rule.setOp(RuleOpEnum.METADATA);
rule.setMode(myRuleMode);
myRules.add(rule);
return new RuleBuilderFinished();
return new RuleBuilderFinished(rule);
}
@Override
@ -176,7 +233,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
private Set<?> myAppliesToTypes;
private RestOperationTypeEnum myOperationType;
public RuleBuilderRuleConditional(RestOperationTypeEnum theOperationType) {
RuleBuilderRuleConditional(RestOperationTypeEnum theOperationType) {
myOperationType = theOperationType;
}
@ -196,6 +253,10 @@ public class RuleBuilder implements IAuthRuleBuilder {
public class RuleBuilderRuleConditionalClassifier extends RuleBuilderFinished implements IAuthRuleBuilderRuleConditionalClassifier {
public RuleBuilderRuleConditionalClassifier() {
super((RuleImplOp) null);
}
@Override
protected void doBuildRule() {
RuleImplConditional rule = new RuleImplConditional(myRuleName);
@ -203,6 +264,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
rule.setOperationType(myOperationType);
rule.setAppliesTo(myAppliesTo);
rule.setAppliesToTypes(myAppliesToTypes);
rule.setTenantApplicabilityChecker(myTenantApplicabilityChecker);
myRules.add(rule);
}
@ -221,6 +283,21 @@ public class RuleBuilder implements IAuthRuleBuilder {
return new RuleBuilderRuleOpClassifier();
}
@Override
public IAuthRuleFinished instance(String theId) {
Validate.notBlank(theId, "theId must not be null or empty");
return instance(new IdDt(theId));
}
@Override
public IAuthRuleFinished instance(IIdType theId) {
Validate.notNull(theId, "theId must not be null");
Validate.notBlank(theId.getValue(), "theId.getValue() must not be null or empty");
Validate.notBlank(theId.getIdPart(), "theId must contain an ID part");
return new RuleBuilderRuleOpClassifier(Arrays.asList(theId)).finished();
}
@Override
public IAuthRuleBuilderRuleOpClassifier resourcesOfType(Class<? extends IBaseResource> theType) {
Validate.notNull(theType, "theType must not be null");
@ -239,14 +316,14 @@ public class RuleBuilder implements IAuthRuleBuilder {
/**
* Constructor
*/
public RuleBuilderRuleOpClassifier() {
RuleBuilderRuleOpClassifier() {
super();
}
/**
* Constructor
*/
public RuleBuilderRuleOpClassifier(List<IIdType> theAppliesToInstances) {
RuleBuilderRuleOpClassifier(List<IIdType> theAppliesToInstances) {
myAppliesToInstances = theAppliesToInstances;
myAppliesTo = AppliesTypeEnum.INSTANCES;
}
@ -264,7 +341,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
rule.setClassifierCompartmentOwners(myInCompartmentOwners);
myRules.add(rule);
return new RuleBuilderFinished();
return new RuleBuilderFinished(rule);
}
@Override
@ -303,21 +380,6 @@ public class RuleBuilder implements IAuthRuleBuilder {
}
@Override
public IAuthRuleFinished instance(String theId) {
Validate.notBlank(theId, "theId must not be null or empty");
return instance(new IdDt(theId));
}
@Override
public IAuthRuleFinished instance(IIdType theId) {
Validate.notNull(theId, "theId must not be null");
Validate.notBlank(theId.getValue(), "theId.getValue() must not be null or empty");
Validate.notBlank(theId.getIdPart(), "theId must contain an ID part");
return new RuleBuilderRuleOpClassifier(Arrays.asList(theId)).finished();
}
}
private class RuleBuilderRuleOperation implements IAuthRuleBuilderOperation {
@ -352,6 +414,22 @@ public class RuleBuilder implements IAuthRuleBuilder {
return rule;
}
@Override
public IAuthRuleFinished onAnyInstance() {
OperationRule rule = createRule();
rule.appliesToAnyInstance();
myRules.add(rule);
return new RuleBuilderFinished(rule);
}
@Override
public IAuthRuleFinished onAnyType() {
OperationRule rule = createRule();
rule.appliesToAnyType();
myRules.add(rule);
return new RuleBuilderFinished(rule);
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished onInstance(IIdType theInstanceId) {
Validate.notNull(theInstanceId, "theInstanceId must not be null");
@ -363,29 +441,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
ids.add(theInstanceId);
rule.appliesToInstances(ids);
myRules.add(rule);
return new RuleBuilderFinished();
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished onServer() {
OperationRule rule = createRule();
rule.appliesToServer();
myRules.add(rule);
return new RuleBuilderFinished();
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished onType(Class<? extends IBaseResource> theType) {
validateType(theType);
OperationRule rule = createRule();
rule.appliesToTypes(toTypeSet(theType));
myRules.add(rule);
return new RuleBuilderFinished();
}
private void validateType(Class<? extends IBaseResource> theType) {
Validate.notNull(theType, "theType must not be null");
return new RuleBuilderFinished(rule);
}
@Override
@ -395,7 +451,25 @@ public class RuleBuilder implements IAuthRuleBuilder {
OperationRule rule = createRule();
rule.appliesToInstancesOfType(toTypeSet(theType));
myRules.add(rule);
return new RuleBuilderFinished();
return new RuleBuilderFinished(rule);
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished onServer() {
OperationRule rule = createRule();
rule.appliesToServer();
myRules.add(rule);
return new RuleBuilderFinished(rule);
}
@Override
public IAuthRuleBuilderRuleOpClassifierFinished onType(Class<? extends IBaseResource> theType) {
validateType(theType);
OperationRule rule = createRule();
rule.appliesToTypes(toTypeSet(theType));
myRules.add(rule);
return new RuleBuilderFinished(rule);
}
private HashSet<Class<? extends IBaseResource>> toTypeSet(Class<? extends IBaseResource> theType) {
@ -404,20 +478,8 @@ public class RuleBuilder implements IAuthRuleBuilder {
return appliesToTypes;
}
@Override
public IAuthRuleFinished onAnyType() {
OperationRule rule = createRule();
rule.appliesToAnyType();
myRules.add(rule);
return new RuleBuilderFinished();
}
@Override
public IAuthRuleFinished onAnyInstance() {
OperationRule rule = createRule();
rule.appliesToAnyInstance();
myRules.add(rule);
return new RuleBuilderFinished();
private void validateType(Class<? extends IBaseResource> theType) {
Validate.notNull(theType, "theType must not be null");
}
}
@ -440,7 +502,7 @@ public class RuleBuilder implements IAuthRuleBuilder {
rule.setOp(myRuleOp);
rule.setTransactionAppliesToOp(TransactionAppliesToEnum.ANY_OPERATION);
myRules.add(rule);
return new RuleBuilderFinished();
return new RuleBuilderFinished(rule);
}
}

View File

@ -9,9 +9,9 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -20,49 +20,55 @@ package ca.uhn.fhir.rest.server.interceptor.auth;
* #L%
*/
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.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.interceptor.auth.AuthorizationInterceptor.Verdict;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import java.util.Set;
public class RuleImplConditional extends BaseRule implements IAuthRule {
private AppliesTypeEnum myAppliesTo;
private Set<?> myAppliesToTypes;
private RestOperationTypeEnum myOperationType;
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
public RuleImplConditional(String theRuleName) {
RuleImplConditional(String theRuleName) {
super(theRuleName);
}
@Override
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
IRuleApplier theRuleApplier) {
IRuleApplier theRuleApplier) {
if (theInputResourceId != null) {
return null;
}
if (theOperation == myOperationType) {
switch (myAppliesTo) {
case ALL_RESOURCES:
case INSTANCES:
break;
case TYPES:
if (theInputResource == null || !myAppliesToTypes.contains(theInputResource.getClass())) {
return null;
}
break;
case ALL_RESOURCES:
case INSTANCES:
break;
case TYPES:
if (theInputResource == null || !myAppliesToTypes.contains(theInputResource.getClass())) {
return null;
}
break;
}
if (theRequestDetails.getConditionalUrl(myOperationType) == null) {
return null;
}
if (myTenantApplicabilityChecker != null) {
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
return null;
}
}
return newVerdict();
}
@ -81,4 +87,8 @@ public class RuleImplConditional extends BaseRule implements IAuthRule {
myOperationType = theOperationType;
}
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
}
}

View File

@ -52,6 +52,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
private RuleOpEnum myOp;
private TransactionAppliesToEnum myTransactionAppliesToOp;
private List<IIdType> myAppliesToInstances;
private RuleBuilder.ITenantApplicabilityChecker myTenantApplicabilityChecker;
public RuleImplOp(String theRuleName) {
super(theRuleName);
@ -60,6 +61,13 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
@Override
public Verdict applyRule(RestOperationTypeEnum theOperation, RequestDetails theRequestDetails, IBaseResource theInputResource, IIdType theInputResourceId, IBaseResource theOutputResource,
IRuleApplier theRuleApplier) {
if (myTenantApplicabilityChecker != null) {
if (!myTenantApplicabilityChecker.applies(theRequestDetails)) {
return null;
}
}
FhirContext ctx = theRequestDetails.getServer().getFhirContext();
IBaseResource appliesToResource;
@ -275,6 +283,10 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
return newVerdict();
}
public void setTenantApplicabilityChecker(RuleBuilder.ITenantApplicabilityChecker theTenantApplicabilityChecker) {
myTenantApplicabilityChecker = theTenantApplicabilityChecker;
}
@Override
public String toString() {
ToStringBuilder builder = new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE);
@ -282,6 +294,7 @@ class RuleImplOp extends BaseRule /* implements IAuthRule */ {
builder.append("transactionAppliesToOp", myTransactionAppliesToOp);
builder.append("appliesTo", myAppliesTo);
builder.append("appliesToTypes", myAppliesToTypes);
builder.append("appliesToTenant", myTenantApplicabilityChecker);
builder.append("classifierCompartmentName", myClassifierCompartmentName);
builder.append("classifierCompartmentOwners", myClassifierCompartmentOwners);
builder.append("classifierType", myClassifierType);

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-apache</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-client-okhttp</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jersey</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-sample-server-jpa</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-spring-boot</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
</parent>
<artifactId>hapi-fhir-spring-boot-samples</artifactId>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -16,6 +16,7 @@ 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.tenant.UrlBaseTenantIdentificationStrategy;
import ca.uhn.fhir.util.PortUtil;
import ca.uhn.fhir.util.TestUtil;
import org.apache.commons.io.IOUtils;
@ -65,9 +66,10 @@ public class AuthorizationInterceptorR4Test {
@Before
public void before() {
ourCtx.setAddProfileTagWhenEncoding(AddProfileTagEnum.NEVER);
for (IServerInterceptor next : new ArrayList<IServerInterceptor>(ourServlet.getInterceptors())) {
for (IServerInterceptor next : new ArrayList<>(ourServlet.getInterceptors())) {
ourServlet.unregisterInterceptor(next);
}
ourServlet.setTenantIdentificationStrategy(null);
ourReturn = null;
ourHitMethod = false;
ourConditionalCreateId = "1123";
@ -236,6 +238,50 @@ public class AuthorizationInterceptorR4Test {
}
@Test
public void testAllowAllForTenant() throws Exception {
ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.deny("Rule 1").read().resourcesOfType(Patient.class).withAnyId().forTenantIds("TENANTA").andThen()
.allowAll("Default Rule")
.build();
}
});
HttpGet httpGet;
HttpResponse status;
String response;
ourHitMethod = false;
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Observation/10");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
ourReturn = Collections.singletonList(createPatient(2));
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient/1");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
ourLog.info(response);
assertThat(response, containsString("Access denied by rule: Rule 1"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient/1/$validate");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
/**
* #528
*/
@ -244,7 +290,10 @@ public class AuthorizationInterceptorR4Test {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder().allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/845bd9f1-3635-4866-a6c8-1ca085df5c1a")).andThen().denyAll().build();
return new RuleBuilder()
.allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/845bd9f1-3635-4866-a6c8-1ca085df5c1a"))
.andThen().denyAll()
.build();
}
});
@ -269,6 +318,40 @@ public class AuthorizationInterceptorR4Test {
}
@Test
public void testAllowByCompartmentWithAnyTypeWithTenantId() throws Exception {
ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").read().allResources().inCompartment("Patient", new IdType("Patient/845bd9f1-3635-4866-a6c8-1ca085df5c1a")).forTenantIds("TENANTA")
.andThen().denyAll()
.build();
}
});
HttpGet httpGet;
HttpResponse status;
ourHitMethod = false;
ourReturn = Collections.singletonList(createCarePlan(10, "845bd9f1-3635-4866-a6c8-1ca085df5c1a"));
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/CarePlan/135154");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourHitMethod = false;
ourReturn = Collections.singletonList(createCarePlan(10, "FOO"));
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/CarePlan/135154");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
/**
* #528
*/
@ -521,6 +604,42 @@ public class AuthorizationInterceptorR4Test {
}
/**
* #528
*/
@Test
public void testDenyActionsNotOnTenant() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.ALLOW) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder().denyAll().notForTenantIds("TENANTA", "TENANTB").build();
}
});
HttpGet httpGet;
HttpResponse status;
String response;
ourReturn = Collections.singletonList(createPatient(2));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient/1");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTC/Patient/1");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
ourLog.info(response);
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
/**
* #528
*/
@ -660,7 +779,7 @@ public class AuthorizationInterceptorR4Test {
}
@Test
public void testInvalidInstanceIds() throws Exception {
public void testInvalidInstanceIds() {
try {
new RuleBuilder().allow("Rule 1").write().instance((String) null);
fail();
@ -1176,6 +1295,43 @@ public class AuthorizationInterceptorR4Test {
assertFalse(ourHitMethod);
}
@Test
public void testOperationTypeLevelWithTenant() throws Exception {
ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("RULE 1").operation().named("opName").onType(Patient.class).forTenantIds("TENANTA").andThen()
.build();
}
});
HttpGet httpGet;
HttpResponse status;
String response;
// Right Tenant
ourHitMethod = false;
ourReturn = Collections.singletonList(createPatient(2));
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient/$opName");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
ourLog.info(response);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
// Wrong Tenant
ourHitMethod = false;
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTC/Patient/$opName");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
assertThat(response, containsString("Access denied by default policy"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@Test
public void testOperationTypeLevelWildcard() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -1307,6 +1463,79 @@ public class AuthorizationInterceptorR4Test {
}
@Test
public void testReadByAnyIdWithTenantId() throws Exception {
ourServlet.setTenantIdentificationStrategy(new UrlBaseTenantIdentificationStrategy());
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@Override
public List<IAuthRule> buildRuleList(RequestDetails theRequestDetails) {
return new RuleBuilder()
.allow("Rule 1").read().resourcesOfType(Patient.class).withAnyId().forTenantIds("TENANTA")
.build();
}
});
HttpGet httpGet;
HttpResponse status;
String response;
ourReturn = Collections.singletonList(createPatient(2));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient/1");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourReturn = Collections.singletonList(createPatient(2));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTB/Patient/1");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
ourReturn = Collections.singletonList(createPatient(2));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient/1/_history/222");
status = ourClient.execute(httpGet);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourReturn = Collections.singletonList(createObservation(10, "Patient/2"));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Observation/10");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
ourLog.info(response);
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
ourReturn = Arrays.asList(createPatient(1), createObservation(10, "Patient/2"));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
ourLog.info(response);
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
ourReturn = Arrays.asList(createPatient(2), createObservation(10, "Patient/1"));
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/TENANTA/Patient");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
ourLog.info(response);
assertThat(response, containsString("Access denied by default policy (no applicable rules)"));
assertEquals(403, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
@Test
public void testReadByCompartmentRight() throws Exception {
ourServlet.registerInterceptor(new AuthorizationInterceptor(PolicyEnum.DENY) {
@ -1438,7 +1667,7 @@ public class AuthorizationInterceptorR4Test {
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Observation/900");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -1446,7 +1675,7 @@ public class AuthorizationInterceptorR4Test {
ourHitMethod = false;
httpGet = new HttpGet("http://localhost:" + ourPort + "/Patient/901");
status = ourClient.execute(httpGet);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -1551,7 +1780,7 @@ public class AuthorizationInterceptorR4Test {
ourHitMethod = false;
httpGet = new HttpGet(respBundle.getLink("next").getUrl());
status = ourClient.execute(httpGet);
respString = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
@ -1640,7 +1869,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPost("http://localhost:" + ourPort + "/Observation");
httpPost.setEntity(createFhirResourceEntity(createObservation(null, "Patient/1")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(201, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
}
@ -1886,7 +2115,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?foo=bar");
httpPost.setEntity(createFhirResourceEntity(createPatient(null)));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -1924,7 +2153,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPut("http://localhost:" + ourPort + "/Patient?foo=bar");
httpPost.setEntity(createFhirResourceEntity(createPatient(null)));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -1961,7 +2190,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation/900");
httpPost.setEntity(createFhirResourceEntity(createObservation(900, "Patient/12")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -1969,7 +2198,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPut("http://localhost:" + ourPort + "/Observation/901");
httpPost.setEntity(createFhirResourceEntity(createObservation(901, "Patient/12")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(200, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -2008,7 +2237,6 @@ public class AuthorizationInterceptorR4Test {
HttpEntityEnclosingRequestBase httpPost;
HttpResponse status;
String response;
String input = "[ { \"op\": \"replace\", \"path\": \"/gender\", \"value\": \"male\" } ]";
@ -2016,7 +2244,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/900");
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(204, status.getStatusLine().getStatusCode());
assertTrue(ourHitMethod);
@ -2024,7 +2252,7 @@ public class AuthorizationInterceptorR4Test {
httpPost = new HttpPatch("http://localhost:" + ourPort + "/Patient/999");
httpPost.setEntity(new StringEntity(input, ContentType.parse("application/json-patch+json")));
status = ourClient.execute(httpPost);
response = extractResponseAndClose(status);
extractResponseAndClose(status);
assertEquals(403, status.getStatusLine().getStatusCode());
assertFalse(ourHitMethod);
}
@ -2085,6 +2313,7 @@ public class AuthorizationInterceptorR4Test {
}
}
@SuppressWarnings("unused")
public static class DummyEncounterResourceProvider implements IResourceProvider {
@Operation(name = "everything", idempotent = true)
@ -2103,6 +2332,7 @@ public class AuthorizationInterceptorR4Test {
}
}
@SuppressWarnings("unused")
public static class DummyObservationResourceProvider implements IResourceProvider {
@Create()
@ -2118,8 +2348,7 @@ public class AuthorizationInterceptorR4Test {
@Delete()
public MethodOutcome delete(@IdParam IdType theId) {
ourHitMethod = true;
MethodOutcome retVal = new MethodOutcome();
return retVal;
return new MethodOutcome();
}
@Override
@ -2201,8 +2430,7 @@ public class AuthorizationInterceptorR4Test {
for (IBaseResource next : ourReturn) {
theRequestOperationCallback.resourceDeleted(next);
}
MethodOutcome retVal = new MethodOutcome();
return retVal;
return new MethodOutcome();
}
@Operation(name = "everything", idempotent = true)

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>

View File

@ -5,7 +5,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -11,7 +11,7 @@
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<packaging>pom</packaging>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<name>HAPI-FHIR</name>
<url>https://hapifhir.io</url>

View File

@ -4,7 +4,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -8,7 +8,7 @@
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir</artifactId>
<version>3.2.0</version>
<version>3.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

View File

@ -7,6 +7,20 @@
</properties>
<body>
<release version="3.3.0" date="TBD">
<action type="add">
This release corrects an ineffiency in the JPA Server, but requires a schema
change in order to update. Prior to this version of HAPI FHIR, a CLOB column
containing the complete resource body was stored in two
tables: HFJ_RESOURCE and HFJ_RES_VER. Because the same content was stored in two
places, the database consumed more space than is needed to.
<![CDATA[<br/><br/>]]>
In order to reduce this duplication, the columns have been removed from the
HFJ_RESOURCE column. This means that on any database that is being upgraded
to HAPI FHIR 3.2.0, you will need to remove the columns
<![CDATA[<code>RES_TEXT</code> and <code>RES_ENCODING</code>]]> (or
set them to nullable if you want an easy means of rolling back). Naturally
you should back your database up prior to making this change.
</action>
<action type="fix">
Fix a crash in the JSON parser when parsing extensions on repeatable
elements (e.g. Patient.address.line) where there is an extension on the

View File

@ -1,222 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>Server Security</title>
</properties>
<body>
<!-- The body of the document contains a number of sections -->
<section name="Server Security">
<p>
Security is a complex topic which goes far beyond the scope of HAPI FHIR.
HAPI does provide mechanisms which can be used to implement security in
your server however.
</p>
<p>
Because HAPI FHIR's REST server is based on the Servlet API, you may use any
security mechanism which works in that environment. Some serlvet containers
may provide security layers you can plug into. The rest of this page
does not explore that method, but rather looks at HAPI FHIR hooks that can
be used to implement FHIR specific security.
</p>
<subsection name="Authentication vs Authorization">
<p>
Background reading: <a href="https://en.wikipedia.org/wiki/Authentication">Wikipedia - Authentication</a>
</p>
<p>
Server security is divided into two topics:
</p>
<ul>
<li>
<b>Authentication (AuthN):</b> Is verifying that the user is who they say they
are. This is typically accomplished by testing a username/password in the request,
or by checking a "bearer token" in the request.
</li>
<li>
<b>Authorization (AuthZ):</b> Is verifying that the user is allowed to perform
the given action. For example, in a FHIR application you might use AuthN to test that
the user making a request to the FHIR server is allowed to access the server, but
that test might determine that the requesting user is not permitted to perform
write operations and therefore block a FHIR <code>create</code> operation. This is
AuthN and AuthZ in action.
</li>
</ul>
</subsection>
</section>
<section name="Authentication Interceptors">
<p>
The <a href="./doc_rest_server_interceptor.html">Server Interceptor</a>
framework can provide an easy way to test for credentials. The following
example shows a simple interceptor which tests for HTTP Basic Auth.
</p>
<macro name="snippet">
<param name="id" value="basicAuthInterceptor" />
<param name="file" value="examples/src/main/java/example/SecurityInterceptors.java" />
</macro>
<subsection name="HTTP Basic Auth">
<p>
Note that if you are implementing HTTP Basic Auth, you may want to
return a <code>WWW-Authenticate</code> header with the response.
The following snippet shows how to add such a header with a custom
realm:
</p>
<macro name="snippet">
<param name="id" value="basicAuthInterceptorRealm" />
<param name="file" value="examples/src/main/java/example/SecurityInterceptors.java" />
</macro>
</subsection>
</section>
<section name="Authorization Interceptor">
<p>
HAPI FHIR 1.5 introduced a new interceptor, the
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.html">AuthorizationInterceptor</a>.
</p>
<p>
This interceptor can help with the complicated task of determining whether a user
has the appropriate permission to perform a given task on a FHIR server. This is
done by declaring a set of rules that can selectively allow (whitelist) and/or selectively
block (blacklist) requests.
</p>
<p class="doc_info_bubble">
AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet
been heavily tested. Use with caution, and do lots of testing! We welcome
feedback and suggestions on this feature. In addition, this documentation is
not yet complete. More examples and details will be added soon! Please get in
touch if you'd like to help test, have suggestions, etc.
</p>
<p>
The AuthorizationInterceptor works by allowing you to declare
permissions based on an individual request coming in. In other
words, you could have code that examines an incoming request and
determines that it is being made by a Patient with ID 123. You
could then declare that the requesting user has access to read and
write any resource in compartment "Patient/123", which corresponds
to any Observation, MedicationOrder etc with a subject of
"<code>Patient/123</code>". On the other hand, another request
might be detemrined to belong to an administrator user, and
could be declared to be allowed to do anything.
</p>
<p>
The AuthorizationInterceptor is used by subclassing it and then registering your
subclass with the <code>RestfulServer</code>. The following example shows a subclassed
interceptor implementing some basic rules:
</p>
<macro name="snippet">
<param name="id" value="patientAndAdmin" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
<subsection name="Using AuthorizationInterceptor in a REST Server">
<p>
The AuthorizationInterceptor works by examining the client request
in order to determine whether "write" operations are legal, and looks at
the response from the server in order to determine whether "read" operations
are legal.
</p>
</subsection>
<subsection name="Authorizing Read Operations">
<p>
When authorizing a read operation, the AuthorizationInterceptor
always allows client code to execute and generate a response.
It then examines the response that would be returned before
actually returning it to the client, and if rules do not permit
that data to be shown to the client the interceptor aborts the
request.
</p>
<p>
Note that there are performance implications to this mechanism,
since an unauthorized user can still cause the server to fetch data
even if they won't get to see it. This mechanism should be comprehensive
however, since it will prevent clients from using various features
in FHIR (e.g. <code>_include</code> or <code>_revinclude</code>) to
"trick" the server into showing them date they shouldn't be allowed to
see.
</p>
<p>
See the following diagram for an example of how this works.
</p>
<img src="./images/hapi_authorizationinterceptor_read_normal.svg" alt="Write Authorization"/>
</subsection>
<subsection name="Authorizing Write Operations">
<p>
Write operations (create, update, etc.) are typically authorized
by the interceptor by examining the parsed URL and making a decision
about whether to authorize the operation before allowing Resource Provider
code to proceed. This means that client code will not have a chance to execute
and create resources that the client does not have permissions to create.
</p>
<p>
See the following diagram for an example of how this works.
</p>
<img src="./images/hapi_authorizationinterceptor_write_normal.svg" alt="Write Authorization"/>
</subsection>
<subsection name="Authorizing Sub-Operations">
<p>
There are a number of situations where the REST framework doesn't
actually know exactly what operation is going to be performed by
the implementing server code. For example, if your server implements
a <code>conditional update</code> operation, the server might not know
which resource is actually being updated until the server code
is executed.
</p>
<p>
Because client code is actually determining which resources are
being modified, the server can not automatically apply security
rules against these modifications without being provided hints
from client code.
</p>
<p>
In this type of situation, it is important to manually
notify the interceptor chain about the "sub-operation" being performed.
The following snippet shows how to notify interceptors about
a conditional create.
</p>
<macro name="snippet">
<param name="id" value="conditionalUpdate" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
</subsection>
</section>
</body>
</document>
<?xml version="1.0" encoding="UTF-8"?>
<document xmlns="http://maven.apache.org/XDOC/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/XDOC/2.0 http://maven.apache.org/xsd/xdoc-2.0.xsd">
<properties>
<title>Server Security</title>
</properties>
<body>
<!-- The body of the document contains a number of sections -->
<section name="Server Security">
<p>
Security is a complex topic which goes far beyond the scope of HAPI FHIR.
HAPI does provide mechanisms which can be used to implement security in
your server however.
</p>
<p>
Because HAPI FHIR's REST server is based on the Servlet API, you may use any
security mechanism which works in that environment. Some serlvet containers
may provide security layers you can plug into. The rest of this page
does not explore that method, but rather looks at HAPI FHIR hooks that can
be used to implement FHIR specific security.
</p>
<subsection name="Authentication vs Authorization">
<p>
Background reading: <a href="https://en.wikipedia.org/wiki/Authentication">Wikipedia - Authentication</a>
</p>
<p>
Server security is divided into two topics:
</p>
<ul>
<li>
<b>Authentication (AuthN):</b> Is verifying that the user is who they say they
are. This is typically accomplished by testing a username/password in the request,
or by checking a "bearer token" in the request.
</li>
<li>
<b>Authorization (AuthZ):</b> Is verifying that the user is allowed to perform
the given action. For example, in a FHIR application you might use AuthN to test that
the user making a request to the FHIR server is allowed to access the server, but
that test might determine that the requesting user is not permitted to perform
write operations and therefore block a FHIR <code>create</code> operation. This is
AuthN and AuthZ in action.
</li>
</ul>
</subsection>
</section>
<section name="Authentication Interceptors">
<p>
The <a href="./doc_rest_server_interceptor.html">Server Interceptor</a>
framework can provide an easy way to test for credentials. The following
example shows a simple interceptor which tests for HTTP Basic Auth.
</p>
<macro name="snippet">
<param name="id" value="basicAuthInterceptor" />
<param name="file" value="examples/src/main/java/example/SecurityInterceptors.java" />
</macro>
<subsection name="HTTP Basic Auth">
<p>
Note that if you are implementing HTTP Basic Auth, you may want to
return a <code>WWW-Authenticate</code> header with the response.
The following snippet shows how to add such a header with a custom
realm:
</p>
<macro name="snippet">
<param name="id" value="basicAuthInterceptorRealm" />
<param name="file" value="examples/src/main/java/example/SecurityInterceptors.java" />
</macro>
</subsection>
</section>
<section name="Authorization Interceptor">
<p>
HAPI FHIR 1.5 introduced a new interceptor, the
<a href="./apidocs/ca/uhn/fhir/rest/server/interceptor/auth/AuthorizationInterceptor.html">AuthorizationInterceptor</a>.
</p>
<p>
This interceptor can help with the complicated task of determining whether a user
has the appropriate permission to perform a given task on a FHIR server. This is
done by declaring a set of rules that can selectively allow (whitelist) and/or selectively
block (blacklist) requests.
</p>
<p class="doc_info_bubble">
AuthorizationInterceptor is a new feature in HAPI FHIR, and has not yet
been heavily tested. Use with caution, and do lots of testing! We welcome
feedback and suggestions on this feature. In addition, this documentation is
not yet complete. More examples and details will be added soon! Please get in
touch if you'd like to help test, have suggestions, etc.
</p>
<p>
The AuthorizationInterceptor works by allowing you to declare
permissions based on an individual request coming in. In other
words, you could have code that examines an incoming request and
determines that it is being made by a Patient with ID 123. You
could then declare that the requesting user has access to read and
write any resource in compartment "Patient/123", which corresponds
to any Observation, MedicationOrder etc with a subject of
"<code>Patient/123</code>". On the other hand, another request
might be detemrined to belong to an administrator user, and
could be declared to be allowed to do anything.
</p>
<p>
The AuthorizationInterceptor is used by subclassing it and then registering your
subclass with the <code>RestfulServer</code>. The following example shows a subclassed
interceptor implementing some basic rules:
</p>
<macro name="snippet">
<param name="id" value="patientAndAdmin" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
<subsection name="Using AuthorizationInterceptor in a REST Server">
<p>
The AuthorizationInterceptor works by examining the client request
in order to determine whether "write" operations are legal, and looks at
the response from the server in order to determine whether "read" operations
are legal.
</p>
</subsection>
<subsection name="Authorizing Read Operations">
<p>
When authorizing a read operation, the AuthorizationInterceptor
always allows client code to execute and generate a response.
It then examines the response that would be returned before
actually returning it to the client, and if rules do not permit
that data to be shown to the client the interceptor aborts the
request.
</p>
<p>
Note that there are performance implications to this mechanism,
since an unauthorized user can still cause the server to fetch data
even if they won't get to see it. This mechanism should be comprehensive
however, since it will prevent clients from using various features
in FHIR (e.g. <code>_include</code> or <code>_revinclude</code>) to
"trick" the server into showing them date they shouldn't be allowed to
see.
</p>
<p>
See the following diagram for an example of how this works.
</p>
<img src="./images/hapi_authorizationinterceptor_read_normal.svg" alt="Write Authorization"/>
</subsection>
<subsection name="Authorizing Write Operations">
<p>
Write operations (create, update, etc.) are typically authorized
by the interceptor by examining the parsed URL and making a decision
about whether to authorize the operation before allowing Resource Provider
code to proceed. This means that client code will not have a chance to execute
and create resources that the client does not have permissions to create.
</p>
<p>
See the following diagram for an example of how this works.
</p>
<img src="./images/hapi_authorizationinterceptor_write_normal.svg" alt="Write Authorization"/>
</subsection>
<subsection name="Authorizing Sub-Operations">
<p>
There are a number of situations where the REST framework doesn't
actually know exactly what operation is going to be performed by
the implementing server code. For example, if your server implements
a <code>conditional update</code> operation, the server might not know
which resource is actually being updated until the server code
is executed.
</p>
<p>
Because client code is actually determining which resources are
being modified, the server can not automatically apply security
rules against these modifications without being provided hints
from client code.
</p>
<p>
In this type of situation, it is important to manually
notify the interceptor chain about the "sub-operation" being performed.
The following snippet shows how to notify interceptors about
a conditional create.
</p>
<macro name="snippet">
<param name="id" value="conditionalUpdate" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
</subsection>
<subsection name="Authorizing Multitenant Servers">
<p>
The AuthorizationInterceptor has the ability to direct individual
rules as only applying to a single tenant in a multitenant
server. The following example shows such a rule.
</p>
<macro name="snippet">
<param name="id" value="authorizeTenantAction" />
<param name="file" value="examples/src/main/java/example/AuthorizationInterceptors.java" />
</macro>
</subsection>
</section>
</body>
</document>